Effective Go给出了这个示例,说明如何使用通道模拟信号量:
var sem = make(chan int, MaxOutstanding)
func handle(r *Request) {
<-sem
process(r)
sem <- 1
}
func init() {
for i := 0; i < MaxOutstanding; i++ {
sem <- 1
}
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req)
}
}
它还说:因为数据同步发生在来自通道的接收上(即,发送“发生在”接收之前;请参阅Go Memory 模型),信号量的获取必须在通道接收上,而不是发送上。
现在,我想我理解了 Go 内存模型和“发生在之前”的定义。但是我看不到阻塞通道发送有什么问题:
func handle(r *Request) {
sem <- 1
process(r)
<-sem
}
func init() {}
此代码(上面有sem
和Serve
不变)以相反的方式使用缓冲通道。频道开始为空。在进入handle
时,如果已经有MaxOutstanding
goroutines 正在执行该过程,则发送将阻塞。一旦其中一个完成处理并从通道中“释放”一个插槽,通过接收一个 int,我们的发送将被解除阻塞,goroutine 将开始自己的处理。
正如教科书似乎暗示的那样,为什么这是一种不好的同步方式?
释放通道槽的接收操作是否不会“发生在”将使用同一槽的发送之前?这怎么可能?
换句话说,语言参考说“缓冲通道上的发送[阻塞直到]缓冲区中有空间。”
但是内存模型只说“来自无缓冲通道的接收发生在该通道上的发送完成之前”。特别是,它并没有说从已满的缓冲通道接收发生在该通道上的发送完成之前。
这是一些不能相信做正确事情的极端案例吗?(这实际上是将被阻止的发送与解除阻止的接收同步)
如果是这样的话,它看起来像是一种令人讨厌的竞争条件,这种语言旨在最大限度地减少偷偷摸摸的竞争条件:-(
var c = make(chan int, 1)
var a string
func f() {
a = "hello, world"
<-c // unblock main, which will hopefully see the updated 'a'
}
func main() {
c <- 0 // fill up the buffered channel
go f()
c <- 0 // this blocks because the channel is full
print(a)
}