理解代码 - Go 并发模式:菊花链

Cod*_*mer 5 concurrency go

我正在研究Go并发模式。

\n\n

我不确定的一种模式是 \xef\xbc\x9a 菊花链 https://talks.golang.org/2012/concurrency.slide#39

\n\n

我很难理解代码的控制流程。

\n\n

有人可以向我解释一下吗?

\n\n
package main\n\nimport (\n    "fmt"\n)\n\nfunc f(left, right chan int) {\n    left <- 1 + <-right\n}\n\nfunc main() {\n    const n = 10000\n\n    leftmost := make(chan int)\n    right := leftmost               //point B: what does these do ?\n    left := leftmost\n\n    for i := 0; i < n; i++ {\n        right = make(chan int)\n        go f(left, right)\n        left = right                //point A\n    }\n    go func(c chan int) { c <- 1 }(right)  \n    fmt.Println(<-leftmost)\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

结论

\n\n
    \n
  1. 通道的流动从右向左。最好编写 \nfunc f(left chan<- int, right <-chan int)而不是上面的原始函数签名。

  2. \n
  3. “连锁反应”直到 c <- 1 才开始,当信号 1 发送到最右边的通道时,\n反应一直到最左端。打印出 10001。

  4. \n
\n\n

原因是go通道阻塞“读取”,直到收到通道接收信号。

\n\n
    \n
  1. @Rick-777 展示了如何使用类似数组的结构以便于理解。\n因为每个 go 协程只有大约 6k 大。创建 10k 频道并不是一个坏主意。

  2. \n
  3. 我清理了 B 点周围的一些代码,用于通道初始化。\n这里是源代码:\n http://play.golang.org/p/1kFYPypr0l

  4. \n
\n

Ric*_*777 7

VonC已经给出了直接的答案。以下是一些进一步的评论。

Playground中有一个稍微整理过的版本,区别在于作为参数传递的通道明确指定了方向,即。<-chanchan<-。这样做是一个很好的做法,因为编译器可以为您捕获更多错误。

n可以使用通道数组来编写具有菊花链 goroutine 的替代且等效的程序。这使用更少的代码行分配相同的通道总数。参见游乐场

package main

import (
    "fmt"
)

func f(left chan<- int, right <-chan int) {
    left <- 1 + <-right
}

func main() {
    const n = 10000

    // first we construct an array of n+1 channels each being a 'chan int'
    var channels [n+1]chan int
    for i := range channels {
        channels[i] = make(chan int)
    }

    // now we wire n goroutines in a chain
    for i := 0; i < n; i++ {
        go f(channels[i], channels[i+1])
    }

    // insert a value into the right-hand end
    go func(c chan<- int) { c <- 1 }(channels[n])

    // pick up the value emerging from the left-hand end
    fmt.Println(<-channels[0])
}
Run Code Online (Sandbox Code Playgroud)

我希望你现在能明白原来的程序与这个程序是如何等价的。有一个细微的差别:原始程序不创建任何通道数组,因此使用的内存稍微少一些。


Von*_*onC 5

它说明你可以生成大量的 goroutine。

在这里,每个go f(left, right)块:left <- 1 + <-right阻塞是因为它等待right获取值。参见“ golang通道是否维持秩序”。
此处创建的所有通道都是无缓冲通道。

所有 10000 个 goroutine 均已创建。

B点:rightleft是使用短变量声明来声明的。

  • right被初始化为最左边,但这并不重要,因为它会在for循环(right = make(chan int))中被重新分配给新的通道。
    另一种声明正确的方式是:

    var right chan int
    
    Run Code Online (Sandbox Code Playgroud)
  • left初始化为leftmost,创建的第一个通道。

A 点:但是一旦该通道开始等待 ( left <- 1 + <-right),for循环就会向左设置right,并创建一个新的right:这就是菊花链的构建方式

 left <- (new) right (now left) <- (new) right (now left) <- ...
Run Code Online (Sandbox Code Playgroud)

然后,一个值被发送到最后right创建的通道:{c <- 1 }(right)

然后您等待leftmost创建的第一个通道接收其值(增加 10000 倍)。

由于接收器总是阻塞,直到有数据要接收为止,因此函数本身在最终接收到其值main()之前不会退出。 如果退出得太早,菊花链将没有时间完成。leftmost
main()


小智 1

我发现试运行这个程序对于理解它确实很有帮助。

首先,执行后

leftmost := make(chan int)
right := leftmost
left := leftmost
Run Code Online (Sandbox Code Playgroud)

leftmostleft、 和right都指同一个chan int

[chan int]
     |                 
left, leftmost, right
Run Code Online (Sandbox Code Playgroud)

让我们对 for 循环运行一些迭代。

i = 0

当我们刚进入for循环时,

[chan int]
     |                 
left, leftmost, right
Run Code Online (Sandbox Code Playgroud)

执行后right = make(chan int)go f(left, right).

[chan int] <-(+1)- [chan int]
     |                 |
left, leftmost        right
Run Code Online (Sandbox Code Playgroud)

执行后left = right

[chan int] <-(+1)- [chan int]
     |                 |
  leftmost        left, right
Run Code Online (Sandbox Code Playgroud)

i = 1

当我们刚进入for循环时,

[chan int] <-(+1)- [chan int]
     |                 |
  leftmost        left, right
Run Code Online (Sandbox Code Playgroud)

执行后right = make(chan int)go f(left, right).

[chan int] <-(+1)- [chan int] <-(+1)- [chan int]
     |                 |                   |
  leftmost            left               right
Run Code Online (Sandbox Code Playgroud)

执行后left = right

[chan int] <-(+1)- [chan int] <-(+1)- [chan int]
     |                                     |
  leftmost                            left, right
Run Code Online (Sandbox Code Playgroud)

我觉得两个循环足以看到该模式:

  • 每个循环我们都会创建一个新的chan int并将其附加到“链表”的末尾chan int
  • 因此n = 100000,在循环之后,我们创建了100000new ,并且“链表”中chan int的数量将为。chan intchan int100001
  • 100001 chan int表示100000每对相邻的之间的间隙chan int,每个间隙表示一个+1

在for循环之前,因为全部chan int都是接收者,没有传入值,所以全部chan int都会等待。

在for循环之后,我们执行go func(c chan int) { c <- 1 }(right),然后1将 传入“链表chan int”并+1对该值执行100000多次,所以最终的结果将leftmost100001

1当我们传递到“链表”时,事情会像这样chan int

[chan int] <-(+1)- [chan int] <-(+1)- ...... <-(+1)- [chan int] <- 1
     |                                                   |
  leftmost                                           left, right
Run Code Online (Sandbox Code Playgroud)

我创建了一个 leetcode 游乐场,其中包含所有代码。你可以在这里尝试一下(https://leetcode.com/playground/gAa59fh3)。