Goroutines节气门示例

时间:2020-05-10 22:49:02

标签: go wait goroutine

我正在通过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中,我们在其中设置了WaitGroupAdd(10)

为什么我要打印100个值?只能将一个值(i := 0放在c1上,并且永远不会从通道中显式删除该值。然后,代码按wg.Done(),等待组队列减少到9,依此类推。

根据我目前的理解,我希望看到0 + rand.Intn(1000)的10个值。

1 个答案:

答案 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的队列长度为零),它才会退出。因此c2main关闭之前将不会继续for循环进行,c2不会发生,直到wg.Wait()返回时才发生,直到{ {1}}呼叫已经发生,直到关闭频道wg.Done()才发生。

这意味着c1main调用for之前无法绕过自己的populate循环,并且只有在生成正好100个值之后才会发生。


1 in comments below所述,短语 up 可能很重要:我们并不真正知道有多少个goroutine会真正消耗值。很大程度上取决于每个goroutine要做多少工作,做什么工作以及Go运行时可用的CPU数量。