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

标签 go concurrency blocking channel coroutine

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

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

Wait can be used to block until all goroutines have finished

许多go开发者的事实prescribe Wait() 作为实现并发的首选 方式似乎与 Golang 使开发人员能够编写高效软件的使命背道而驰,因为阻塞 是低效的,并且真正的异步代码从不阻塞。

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 周期做无用的事情,只是反复检查当前运行的任务是否可以停止等待并继续执行。

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

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

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

我引用的文章给出了几个例子。以下是作者所说的“老派”方式:

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)
    }
}

这是首选的“规范”方式:

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 社区不认为它是一种反模式?为什么在这种情况下 channel 不是大多数人的首选,因为它们可以用来保持所有代码异步和线程优化?

最佳答案

您对“阻塞”的理解不正确。诸如 WaitGroup.Wait() 或 channel 接收(当没有可接收的值时)之类的阻塞操作只会阻塞 goroutine 的执行,它们不会(必然)阻塞所使用的 OS 线程执行(的)goroutine。

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

请查看相关问题:Number of threads used by Go runtime

关于go - 如果 sync.WaitGroup 类型的 Wait() 方法阻塞,因此不是异步的,为什么要使用它?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52222438/

相关文章:

Go 不接收错误会触发 panic ,但接收错误不会触发 panic

go - 如何从 url 池发出并发 GET 请求

c - 事件驱动 IO 和阻塞与非阻塞

python - Python 中的阻塞子进程函数?

具有未导出字段的 Golang 结构文字语法

go - 无法使用 Gin 加载 HTML 模板

concurrency - runtime.Gosched 到底做了什么?

multithreading - 这个 Ruby 代码会在 Puma 下使用非阻塞 I/O 吗?

string - 戈朗 : Strings Trim Function

database - dml 由多个用户/在 oracle 中提交场景