如果 sync.WaitGroup 类型的 Wait() 方法阻塞,因此不是异步的,为什么要使用它?

If the Wait() method of the sync.WaitGroup type blocks, and is thus not asynchronous, why use it?

我一直在研究 Golang,并通过其创新的 goroutines 构造来了解它的并发性以及它的仅协程通道模型的实施有多好。

我立即觉得麻烦的一件事是 Wait() 方法的使用,该方法用于等待父 goroutine 中产生的多个未完成的 goroutine 完成。引用 Golang docs

Wait can be used to block until all goroutines have finished

许多 Go 开发人员 prescribe Wait() 作为 首选 实现并发的方式这一事实似乎与 Golang 使开发人员能够编写高效软件的使命背道而驰,因为 blocking 是低效的,真正的异步代码 never blocks.

A process [or thread] that is blocked is one that is waiting for some event, such as a resource becoming available or the completion of an I/O operation.

换句话说,一个被阻塞的线程将花费 CPU 个周期来做无用的事情,只是反复检查它当前的 运行 任务是否可以停止等待并继续执行。

真正异步代码中,当协程遇到无法继续执行直到结果到达的情况时,它必须将其执行让给调度程序而不是 的阻塞,通过将其状态从 运行 切换到 waiting,因此调度程序可以开始执行下一个- 来自 runnable 队列的协程。只有在需要的结果到达时,等待协程才应将其状态从等待更改为可运行。

因此,由于 Wait() 阻塞直到 x 个 goroutine 调用 Done(),调用 Wait() 的 goroutine 将始终保持可运行或 运行 状态,浪费 CPU 周期并依赖调度程序抢占 long-运行 goroutine 只是为了将其状态从 运行 runnable 更改,而不是将其更改为应有的等待状态。

如果这一切都是真的,而且我理解 Wait() 如何正确工作,那么为什么人们不使用内置的 Go 通道来完成等待子 goroutines 完成的任务?如果我理解正确的话,发送到缓冲通道和从任何通道读取都是异步操作,这意味着调用它们会使 goroutine 进入等待状态,那么为什么它们不是首选方法?

我参考的文章给出了几个例子。下面是作者所说的 "Old School" 方式:

package main

import (
    "fmt"
    "time"
)

func main() {
    messages := make(chan int)
    go func() {
        time.Sleep(time.Second * 3)
        messages <- 1
    }()
    go func() {
        time.Sleep(time.Second * 2)
        messages <- 2
    }()
    go func() {
        time.Sleep(time.Second * 1)
        messages <- 3
    }()
    for i := 0; i < 3; i++ {
        fmt.Println(<-messages)
    }
}

这里是首选,"Canonical"方式:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    messages := make(chan int)
    var wg sync.WaitGroup
    wg.Add(3)
    go func() {
        defer wg.Done()
        time.Sleep(time.Second * 3)
        messages <- 1
    }()
    go func() {
        defer wg.Done()
        time.Sleep(time.Second * 2)
        messages <- 2
    }() 
    go func() {
        defer wg.Done()
        time.Sleep(time.Second * 1)
        messages <- 3
    }()
    wg.Wait()
    for i := range messages {
        fmt.Println(i)
    }
}

我能理解第二个可能比第一个更容易理解,但第一个是异步的,没有协程阻塞,第二个有一个阻塞的协程:运行 主函数。 HereWait() 被普遍接受的方法的另一个例子。

为什么 Wait() 如果它创建了一个低效的阻塞线程,Go 社区不认为它是一种反模式?为什么在这种情况下通道不是大多数人首选的,因为它们可以用来保持所有代码异步和线程优化?

您对"blocking"的理解有误。 WaitGroup.Wait() 或通道接收(当没有值可接收时)等阻塞操作只会阻塞 goroutine 的执行,它们不会(必然)阻塞用于执行 goroutine 的 OS 线程(的)goroutine.

每当遇到阻塞操作(例如上面提到的)时,goroutine 调度器可能(并且它会)切换到另一个可能继续 运行 的 goroutine。在 WaitGroup.Wait() 调用期间没有(重要的)CPU 周期丢失,如果有其他 goroutine 可能会继续 运行,它们会。

请检查相关问题: