goroutines是否忽略通道的缓冲区大小?

huangapple go评论160阅读模式
英文:

Is gorouines ignore channel's buffer size

问题

环境:OS X 10.8,Go 1.0.2

我创建了一个带有缓冲大小为2的通道,然后如果我向通道写入三次,它会抛出错误:
> throw: all goroutines are asleep - deadlock!

当然,这是正确的。

但是,如果我在goroutine中写入通道四次或更多次,它就能正常工作,为什么呢?
通道的容量是2,为什么goroutine忽略了容量设置或者忘记了容量设置?
我注释了读取通道的代码,所以没有人会读取通道并保存容量。我还使用了time.Sleep来等待所有的goroutine完成它们的工作。

请检查以下代码:
package main

//import "fmt"

func main() {
    c := make(chan int, 2)
    /*c <- 1
    c <- 2
    c <- 3*/
    for i:=0; i<4; i++ {
        go func(i int) {
            c <- i
            c <- 9
            c <- 9
            c <- 9
        }(i)
    }
    time.Sleep(2000 * time.Millisecond)

    /*for i:=0; i<4*2; i++ {
        fmt.Println(<-c)
    }*/
}

有人可以给一些提示吗?谢谢,伙计们。

英文:

Environment: OS X 10.8, Go 1.0.2

I make a channel with buffer-size 2, then if I write channel three times, it will throw error:
> throw: all goroutines are asleep - deadlock!

Of course, it's correct.

BUT if I write channel four or more times in the goroutines, it works fine, why?
The channel's capacity is 2, why goroutines ignore that or forget the capacity setting?
I comment the read-channel codes, so no one will read channel and save the capacity. I also use time.Sleep to waiting for all goroutines finish their work.

please review following codes:
package main

//import "fmt"

func main() {
    c := make(chan int, 2)
    /*c <- 1
    c <- 2
    c <- 3*/
    for i:=0; i<4; i++ {
        go func(i int) {
            c <- i
            c <- 9
            c <- 9
            c <- 9
        }(i)
    }
    time.Sleep(2000 * time.Millisecond)

    /*for i:=0; i<4*2; i++ {
        fmt.Println(<-c)
    }*/
}

Would anyone please give some hits? thanks, guys.

答案1

得分: 9

当一个通道被缓冲时,这意味着它不会在缓冲区满之前阻塞。一旦缓冲区满了,发送的goroutine将会阻塞,因为它试图向通道中添加东西。

这意味着以下代码会阻塞:

c := make(chan int)
c <- 1          // 在这里阻塞,这是无缓冲的!
println(<-c)

而以下代码也会阻塞:

c := make(chan int, 2)
c <- 1
c <- 2
c <- 3           // 在这里阻塞,缓冲区已满!
println(<-c)

但是goroutine和通道的关键是可以并发运行,所以以下代码可以正常工作:

c := make(chan int)
go func() { c <- 1; }() // 这会在生成的goroutine中阻塞,直到...
println(<-c)            // ... 主goroutine 中达到这一行

类似地:

c := make(chan int, 2)
go func() {  // `go ...` 生成一个goroutine
    c <- 1   // 缓冲区未满,不会阻塞
    c <- 2   // 缓冲区未满,不会阻塞
    c <- 3   // 缓冲区已满,生成的goroutine会阻塞,直到...
}()
println(<-c) // ... 主goroutine 中达到这一行

在你的例子中,你生成了四个不同的goroutine,它们都向同一个缓冲通道写入四个数字。由于缓冲区大小为2 < 16,它们最终会阻塞。

但问题的关键在于Go的策略是只等待主goroutine

程序的执行从初始化主包开始,然后调用main函数。当main函数返回时,程序退出。它不会等待其他(非主)goroutine 完成。

这意味着在你的第一个例子中,当主goroutine达到c <- 3这一行时,它被阻塞了。由于没有其他goroutine能够执行任何可能解除阻塞的操作,运行时检测到程序发生了死锁,并报告了一个错误。

然而,在你的第二个例子中,生成的goroutine被阻塞,而主goroutine则继续静默执行,直到它执行结束,此时所有(被阻塞的)生成的goroutine都被静默终止,不会报告任何错误。

英文:

When a channel is buffered, this means it will not block until the buffer is full. Once the buffer is full, the sending goroutine will block as it tries to add things to the channel.

This means that this will block:

c := make(chan int)
c &lt;- 1          // Block here, this is unbuffered !
println(&lt;-c)

And this will also block:

c := make(chan int, 2)
c &lt;- 1
c &lt;- 2
c &lt;- 3           // Block here, buffer is full !
println(&lt;-c)

But the point of goroutines and channel is precisely to run things concurrently, so this will work:

c := make(chan int)
go func() { c &lt;- 1; }() // This will block in the spawned goroutine until...
println(&lt;-c)            // ... this line is reached in the main goroutine

And similarly:

c := make(chan int, 2)
go func() {  // `go ...` spawns a goroutine
    c &lt;- 1   // Buffer is not full, no block
    c &lt;- 2   // Buffer is not full, no block
    c &lt;- 3   // Buffer is full, spawned goroutine is blocking until...
}()
println(&lt;-c) // ... this line is reached in the main goroutine

In your example, you spawn four different goroutines, which all write four numbers to the same buffered channel. As the buffer is 2 < 16, they will end up blocking

But the crux of the matter is that the Go policy is to wait only for the main goroutine:

> Program execution begins by initializing the main package and then invoking the function main. When the function main returns, the program exits. It does not wait for other (non-main) goroutines to complete.

This means that in your first example, the main goroutine was blocking when it reached line c &lt;- 3. As no other goroutine was able to do anything which could potentially unblock it, the runtime detected that the program was deadlocked and reported an error.

In your second example however, spawned goroutines block, while the main continues quietly until it reaches the end of its execution, at which point all the (blocked) spawned goroutines are silently killed, and no error is reported.

答案2

得分: 1

val给出了一个很好的答案。我想再补充一个我发现很有用的小技巧。

在学习使用goroutines时,最好先使用无缓冲通道。这样,当你犯错时,你会立即遇到死锁,从中学到经验。你需要学习如何编写不会死锁的代码,这意味着学习一些技巧,比如在客户端和服务器之间不要有循环依赖(假设你的goroutines是作为客户端或服务器编写的)。

在没有缓冲的情况下推理网络更简单,尽管一开始可能不太明显。

缓冲非常有用,但应该被视为提高性能的手段。

英文:

val has given a good answer. I'd like to add an extra tip I've found useful.

When learning to use goroutines, use zero-buffered channels to start with. This way, when you make a mistake, you'll immediately get a deadlock, which you can learn from. You need to learn how to write code that doesn't deadlock, which means learning tricks like not having cyclic dependencies in the client-server relationships (assuming your goroutines are written as clients or servers).

Reasoning about the network without buffering is simpler, although this might not be obvious at first.

Buffering is really useful, but should be considered as a means to enhance performance.

huangapple
  • 本文由 发表于 2013年11月7日 19:13:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/19834469.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定