Go 中的多线程。有人可以向我解释这些答案吗?
Multithreading in Go. Can somebody explain these answers to me?
我在模拟考试中遇到了两个问题。我得到了答案,但无法弄清楚它们背后的基本原理。
我会先post代码,然后是问题和答案。也许有人会很友好地向我解释答案?
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)
}
第一个问题是:
如果我们在主程序中省略 "d <- 0" 行,程序会如何运行,为什么?
老师的答案是:
所有线程的状态都是阻塞的,主线程也是。
第二个问题是:
如果交换事实过程的前两行,对整个程序的效率有何影响?
答案是:
所有线程都将按顺序运行。每个线程只有在完成时才会触发另一个线程。
最好不要认为这是 "multi-threading." Go 提供直接的并发设施,而不是线程。它恰好通过线程实现并发,但这是一个实现细节。请参阅 Rob Pike 的演讲,Concurrency is not parallelism 以获得更深入的讨论。
你的问题的关键是默认情况下通道是同步的(如果它们在构建期间没有被缓冲)。当一个 goroutine 写入通道时,它将阻塞直到其他 goroutine 从该通道读取。所以当这一行执行时:
z := <- d
在执行此行之前无法继续:
d <- 0
如果 d
频道上没有可用的值,fact
将永远不会继续。这对你来说可能是显而易见的。 反之亦然。在从 d
通道读取数据之前,主 goroutine 无法继续。通过这种方式,无缓冲通道提供了跨并发 goroutines 的同步点。
同样,主循环无法继续,直到 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
。什么将使它继续进行?另一个 goroutine 读取 d
。请记住,我们启动了 N
个 goroutine,它们都尝试从 d
中读取数据。只有其中一个会成功。其他人将在 (1) 处阻塞,等待 d
上出现某些内容。当第一个到达 (2) 时,就会发生这种情况。然后那个 goroutine 退出,一个随机的 goroutine 将继续。
但是会有一个最终的 goroutine 永远无法写入 d
并且它会泄漏。为了解决这个问题,需要在最后的 Printf
:
之前添加以下内容
<-d
这将允许最后一个 goroutine 退出。
How does the program behave if we omit the line "d <- 0" in the main procedure, and why?
通过该行,由 go fact(...)
启动的每个 goroutine 都将等待来自通道的内容,并在语句 z := <- d
.
处被阻塞
fact()
功能对 d
频道的内容没有实际影响 - 它删除了一些内容并添加了一些内容。所以如果频道里什么都没有就不会有进展,程序就会死锁。
从同一通道读取和写入的 goroutine 正在请求死锁 - 在现实生活中避免!
How would the efficiency of the whole program be affected if we swapped the first two lines of the fact procedure?
fact()
例程将等到它从 d
通道获得令牌,然后再进行冗长的阶乘计算。
因为 d
通道中一次只有一个令牌在播放,这意味着每个 go 例程只会在收到令牌时进行昂贵的计算,有效地序列化它们。
和原来一样,昂贵的阶乘计算在等待令牌之前并行完成。
在实践中,这可能不如您希望的那样有效,因为 goroutines 不是 pre-emptively 计划的,仅在阻塞操作和函数调用时。
我在模拟考试中遇到了两个问题。我得到了答案,但无法弄清楚它们背后的基本原理。
我会先post代码,然后是问题和答案。也许有人会很友好地向我解释答案?
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)
}
第一个问题是:
如果我们在主程序中省略 "d <- 0" 行,程序会如何运行,为什么?
老师的答案是:
所有线程的状态都是阻塞的,主线程也是。
第二个问题是:
如果交换事实过程的前两行,对整个程序的效率有何影响?
答案是:
所有线程都将按顺序运行。每个线程只有在完成时才会触发另一个线程。
最好不要认为这是 "multi-threading." Go 提供直接的并发设施,而不是线程。它恰好通过线程实现并发,但这是一个实现细节。请参阅 Rob Pike 的演讲,Concurrency is not parallelism 以获得更深入的讨论。
你的问题的关键是默认情况下通道是同步的(如果它们在构建期间没有被缓冲)。当一个 goroutine 写入通道时,它将阻塞直到其他 goroutine 从该通道读取。所以当这一行执行时:
z := <- d
在执行此行之前无法继续:
d <- 0
如果 d
频道上没有可用的值,fact
将永远不会继续。这对你来说可能是显而易见的。 反之亦然。在从 d
通道读取数据之前,主 goroutine 无法继续。通过这种方式,无缓冲通道提供了跨并发 goroutines 的同步点。
同样,主循环无法继续,直到 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
。什么将使它继续进行?另一个 goroutine 读取 d
。请记住,我们启动了 N
个 goroutine,它们都尝试从 d
中读取数据。只有其中一个会成功。其他人将在 (1) 处阻塞,等待 d
上出现某些内容。当第一个到达 (2) 时,就会发生这种情况。然后那个 goroutine 退出,一个随机的 goroutine 将继续。
但是会有一个最终的 goroutine 永远无法写入 d
并且它会泄漏。为了解决这个问题,需要在最后的 Printf
:
<-d
这将允许最后一个 goroutine 退出。
How does the program behave if we omit the line "d <- 0" in the main procedure, and why?
通过该行,由 go fact(...)
启动的每个 goroutine 都将等待来自通道的内容,并在语句 z := <- d
.
fact()
功能对 d
频道的内容没有实际影响 - 它删除了一些内容并添加了一些内容。所以如果频道里什么都没有就不会有进展,程序就会死锁。
从同一通道读取和写入的 goroutine 正在请求死锁 - 在现实生活中避免!
How would the efficiency of the whole program be affected if we swapped the first two lines of the fact procedure?
fact()
例程将等到它从 d
通道获得令牌,然后再进行冗长的阶乘计算。
因为 d
通道中一次只有一个令牌在播放,这意味着每个 go 例程只会在收到令牌时进行昂贵的计算,有效地序列化它们。
和原来一样,昂贵的阶乘计算在等待令牌之前并行完成。
在实践中,这可能不如您希望的那样有效,因为 goroutines 不是 pre-emptively 计划的,仅在阻塞操作和函数调用时。