7

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() {}

此代码(上面有semServe不变)以相反的方式使用缓冲通道。频道开始为空。在进入handle时,如果已经有MaxOutstandinggoroutines 正在执行该过程,则发送将阻塞。一旦其中一个完成处理并从通道中“释放”一个插槽,通过接收一个 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)
}
4

2 回答 2

5

这段 Effective Go 文档也让我印象深刻。事实上,在相对较新的 Effective Go 版本中,有问题的代码在通道发送上获取信号量(而不是像在当前版本中那样使用通道接收,它使用 init() 来“启动”通道)。

显然已经有很多关于这个话题的讨论。我不会费心去总结一切,但讨论都可以从这里找到:

https://code.google.com/p/go/issues/detail?id=5023

它确实让我感到不幸,但引用该问题的提交者的话,短篇小说似乎是,除非在频道接收上获取信号量......:

以下代码:

func handle(r *Request) {
    sem <- 1    // Wait for active queue to drain.
    process(r)  // May take a long time.
    <-sem       // Done; enable next request to run.
}

...可以合法地“优化”为:

func handle(r *Request) {
    process(r)  // May take a long time.
    sem <- 1    // Wait for active queue to drain.
    <-sem       // Done; enable next request to run.
}

...或进入:

func handle(r *Request) {
    sem <- 1    // Wait for active queue to drain.
    <-sem       // Done; enable next request to run.
    process(r)  // May take a long time.
}
于 2013-05-02T07:46:09.300 回答
1

如果我理解正确(很可能我没有理解),问题只是语言没有正确的保证,其中一些事情会以这种方式使用。

当我遇到这样的事情时,我通常会发现(有时在令人尴尬的反复试验之后)并不是语言“缺少某些东西”,而是我试图用锤子作画。

在您上面的具体示例中,我将通过稍微不同的结构来解决它:

无需在发送方中使用信号量(并在接收方中解除阻塞),只需预先生成所需数量的 goroutine,然后通过通道发送它们。不需要信号量。我知道这只是一个精简的例子,但如果你更详细地描述你的实际用例/问题,很可能有人会为它提供一个干净的 go-like 解决方案。

于 2013-05-01T22:04:19.010 回答