为什么需要 Goroutine
简单一句话,Goroutine 相比线程拥有更优越的开销性能。
这里先简单介绍下进程、线程、协程:
- 进程:操作系统创建、资源分配的基本单位、同一个进程内的线程会共享资源。
- 线程:操作系统创建、CPU 调度的基本单位、有独立的堆栈空间
- 协程:可通过用户程序创建
- 有栈协程:golang
- 无栈协程:c++,rust 等
Goroutine 就是一个用户级线程,相比传统线程更加轻量(传统协程 1 MB,Goroutine 约 2 KB)。其不涉及内核态的切换,因此 golang 的并发性能很好。
如何关闭 Goroutine
关闭 channel
1 2
// 根据第二个参数进行判别,当关闭 channel 时,根据其返回结果跳出 msg, ok := <-ch
定期轮询 channel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
func main() { ch := make(chan string, 6) done := make(chan struct{}) go func() { for { select { case ch <- "hello world": case <-done: close(ch) return } } }() go func() { time.Sleep(3 * time.Second) done <- struct{}{} }() for i := range ch { fmt.Println("接收到的值:", i) } fmt.Println("over") }
变量 done 作为 channel 类型,用作信号量处理 Goroutine 的关闭。for-loop 结合 select 进行监听,处理完业务之后才会调用 close 关闭 channel。
使用 context
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
func main() { ch := make(chan struct{}) ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { for { select { case <-ctx.Done(): ch <- struct{}{} return default: fmt.Println("hello world") } time.Sleep(1 * time.Second) } }(ctx) go func() { time.Sleep(4 * time.Second) cancel() }() <-ch fmt.Println("over") }
在 context 中,可以借助 ctx.Done 获取一个只读的 channel,可用来识别当前 channel 是否已被关闭。context 对于跨 Goroutine 控制灵活,可以调动 context.WithTimeout 根据时间,或者主动调用 cancel 方法手动关闭。
如何实现并行 Goroutine
通过设置最大的可同时使用的 CPU 核心数
|
|
为什么不能大量使用 Goroutine
- 虽然 Goroutine 的初始栈(自动扩容)很小,但是大部分业务需要更多的栈空间,而频繁的扩容需要很大的成本。
- Golang 的 GMP 调度模型中的 M 和 P 是有数量限制的,大量的 Goroutine 会导致过长的调度队列,从而影响性能。
- 过多的 Goroutine 还会导致频繁的 GC,影响性能。
Goroutine A 能否停止另一个 Goroutine
不能。Goroutine 只有自己主动退出,不能被外界的 Goroutine 关闭。
父 Goroutine 退出,子 Goroutine 一定会退出嘛
- 当父 Goroutine 为 main 时,所有的子 Goroutine 都会跟着父 Goroutine 退出
- 若父 Goroutine 不为 main 时,子 Goroutine 不会跟着父 Goroutine 退出
Goroutine 的状态流转
状态 | 含义 | |
---|---|---|
_Gidle | 空闲态 | G 刚刚创建,还未初始化 |
_Grunnable | 就绪态 | G 在运行队列,等待 M 取出(此时 M 有 P) |
_Grunning | 运行态 | M 正在运行 G |
_Gsyscall | 系统调用 | M 中运行的 G 发起系统调用(此时 M 无 P) |
_Gwaiting | 阻塞态 | G 等待执行资源 |
_Gdead | 完成态 | G 已经执行完毕 |
_Gcopystack | 复制栈 | G 正获取一个新的栈空间,并将原内容复制过去 |
Goroutine 泄露
Goroutine 没有被正确的关闭或管理,会导致他们在程序运行过程中无法被回收,最终导致资源浪费和潜在的性能问题
泄露原因
- Goroutine 内部进行 channel/mutex 等读写操作被一直阻塞
- Goroutine 内的业务逻辑进入死循环,资源无法释放
- Goroutine 内的业务逻辑进入长时间等待,又不断新增的 Goroutine 进入等待
泄露场景
- 未初始化 channel
- channel 发送未接收
- channel 接收未发送
- 资源连接未关闭
- 未成功解锁
- 死循环
- sync.WaitGroup 使用不当
多个协程交替打印奇偶数字
|
|