Goroutines和Channels
Goroutines
和Channels
Go
语言中的并发程序可以用两种手段来实现. goroutine
和channel
, 其支持”顺序通信进程”(CSP)
. CSP
是一种现代的并发编程模型, 在这种编程模型中值会在不同的运行实例goroutine
中传递, 尽管大多数情况下仍然是被限制在单一实例中.
Goroutines
在Go
语言中, 每一个并发的执行单元叫作一个goroutine
. 当一个程序启动时, 其主函数即在一个单独的goroutine
中运行, 我们叫它main goroutine
. 新的gorountine
会用go
语句来创建. 在语法上, go
语句是一个普通的函数或方法调用前加上关键字go
. go
语句会使其语句中的函数在一个新创建的goroutine
中运行. 而go
语句本身会迅速地完成.
主函数返回时, 所有的goroutine
都会被直接打断, 程序退出. 除了从主函数退出或者直接终止程序之外, 没有其它的编程方法能够让一个goroutine
来打断另一个执行, 但是之后可以看到一种方式来实现这个目的, 通过goroutine
之间的通信来让一个goroutine
请求其它的goroutine
, 让被请求的goroutine
自行结束执行.
示例: 并发的Clock
服务
time.Time.Format
将时间格式化. time.Parse
将字符串转化为时间.
示例: 并发的Echo
服务
函数值在循环体中才会出现捕获迭代变量的情况.
Channels
如果说goroutine
是Go
语言程序的并发体的话, 那么channels
是它们之间的通信机制. 一个channels
是一个通信机制, 它可以让一个goroutine
通过它给另一个goroutine
发送值信息. 每个channel
都有一个特殊的类型, 也就是channels
可以发送的数据类型. 一个可以发送int
类型数据的channel
一般写作chan int
.
使用内置的make
函数, 我们可以创建一个channel
:
1 | ch := make(chan int) |
和map
类似, channel
也是一个对应make
创建的底层数据结构的引用. 当我们复制一个channel
或用于函数传递参数时, 我们只是拷贝了一个channel
引用, 因此调用者和被调用者讲引用同一个channel
对象. 和其他的引用类型一样, channel
的零值也是nil
.
两个相同类型的channel
可以使用==
运算符比较. 如果两个channel
引用的是相通的对象, 那么比较的结果为真. 一个channel
也可以和nil
进行比较.
channel
支持close
操作, 用于关闭channel
, 随后对与基于该channel
的任何发送操作都将都导致panic
异常.
不带缓存的Channels
一个基于无缓存channel
的发送操作将导致发送者goroutine
阻塞, 直到另一个goroutine
在相同的channel
上执行接收操作, 当发送的值通过channel
成功传输之后, 两个goroutine
可以继续执行后面的语句. 反之, 如果接收操作先发生, 那么接收者goroutine
也将阻塞, 直到有另一个goroutine
在相同的channel
上执行发送操作.
串联的channels(pipeline)
channel
也可以用于将多个goroutine
链接在一起, 一个channel
的输出作为下一个channel
输入. 这种串联的channel
就是所谓的管道pipeline
.
当一个channel
被关闭后, 再向该channel
发送数据将导致panic
异常. 当一个被关闭的channel
中已经发送的数据都被成功接收后, 后续的接收操作将不再阻塞, 它们会立即返回一个零值.
没有办法直接测试一个channel
是否被关闭, 但是接收操作有一个变体形式: 它多接收一个结果, 多接收的第二个结果是一个布尔值, true
表示成功从channel
接收到值, false
表示channel
已经被关闭并且里面没有值可被接收.
Go
语言的range
循环可直接在channel
上迭代, 它依次从channel
接收数据, 当channel
被关闭并且没有值可被接收时跳出循环.
并不需要关闭每一个channel
. 不管一个channel
是否被关闭, 当它没有被引用时会被Go
语言的垃圾自动回收器回收.
试图重复关闭一个channel
将导致panic
异常, 试图关闭一个nil
的channel
也将导致panic
异常. 关闭channel
还会触发一个广播机制.
单方向的channel
当一个channel
作为函数参数时, 它一般总是被专门用于只发送或者只接收.
为了表示这种意图并防止被滥用, Go
语言的类型系统提供了单方向的channel
类型, 分别用于只发送或只接收的channel
. 类型chan<- int
表示一个只发送int
的channel
, 只能发送不能接收. 相反, 类型<-chan int
表示一个只接收int
的channel
, 只能接收不能发送. 这种限制将在编译期检测.
因为close
操作说明了通道上没有数据再发送, 仅仅在发送方goroutine
上才能调用它, 所以试图关闭一个仅能接收的channel
在编译时会报错.
任何双向channel
向单向channel
变量的赋值操作都将导致隐式转换. 这里没有反向转换的语法, 也就是不能将单向channel
转换为双向channel
.
带缓存的channel
带缓存的channel
内部持有一个元素队列. 队列的最大容量是在调用make
函数创建channel
时通过第二个参数指定的.
向缓存channel
的发送操作就是向内部缓存队列的尾部插入元素, 接收操作则是从队列的头部删除元素. 如果内部缓存队列是满的, 那么发送操作将阻塞直到因另一个goroutine
执行接收从而释放了新的队列空间. 相反, 如果channel
是空的, 接收操作将阻塞直到有另一个goroutine
执行发送操作而向队列插入元素.
在某些特殊情况下, 程序可能需要知道channel
内部缓存的容量, 可以用内置的cap
函数获取.
同样, 对于内置的len
函数, 如果传入的是channel
, 那么将返回channel
内部缓存队列中有效元素的个数.
goroutine
可能因为channel
无法接收可卡住, 导致goroutine
泄露. 泄漏的goroutine
并不会被自动回收, 因此确保每个不再需要的goroutine
能正常退出是重要的.
并发的循环
每个子问题都是完全彼此独立的问题叫做易并行问题. 易并行问题是最容易被实现成并行的一类问题, 并且是最能够享受并发带来的好处, 能够随着并行的规模线性地扩展.
为了知道最后一个goroutine
什么时候结束, 我们需要一个递增的计数器, 在每一个goroutine
启动时加一, 在goroutine
退出时减一. 这需要一种特殊的计数器, 这个计数器需要多个goroutine
操作时做到安全并且提供在其减为零之前一直等待的一种方法. 这种计数类型被称为sync.WaitGroup
.
实例: 并发的Web
爬虫
注意channel
可能造成死锁.
限制goroutine
数量的方法. 使用缓存队列.
终止程序的方法.
限制goroutine
数量的方法. 保持长活goroutine
.
基于select
的多路复用
time.Tick
函数返回一个channel
, 程序会周期性的像一个节拍器一样向这个channel
发送事件. 每一个事件的值是一个时间搓.
1 | select { |
select
会等待case
中有能够执行的case
时去执行. 当条件满足时, select
才会去通信并执行case
之后的语句; 这时候其它通信是不会执行的. 一个没有任何case
的select
语句写作select{}
, 会永远地等待下去.
time.After
函数会立即返回一个channel
, 并起一个新的goroutine
在经过特定的时间后向该channel
发送一个独立的值.
如果多个case
同时就绪, select
会随机地选择一个执行, 这样保证每一个channel
都有平等的被select
的机会.
Tick
函数挺方便, 但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适. 否则应该使用, 下面这种模式.
1 | ticker := time.NewTicker(1 * time.Second) |
有时候我们希望能够从channel
中发送或者接收值, 并避免因为发送或者接收导致的阻塞, 尤其是当channel
没有准备好写或者读时. select
语句就可以实现这样的功能. select
会有一个default
来设置当其它的操作都不能够被马上处理时程序需要执行哪些逻辑.
因为对一个nil
的channel
发送和接收操作会被永远阻塞, 在select
语句中操作nil
的channel
永远都不会被select
到.
这使得我们可以使用nil
来激活或者禁用case
, 来达成处理其它输入或输出事件时超时和取消的逻辑.
示例: 并发的字典遍历
nil
的channel
可以和select
配合来达到关闭case
的效果.
并发的退出
Go
语言并没有提供一个goroutine
中终止另一个goroutine
的方法, 由于这样会导致goroutine
之间的共享变量落在未定义的状态上.
可以创建一个退出channel
, 只用退出来, 作为一个广播.
1 | var done = make(chan struct{}) |
实例: 聊天服务
学习broadcaster
中的select
用法.
常用函数
1 | 获得文件的属性 |