我正在通过Udemy课程学习基础Go。在goroutines部分中,有一个节流示例,使我对等待组的工作方式有所了解。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func main() {
c1 := make(chan int)
c2 := make(chan int)
go populate(c1)
go fanOutIn(c1, c2)
for v := range c2 {
fmt.Println(v)
}
fmt.Println("about to exit")
}
func populate(c chan int) {
for i := 0; i < 100; i++ {
c <- i
}
close(c)
}
func fanOutIn(c1, c2 chan int) {
var wg sync.WaitGroup
const goroutines = 10
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func() {
for v := range c1 {
func(v2 int) {
c2 <- timeConsumingWork(v2)
}(v)
}
wg.Done()
}()
}
wg.Wait()
close(c2)
}
func timeConsumingWork(n int) int {
time.Sleep(time.Microsecond * time.Duration(rand.Intn(500)))
return n + rand.Intn(1000)
}
与我的理解不符的部分是在函数fanOutIn
中,我们在其中设置了WaitGroup
和Add(10)
。
为什么我要打印100个值?只能将一个值(i := 0
放在c1
上,并且永远不会从通道中显式删除该值。然后,代码按wg.Done()
,等待组队列减少到9,依此类推。
根据我目前的理解,我希望看到0 + rand.Intn(1000)
的10个值。
答案 0 :(得分:3)
分离出来的函数的内容如下(包括前面的go
和要括起来的括号):
go func() {
for v := range c1 {
func(v2 int) {
c2 <- timeConsumingWork(v2)
}(v)
}
wg.Done()
}()
这段代码有点怪异和奇怪。让我们进一步缩小它,丢弃wg.Done
并仅保留for
循环本身:
for v := range c1 {
func(v2 int) {
c2 <- timeConsumingWork(v2)
}(v)
}
有一个内部的未命名函数,在这里几乎没有用。我们可以在不更改程序行为的情况下将其丢弃,以获得:
for v := range c1 {
c2 <- timeConsumingWork(v)
}
这最后是一个简单的循环。现在有一个关键问题:您希望从该循环中进行多少次迭代?注意:它不一定是任何恒定编号。也许用更好的方式来表达问题:此循环何时结束?
for
循环读取一个通道。当从通道中读取表示没有更多数据,即通道已关闭并且其队列为空时,这种循环结束。 (请参见the Go specification section on for
loops。)
因此,最内部的循环for v := range c1
不会终止,直到通道c1
关闭并且队列中没有更多数据为止。该频道是通过以下方式创建的:
c1 := make(chan int)
因此它没有队列,因此我们甚至不需要考虑:它在close(c1)
关闭之后终止。 您现在应该寻找一个close
关闭的c1
。
这是关闭c1
的地方:
func populate(c chan int) {
for i := 0; i < 100; i++ {
c <- i
}
close(c)
}
我们以c1
作为参数来称呼它,因此其最终close(c)
关闭c1
。现在您可以问:我们何时到达此close
呼叫?答案很明显:在循环中i >= 100
之后,即,在我们发送了100个值之后,零到分别进入99 c1
频道。
fanOutIn
的工作是产生10个goroutine。 10个goroutine中的每一个都运行我在上面引用的第一个匿名函数。该匿名函数有一个循环,循环运行的次数不确定,直到循环c1
被关闭为止。循环中的每次行程都会获取通道的值,因此,最初,如果十个goroutine在所有可用值之前都设法启动,那么所有十个goroutine将等待值。
当生产者函数将一个值放入通道时,十个等待的goroutine中的一个将获取它并开始使用它。如果该goroutine需要很长时间才能返回到自己的for
循环的顶部,则另一个goroutine将采用下一个产生的值。因此,这里发生的事情是,多达十个产生的值通过通道传播到多达十个goroutine。 1 这些(多达十个)goroutine中的每一个花费一些琐碎的时间使用其值,然后将最终产品值发送到通道c2
,并返回到其不确定的for
循环的顶部。
只有当生产者关闭其通道c
(此处是我们的c1
)时,这十个goroutine才会看到一个闭合通道空队列,从而允许它们退出其for
循环。当它们确实退出其for
循环时,每个循环都会调用wg.Done()
(每个循环一次)并终止。
因此,一旦close(c1)
发生(通过close(c)
中的populate
),最终所有这十个匿名goroutine都将调用{{1 }}。届时,wg.Done()
中的wg.Wait()
将返回。这将调用fanOutIn
并从close(c2)
返回,并终止那个 goroutine。
与此同时,在fanOutIn
中,我们使用main
从通道for v := range c2
进行读取。十个goroutine中的任何一个将值写入c2
时,都会运行此for
循环。仅当c2
自身关闭时(它的队列也必须为空,但是c2
的队列长度为零),它才会退出。因此c2
在main
关闭之前将不会继续for
循环进行,c2
不会发生,直到wg.Wait()
返回时才发生,直到{ {1}}呼叫已经发生,直到关闭频道wg.Done()
才发生。
这意味着c1
在main
调用for
之前无法绕过自己的populate
循环,并且只有在生成正好100个值之后才会发生。
1 如in comments below所述,短语 up 可能很重要:我们并不真正知道有多少个goroutine会真正消耗值。很大程度上取决于每个goroutine要做多少工作,做什么工作以及Go运行时可用的CPU数量。