我正在研究Go并发模式。
\n\n我不确定的一种模式是 \xef\xbc\x9a 菊花链 https://talks.golang.org/2012/concurrency.slide#39
\n\n我很难理解代码的控制流程。
\n\n有人可以向我解释一下吗?
\n\npackage 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}\nRun Code Online (Sandbox Code Playgroud)\n\n结论:
\n\n通道的流动从右向左。最好编写 \nfunc f(left chan<- int, right <-chan int)而不是上面的原始函数签名。
“连锁反应”直到 c <- 1 才开始,当信号 1 发送到最右边的通道时,\n反应一直到最左端。打印出 10001。
原因是go通道阻塞“读取”,直到收到通道接收信号。
\n\n@Rick-777 展示了如何使用类似数组的结构以便于理解。\n因为每个 go 协程只有大约 6k 大。创建 10k 频道并不是一个坏主意。
我清理了 B 点周围的一些代码,用于通道初始化。\n这里是源代码:\n http://play.golang.org/p/1kFYPypr0l
VonC已经给出了直接的答案。以下是一些进一步的评论。
Playground中有一个稍微整理过的版本,区别在于作为参数传递的通道明确指定了方向,即。<-chan和chan<-。这样做是一个很好的做法,因为编译器可以为您捕获更多错误。
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)
我希望你现在能明白原来的程序与这个程序是如何等价的。有一个细微的差别:原始程序不创建任何通道数组,因此使用的内存稍微少一些。
它说明你可以生成大量的 goroutine。
在这里,每个go f(left, right)块:left <- 1 + <-right阻塞是因为它等待right获取值。参见“ golang通道是否维持秩序”。
此处创建的所有通道都是无缓冲通道。
所有 10000 个 goroutine 均已创建。
B点:right和left是使用短变量声明来声明的。
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()之前不会退出。
如果退出得太早,菊花链将没有时间完成。leftmostmain()
小智 1
我发现试运行这个程序对于理解它确实很有帮助。
首先,执行后
leftmost := make(chan int)
right := leftmost
left := leftmost
Run Code Online (Sandbox Code Playgroud)
leftmost、left、 和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 int100001100001 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多次,所以最终的结果将leftmost是100001。
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)。