Goroutine
什么是Goroutine
- Goroutine是Golang特有的并发体,是一种轻量级的”线程”
- Go中最基本的执行单元,每个Goroutine独立执行
- 每一个Go程序至少有一个Goroutine:主Goroutine。当程序启动时,它会自动创建。
1 | go复制代码func main() { |
Vs Os Thread
系统线程 Os Thread
每个系统线程有固定大小的栈,一般默认2M,这个栈主要用来保存函数递归调用时参数和局部变量,由内核调度
固定大小的栈就会带来问题
- 空间浪费
- 空间可能又不够,存在栈溢出的风险
Goroutine
由Go的调度器调度,刚创建的时候很小(2kb或者4kb),会根据需要动态地伸缩栈的大小(主流实现中栈的最大值可达到1GB)。
因为启动的代价很小,所以我们可以轻易地启动成千上万个Goroutine。
通过示例了解
1. 多个goroutine同时运行
运行的顺序由调度器决定,不需要相互依赖
1 | go复制代码func main() { |
2. 图片并发下载
1 | go复制代码func main() { |
Recover
每个Goroutine都要有recover机制,因为当一个Goroutine抛panic的时候只有自身能够捕捉到其它Goroutine是没有办法捕捉的。
如果没有recover机制,整个进程会crash。
注意:Goroutine发生panic时,只会调用自身的defer,所以即便主Goroutine里写了recover逻辑,也无法recover。
1 | go复制代码func main() { |
Channel
基本介绍
Channel是Go内置的数据类型,为初始化的channel的值为nil
通过发送和接收指定元素类型的值来进行通信
- Channel 提供了 goroutines 之间的同步和通信
- Goroutine 实现并发/并行的轻量级独立执行。
Shard Memory
thread1Memorythread2thread3
CSP
Communicating sequential processes 通信顺序编程
用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型,
不关注发送消息的实体,而关注与发送消息时使用的channel
不要通过共享内存来通信,而通过通信来共享内存。 – Rob Pike
Goroutine1ChannelGoroutine2
数据结构
1 | go复制代码type hchan struct { |
基本用法
定义
1 | go复制代码ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType . |
<-
运算符指定通道方向, *发送或接收。如果没有给出方向,则通道是 *双向的
1 | go复制代码chan T // 可以发送接收T |
创建
1 | go复制代码ch := make(chan int) // 无缓冲 cap 0 |
操作
1 | go复制代码ch <- 1. // 发送 |
代码示例
1 | go复制代码func goroutineA(ch <-chan int) { |
groutineAchannelgroutineBhello,我想要获取一个数据我现在还没有数据那我睡觉了,等有数据再叫醒我okhello,我要发送一个数据给你ok,发过来吧醒醒,接收数据啦来咯groutineAchannelgroutineB
Unbuffered channels
缓冲区大小为0的channel
channel接收者会阻塞,直到收到消息,channel发送者会阻塞,直到接收者收到消息
Buffered channels
拥有缓冲区,当缓冲区已满时,发送者会阻塞;当缓冲区为空时,接收者会阻塞
总结
不要关注channel的数据结构,更应该关注channel的行为
Command | nil | empty | full | not full & empty | closed |
---|---|---|---|---|---|
Receive | block | block | success | success | success |
Send | block | success | block | success | panic |
Close | panic | success | success | success | panic |
几条原则
- channel 上的发送操作总在对应的接收操作完成前发生
- 如果 channel 关闭后从中接收数据,接受者就会收到该 channel 返回的零值
- 从无缓冲的 channel 中进行的接收,要发生在对该 channel 进行的发送完成前
- 不要在数据接收方或者在有多个发送者的情况下关闭通道。换句话说,我们只应该让一个通道唯一的发送者关闭此通道
示例
1 | go复制代码package main |
执行之后会报错
1 | go复制代码fatal error: all goroutines are asleep - deadlock! |
原因?
第7行 给通道ch1传入值 hello world
,但是对于无缓冲的通道,在接收者未准备好前发送操作是阻塞的,缺少接收者造成死锁
如何解决?
1. 增加接收者
1 | go复制代码func main() { |
2. 增加channel容量
1 | go复制代码func main() { |
Goroutine & Channel 串起来
常见的并发模式
通知
- 向一个通道发送一个值实现通知
1 | go复制代码func main() { |
- 从一个通道接收值实现通知
1 | go复制代码func main() { |
互斥锁
1 | go复制代码func main() { |
控制协程的并发数量
不限制的场景
1 | go复制代码func main() { |
运行结果
1 | go复制代码$ go run main.go |
问题:如何限制协程的数量?
1 | go复制代码// main_chan.go |
生产者消费者模型
1 | go复制代码// 生产者: 生成 factor 整数倍的序列 |
返回最优的结果
1 | go复制代码func main() { |
问题1:
当获得想要的结果之后,如何通知或者安全退出其他还在执行的协程?
1 | go复制代码func main() { |
问题2:
如何做超时控制?
Goroutine 泄漏
1. 被遗忘的发送者
1 | go复制代码func searchByBaidu(search string,cancel chan struct{}) string { |
case <- done
和 case <- cancel
不确定会执行哪一个,如果执行 <-cancel
,则第五行 done <- struct{}{}
会永远阻塞,Goroutine无法退出
如何解决?
增加channel容量
1 | go复制代码done := make(chan struct{}) |
还有其他办法吗?
1 | go复制代码func searchByBaidu(search string,cancel chan struct{}) string { |
2. 被遗忘的接收者
1 | go复制代码import ( |
这个会产生Goroutine的泄漏,原因是第49行的for v := range input
当没有数据输入的时候还在继续等待输入,所有应该在没有数据输入的时候告诉它,让它不要傻傻的等
如何解决?
- 关闭input 通道
- 传递一个通道告诉它结果
原则
- 永远不要在不知道如何停止的情况下启动Goroutine,当我们启动一个Goroutine的时候需要考虑几个问题
* 什么时候停止?
* 可以通过什么方式终止它?
- 将并发留给调用者
* 请将是否异步调用的选择权交给调用者,不然很有可能调用者并不知道你在这个函数里面使用了Goroutine
* 如果你的函数启动了一个 Goroutine,您必须为调用者提供一种明确停止该 Goroutine 的方法。将异步执行函数的决定留给该函数的调用者通常更容易。
总结
Concurrency is a useful tool, but it must be used with caution.
并发是一个有用的工具,但是必须谨慎使用
参考链接
本文转载自: 掘金