Goroutines和Channels

GoroutinesChannels

Go语言中的并发程序可以用两种手段来实现. goroutinechannel, 其支持”顺序通信进程”(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

如果说goroutineGo语言程序的并发体的话, 那么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异常, 试图关闭一个nilchannel也将导致panic异常. 关闭channel还会触发一个广播机制.

单方向的channel

当一个channel作为函数参数时, 它一般总是被专门用于只发送或者只接收.

为了表示这种意图并防止被滥用, Go语言的类型系统提供了单方向的channel类型, 分别用于只发送或只接收的channel. 类型chan<- int表示一个只发送intchannel, 只能发送不能接收. 相反, 类型<-chan int表示一个只接收intchannel, 只能接收不能发送. 这种限制将在编译期检测.

因为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
2
3
4
5
6
7
8
9
10
select {
case <- ch1:
// ...
case x := <- ch2:
// ...
case ch3 <- y:
// ...
default:
// ...
}

select会等待case中有能够执行的case时去执行. 当条件满足时, select才会去通信并执行case之后的语句; 这时候其它通信是不会执行的. 一个没有任何caseselect语句写作select{}, 会永远地等待下去.

time.After函数会立即返回一个channel, 并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值.

如果多个case同时就绪, select会随机地选择一个执行, 这样保证每一个channel都有平等的被select的机会.

Tick函数挺方便, 但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适. 否则应该使用, 下面这种模式.

1
2
3
ticker := time.NewTicker(1 * time.Second)
<-ticker.C
ticker.Stop()

有时候我们希望能够从channel中发送或者接收值, 并避免因为发送或者接收导致的阻塞, 尤其是当channel没有准备好写或者读时. select语句就可以实现这样的功能. select会有一个default来设置当其它的操作都不能够被马上处理时程序需要执行哪些逻辑.

因为对一个nilchannel发送和接收操作会被永远阻塞, 在select语句中操作nilchannel永远都不会被select到.

这使得我们可以使用nil来激活或者禁用case, 来达成处理其它输入或输出事件时超时和取消的逻辑.

示例: 并发的字典遍历

nilchannel可以和select配合来达到关闭case的效果.

并发的退出

Go语言并没有提供一个goroutine中终止另一个goroutine的方法, 由于这样会导致goroutine之间的共享变量落在未定义的状态上.

可以创建一个退出channel, 只用退出来, 作为一个广播.

1
2
3
4
5
6
7
8
9
10
var done = make(chan struct{})

func cancelled() bool {
select {
case <- done:
return true
default:
return false
}
}

实例: 聊天服务

学习broadcaster中的select用法.

常用函数

1
2
3
4
5
6
7
8
# 获得文件的属性
os.Stat()

# 获得目录下的所有文件
ioutil.ReadDir()

# 读取命令行参数
flag.Args()
-------------本文结束感谢您的阅读-------------