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 计划的,仅在阻塞操作和函数调用时。