java并发编程流程:
不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存
多线程访问一个内存时
需要限制同一时间能够读写这些变量的线程数量
Go 语言提供了一种不同的并发模型,即通信顺序进程(Communicating sequential processes,CSP)
Goroutine 和 Channel 分别对应 CSP 中的实体和传递信息的媒介
两个 Goroutine不存在直接关联,但是能通过 Channel 间接完成通信。
Go 服务的每一个请求都是通过单独的 Goroutine 处理的
所以就要在不同 Goroutine 之间 同步请求特定数据、取消信号以及处理请求的截止日期。
用来设置截止日期、同步信号,传递请求相关值的结构体
作用:
对Goroutine 信号进行同步 以减少计算资源的浪费
原理
每一个 context.Context 都会从最顶层的 Goroutine 一层一层传递到最下层。
context.Context 可以在上层 Goroutine 执行出现错误时,将取消信号及时同步给下层,下层及时停掉无用的工作以减少额外资源的消耗:一旦接收到取消信号就立刻停止当前正在执行的工作。
接口
type Context interface {
// 上下文截止时间
Deadline() (deadline time.Time, ok bool)
// Channel,在上下文的工作完成后关闭
Done() <-chan struct{}
// 上下文结束的原因
Err() error
// 上下文中的 K-V
Value(key interface{}) interface{}
}
从 context.Context 中衍生出一个新的子上下文,并返回一个取消函数
执行取消函数,当前上下文以及它的子上下文都会被取消
context.WithValue 能从父上下文中创建一个子上下文,传值的子上下文使用 context.valueCtx 类型
Go 核心的数据结构, Goroutine 之间的通信方式
从某种程度上说,Channel 是一个用于同步和通信的有锁队列
Channel 与关键字 range 和 select 的关系紧密
Channel 收发操作均遵循了先进先出的设计,具体规则如下:
处理单个请求的多个Goroutine之间与请求域的数据、超时和退出等操作
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/
type Context interface {
// 上下文截止时间
Deadline() (deadline time.Time, ok bool)
// Channel,在上下文的工作完成后关闭
Done() <-chan struct{}
// 上下文结束的原因
Err() error
// 上下文中的 K-V
Value(key interface{}) interface{}
}
// 默认上下文, 最简单、最常用的上下文类型
context.Background()
// 取消信号: 衍生出一个新的子上下文 + 一个用于取消该上下文的函数
ctx, cancel := context.WithCancel(context.Background())
// 计时器上下文:衍生出一个过期时间为 1s 的上下文
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
func main() {
//当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始的上下文向下传递。
ctx, cancel := context.WithCancel(context.Background())
go handle(ctx)
cancel() // 通知子goroutine结束
}
func handle(ctx context.Context) {
select {
case <-ctx.Done(): // 等待上级通知
fmt.Println("handle", ctx.Err())
}
}
通道有发送(send)、接收(receive)和关闭(close)三种操作
ch <- 10 // 把10发送到ch中
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
close(ch)
对于从无缓冲Channel进行的接收,发生在对该Channel进行的发送完成之前(无缓冲Channel:接收完成,发送才能完成)
select 可以同时响应多个通道的操作。
同时监听多个channel,直到其中一个channel ready
控制goroutine数量,防止暴涨
减少goroutine的创建/销毁的性能损耗
没有官方包,根据需求自实现,
"gitee.com/johng/gf/g/os/grpool"包
https://github.com/panjf2000/ants/blob/master/README_ZH.md
runtime.Gosched()
让出CPU时间片,重新等待安排任务
runtime.Goexit()
退出当前协程
runtime.GOMAXPROCS()
设置当前程序并发时占用的CPU逻辑核心数
Time = time.Now()
输出当前时间
*Timer = time.NewTimer( time.Second)
时间到了,输出1次时间
timer1 := time.NewTimer(time.Second)
fmt.Println("时间到:", <-timer1.C)
*Timer = time.NewTicker( time.Second)
ticker1 := time.NewTicker(1 * time.Second)
// 一直在循环输出时间
for {
fmt.Println(<-ticker.C)
}
var lock sync.Mutex
func main() {
go add()
go add()
}
func add() {
lock.Lock() // 加锁, goroutine获取锁才能进入临界区
x = x + 1
lock.Unlock() // 解锁
}
分为 读锁和写锁, 读锁能多次获取,写锁需要等待
适合读多写少的场景
rwlock sync.RWMutex
rwlock.Lock() // 加写锁
rwlock.Unlock() // 解写锁
rwlock.RLock() // 加读锁
rwlock.RUnlock() // 解读锁
(wg *WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0
var wg sync.WaitGroup
func hello() {
defer wg.Done() //计数器-1
fmt.Println("Hello Goroutine!")
}
func main() {
wg.Add(1) //计数器+1
go hello()
wg.Wait() //等计数器变为0 才执行
fmt.Println("main goroutine done!")
}
var loadIconsOnce sync.Once
//func()只执行一次,并发安全
loadIconsOnce.Do(func())
var m = sync.Map{}
性能比加锁操作更好
和java用操作系统来调度线程不同:线程由 CPU 调度是抢占式的
go语言自己实现的一套调度系统,用来调度goroutine
调度是在用户态下完成的,不需要内核态切换,内存分配,协程由用户态调度是协作式的,一个协程让出 CPU 后,才执行下一个协程。
M与内核线程一般是一一映射
P与M一般也是一一对应
协程与线程 关系是M:N
当一个G长久阻塞在一个M1上时,
runtime会新建一个M2,阻塞G所在的P 会把其他的G挂载在新建的M2上。
当旧的G阻塞完成或者认为其已经死掉时 回收旧的M1。
go 并发相比线程同步,更推荐用channel通信
同步通信:后台线程执行后 ,主线程再退出
func main() {
var mu sync.Mutex
mu.Lock()
go func(){
fmt.Println("你好, 世界")
mu.Unlock()
}()
mu.Lock()
}
扩展到N个线程完成后,再进行下一步
func main() {
var wg sync.WaitGroup
// 开N个后台打印线程
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("你好, 世界")
}()
}
// 等待N个后台线程完成
wg.Wait()
}
同步通信:后台线程执行后 ,主线程再退出
func main() {
done := make(chan int, 1) // 带缓存的管道
go func(){
fmt.Println("你好, 世界")
done <- 1
}()
<-done
}
扩展到N个线程完成后,再进行下一步
func main() {
done := make(chan int, 10) // 带 10 个缓存
// 开N个后台打印线程
for i := 0; i < cap(done); i++ {
go func(){
fmt.Println("你好, 世界")
done <- 1
}()
}
// 等待N个后台线程完成
for i := 0; i < cap(done); i++ {
<-done
}
}
Go语言实现生产者消费者并发很简单:
当成果队列中没有数据时,消费者就进入饥饿的等待中;
而当成果队列中数据已满时,生产者则面临因产品挤压导致CPU被剥夺的下岗