我在模拟考试中遇到两个问题。我得到了答案,但无法弄清楚背后的理由。
我将首先发布代码,然后发布问题和答案。也许有人会这么好解释我的答案?
package main
import "fmt"
func fact(n int, c chan int, d chan int) {
k := /* code to compute factorial of n */
z := <- d
c <- k + z
d <- z + 1
}
func main() {
r := 0
c := make(chan int)
d := make(chan int)
for i = 0 ; i < N ; i++ {
go fact(i,c,d)
}
d <- 0
for j = 0 ; j < N ; j++ {
r = r + <-c
}
fmt.Printf("result = %d\n",r)
}
第一个问题是:
如果省略行&#34; d&lt; - 0&#34;程序如何表现?在主程序中,为什么?
老师的回答是:
所有线程的状态都被阻止,也是主线程。
第二个问题是:
如果我们交换事实程序的前两行,整个计划的效率如何受到影响?
答案是:
所有线程都将按顺序运行。每个线程只有在完成时才会触发另一个线程。
答案 0 :(得分:10)
最好不要将其视为“多线程”。 Go为并发提供直接设施,而不是线程。它恰好通过线程实现其并发性,但这是一个实现细节。请参阅Rob Pike的演讲,Concurrency is not parallelism进行更深入的讨论。
您的问题的关键是默认情况下频道是同步的(如果它们在构建期间没有被缓冲)。当一个goroutine写入一个通道时,它将阻塞,直到某个其他goroutine从该通道读取。所以当这一行执行时:
z := <- d
在此行执行之前无法继续:
d <- 0
如果d
频道没有提供任何价值,fact
将永远不会继续。这对你来说很明显。 但反过来也是如此。在从d
频道读取内容之前,主要的goroutine无法继续。通过这种方式,无缓冲通道可以在并发goroutine之间提供同步点。
同样,在c
出现某个值之前,主循环无法继续。我发现使用两个手指并指向每个goroutine中的当前代码行很有用。前进一根手指直到进入通道操作。然后前进另一个直到它到达通道操作。如果您的手指指向同一频道上的读取和写入,则您可以继续。如果他们不是,那么你就陷入僵局。
如果你想到这一点,你会发现一个问题。这个程序漏掉了goroutine。
func fact(n int, c chan int, d chan int) {
k := /* code to compute factorial of n */
z := <- d // (1)
c <- k + z
d <- z + 1 // (2)
}
在(2)我们尝试写d
。什么将允许继续?来自d
的另一个goroutine读物。请记住,我们开始N
goroutines,所有人都试图从d
读取。只有其中一个会成功。其他人将阻止(1),等待d
上显示的内容。当第一个到达(2)时会发生这种情况。然后那个goroutine退出,随机goroutine将继续。
但是会有一个永远无法写入d
的最终goroutine,它会泄漏。为了解决这个问题,需要在最终Printf
:
<-d
这将允许最后一个goroutine退出。
答案 1 :(得分:4)
如果我们在主程序中省略“d&lt; - 0”行,该程序如何表现?为什么?
由于该行,go fact(...)
开始的每个goroutine都会等待来自频道的内容,在z := <- d
语句处被屏蔽。
fact()
函数对d
频道的内容没有净影响 - 删除了某些内容并添加了一些内容。因此,如果频道中没有任何内容,则不会有任何进展,程序将会死锁。
从同一频道读取和写入的goroutine要求死锁 - 避免在现实生活中使用!
如果我们交换事实程序的前两行,整个计划的效率如何受到影响?
fact()
例程将等待它从d
频道获取一个令牌,然后再进行冗长的因子计算。
因为d
频道中只有一个令牌同时存在,这意味着每个go例程只会在收到令牌时进行昂贵的计算,从而有效地序列化它们。
与最初一样,在等待令牌之前,并行进行昂贵的因子计算。
在实践中,由于goroutine没有先发制人地安排,只能阻止操作和函数调用,因此这种方法的效果不如您所希望的那么好。