【问题标题】:Channel synchronisation with WaitGroup. Closing channel and Waitgroup与 WaitGroup 的频道同步。关闭通道和等待组
【发布时间】:2020-04-25 13:53:08
【问题描述】:

我正在尝试理解 Goroutine 中的同步。我在这里有一个代码,它在通道上写入 0 到 4 的数字,完成后我使用 range 从通道读取并打印值。

下面的代码在我等待使用 wg.Wait() 并在单独的 Goroutine 中关闭通道时运行良好。

package main

import (
    "fmt"
    "strconv"
    "sync"
)

func putvalue(i chan string, value string, wg *sync.WaitGroup) {
    i <- value
    defer wg.Done()
}

func main() {
    queue := make(chan string)
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go putvalue(queue, strconv.Itoa(i), &wg)
    }

    go func() {
        wg.Wait()
        close(queue)
    }()

    for elem := range queue {
        fmt.Println(elem)
    }

}

https://play.golang.org/p/OtaRP3Mm4lk

但如果我使用完全相同的代码,但在主线程中等待并关闭通道,则会导致死锁。下面的代码导致死锁。

package main

import (
    "fmt"
    "strconv"
    "sync"
)

func putvalue(i chan string, value string, wg *sync.WaitGroup) {
    i <- value
    defer wg.Done()
}

func main() {
    queue := make(chan string)
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go putvalue(queue, strconv.Itoa(i), &wg)
    }

    wg.Wait()
    close(queue)

    for elem := range queue {
        fmt.Println(elem)
    }

}

https://play.golang.org/p/JXmdsdPKQPu

据我了解,在第二种情况下,主线程执行停止并等待,但它与在单独的 goroutine 中执行它有什么不同?请帮助我理解这一点。

【问题讨论】:

    标签: go goroutine


    【解决方案1】:

    将每个 goroutine 视为一个单独的人(或 gopher:https://blog.golang.org/gopher)可能会有所帮助。当您go f() 时,您会得到一个新人/地鼠,并让他们负责运行该功能。因此,您有 5 个额外的 gopher 正在运行:

    func putvalue(i chan string, value string, wg *sync.WaitGroup) {
        i <- value
        defer wg.Done()
    }
    

    5 人中的每一个都跑到到达i &lt;- value 线的位置,然后他们停下来,等待地鼠跑到频道/邮箱的“获取”一侧并伸手穿过得到一个字符串,这是进入i频道/邮箱的那种包。

    (另外:defer wg.Done() 应该是函数的第一行,而不是最后一行。或者,只需将wg.Done() 作为函数的最后一行。)

    现在,如果此时您获得第六个额外的地鼠并让他这样做:

    {
        wg.Wait()
        close(queue)
    }
    

    他会在wg.Wait() 里面停下来,等待。

    您的主要 gopher 现在继续执行 for 循环。这从邮箱中读取,即现在您的 main gopher 将手伸入邮箱/窗口,位于通道的“获取”侧。五个等待的地鼠中的一个终于可以把他的绳子放到你的地鼠手里了。您的主要 Gopher 获取字符串并将其带回您的 for 循环。被阻止的五个地鼠中的一个现在可以执行他的wg.Done() 并过期(大概是去一个退休地鼠的幸福之地?)。

    您的主要地鼠继续for 循环,通过邮箱获取更多包裹。当他这样做时,在邮箱“put”上等待的四个地鼠完成并呼叫wg.Done(),这会将工作组计数器倒计时。当计数达到零时,不再有 gopher 等待将包裹放入邮箱,但现在正在等待 wg.Wait() 的 gopher 已被唤醒。所以他很快就会醒来并打电话给close

    如果他还没有打电话给close,那么你的主 gopher 就会卡住等待下一个包。所以只有剩下的一个 gopher 可以做任何事情:他会做关闭。或者,也许他很快醒来并且已经关闭了,但如果是这样,你的主 gopher 已经看到邮箱窗口永远关闭了。无论哪种方式,您的主 gopher 已经看到或即将看到邮箱窗口(通道)已关闭,for 循环将停止,您的主 gopher 将从 main 返回并返回去快乐的退休之地。

    不过,作为Burak Serdar noted,如果没有单独的 gopher 执行 wg.Wait() 后跟 close,那么执行 wg.Wait() 的是您的 main gopher。所以他永远不会绕过for 循环来读取(仍然打开的)邮箱/频道。你的五个地鼠睡着了,等着一个地鼠把手伸进邮箱去取他们的包裹。您的主要地鼠睡着了,等待sync.WaitGroup 中的计数器降为零。大家都睡了!

    【讨论】:

    • 谢谢@torek,有点疑问,... then they stop, waiting for a gopher to run up to the "put" side of the channel... 不应该是"get" side of the channel 吗?另外,如果每个 gopher 在执行 i &lt;- value 后等待另一个 gopher 读取它,那么等待组的目的是什么?我尝试在没有 wg 的情况下运行它,其中一个 gopher 正在写入通道,而另一个来自主线程的 gopher 正在读取它,这导致了死锁。不知道为什么...
    • 是的,你是对的 - 匆忙输入。等待组的目的是让关闭的 goroutine(或 gopher)可以等到所有发送者都完成 - 否则他可能会过早关闭通道。而且,如果没有人关闭频道,for 循环不会终止:他不知道没有人会过来放任何东西。
    【解决方案2】:

    在第一个程序中,主 goroutine 创建了几个 goroutine,每个 goroutine 开始等待通道写入。然后主 goroutine 创建另一个等待 wg.Wait() 的 goroutine。主 goroutine 继续,从通道中读取,这也使所有 goroutine 一个接一个地启用,然后它们终止,释放在 wg.Wait() 上等待的 goroutine。

    在第二个程序中,您再次创建等待通道写入的 goroutine,但这一次,主 goroutine 调用 wg.Wait()。此时,你创建的所有 goroutine 都在等待 channel 变为可写,而 main goroutine 正在等待 goroutines 结束,这意味着死锁

    【讨论】:

      猜你喜欢
      • 2023-02-11
      • 2020-03-18
      • 2017-10-20
      • 2013-05-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-02-18
      相关资源
      最近更新 更多