相比Erlang,Go并未实现严格的并发安全。
允许全局变量、指针、引用类型这些非安全内存共享操作,就需要开发人员自行维护数据一致和完整性。Go鼓励使用CSP通道,以通信来代替内存共享,实现并发安全。
Don't communicate by sharing memory,share memory by communicating.
CSP:Communicating Sequential Process.
通过消息来避免竞态的模型除了CSP,还有Actor。但两者有较大区别。
作为CSP核心,通道(channel)是显式的,要求操作双方必须知道数据类型和具体通道,并不关心另一端操作者身份和数量。可如果另一端未准备妥当,或消息未能及时处理时,会阻塞当前端。
相比起来,Actor是透明的,它不在乎数据类型及通道,只要知道接收者信箱即可。默认就是异步方式,发送方对消息是否被接收和处理并不关心。
从底层实现上来说,通道只是一个队列。同步模式下,发送和接收双方配对,然后直接复制数据给对方。如配对失败,则置入等待队列,直到另一方出现后才被唤醒。异步模式抢夺的则是数据缓冲槽。发送方要求有空槽可供写入,而接收方则要求有缓冲数据可读。需求不符时,同样加入等待队列,直到有另一方写入数据或腾出空槽后被唤醒。
除传递消息(数据)外,通道还常被用作事件通知。
func main() {
done:=make(chan struct{}) // 结束事件
c:=make(chan string) // 数据传输通道
go func() {
s:= <-c // 接收消息
println(s)
close(done) // 关闭通道,作为结束通知
}()
c<- "hi!" // 发送消息
<-done // 阻塞,直到有数据或管道关闭
}
输出:
hi!
同步模式必须有配对操作的goroutine出现,否则会一直阻塞。而异步模式在缓冲区未满或数据未读完前,不会阻塞。
func main() {
c:=make(chan int,3) // 创建带3个缓冲槽的异步通道
c<-1 // 缓冲区未满,不会阻塞
c<-2
println(<-c) // 缓冲区尚有数据,不会阻塞
println(<-c)
}
输出:
1
2
多数时候,异步通道有助于提升性能,减少排队阻塞。
缓冲区大小仅是内部属性,不属于类型组成部分。另外通道变量本身就是指针,可用相等操作符判断是否为同一对象或nil。
func main() {
var a,b chan int=make(chan int,3),make(chan int)
var c chan bool
println(a==b)
println(c==nil)
fmt.Printf("%p, %d\n",a,unsafe.Sizeof(a))
}
输出:
false
true
0xc820076000,8
虽然可传递指针来避免数据复制,但须额外注意数据并发安全。
内置函数cap和len返回缓冲区大小和当前已缓冲数量;而对于同步通道则都返回0,据此可判断通道是同步还是异步。
func main() {
a,b:=make(chan int),make(chan int,3)
b<-1
b<-2
println("a:",len(a),cap(a))
println("b:",len(b),cap(b))
}
输出:
a:0 0
b:2 3
收发
除使用简单的发送和接收操作符外,还可用ok-idom或range模式处理数据。
func main() {
done:=make(chan struct{})
c:=make(chan int)
go func() {
defer close(done) // 确保发出结束通知
for{
x,ok:= <-c
if!ok{ // 据此判断通道是否被关闭
return
}
println(x)
}
}()
c<-1
c<-2
c<-3
close(c)
<-done
}
输出:
1
2
3
对于循环接收数据,range模式更简洁一些。
func main() {
done:=make(chan struct{})
c:=make(chan int)
go func() {
defer close(done)
for x:=range c{ // 循环获取消息,直到通道被关闭
println(x)
}
}()
c<-1
c<-2
c<-3
close(c)
<-done
}
及时用close函数关闭通道引发结束通知,否则可能会导致死锁。
fatal error:all goroutines are asleep-deadlock!
通知可以是群体性的。也未必就是通知结束,可以是任何需要表达的事件。
func main() {
var wg sync.WaitGroup
ready:=make(chan struct{})
for i:=0;i<3;i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
println(id, ":ready.") // 运动员准备就绪
<-ready // 等待发令
println(id, ":running...")
}(i)
}
time.Sleep(time.Second)
println("Ready?Go!")
close(ready) // 砰!
wg.Wait()
}
输出:
0:ready.
2:ready.
1:ready.
Ready?Go!
1:running...
0:running...
2:running...
一次性事件用close效率更好,没有多余开销。连续或多样性事件,可传递不同数据标志实现。还可使用sync.Cond实现单播或广播事件。
对于closed或nil通道,发送和接收操作都有相应规则:
- 向已关闭通道发送数据,引发panic。
- 从已关闭接收数据,返回已缓冲数据或零值。
- 无论收发,nil通道都会阻塞。
func main() {
c:=make(chan int,3)
c<-10
c<-20
close(c)
for i:=0;i<cap(c)+1;i++ {
x,ok:= <-c
println(i, ":",ok,x)
}
}
输出:
0:true 10
1:true 20
2:false 0
3:false 0
重复关闭,或关闭nil通道都会引发panic错误。
panic:close of closed channel
panic:close of nil channel