【问题标题】:If the Wait() method of the sync.WaitGroup type blocks, and is thus not asynchronous, why use it?如果 sync.WaitGroup 类型的 Wait() 方法阻塞,因此不是异步的,为什么要使用它?
【发布时间】:2019-02-12 19:14:02
【问题描述】:

我一直在研究 Golang,并通过其创新的 goroutines 构造来了解它的并发性有多好,因为它强制执行纯协程通道模型。

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

Wait 可用于阻塞,直到所有 goroutine 完成

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

被阻塞的进程[或线程]是等待某个事件的进程,例如资源变得可用或 I/O 操作完成。

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

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

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

如果这一切都是真的,并且我理解Wait() 是如何正常工作的,那么为什么人们不使用内置的 Go 通道来完成等待子 goroutine 完成的任务呢?如果我理解正确的话,发送到缓冲通道和从任何通道读取都是异步操作,这意味着调用它们会使 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)
    }
}

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

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

【问题讨论】:

  • “真正的异步代码永远不会阻塞”——完全不正确。异步只是意味着整个进程没有被阻塞。 Wait() 并没有使整个进程阻塞——它显然只会阻塞调用它的单个 goroutine。
  • “为什么 Wait() 不被认为是一种反模式……如果它创建了一个低效的阻塞线程?” -- 仅仅因为它不会创建一个低效的阻塞线程。
  • &lt;- channel 也被屏蔽了。
  • “非阻塞等待方式”在术语上是矛盾的。阻止意味着等待。
  • 这个问题的一个主要误解是阻塞的 goroutine 会花费 CPU 周期反复检查当前的 goroutine 是否可以停止等待并继续执行。这不是它的工作方式。在重新安排运行之前,goroutine 不会消耗任何周期。

标签: go concurrency blocking channel coroutine


【解决方案1】:

您对“阻塞”的理解是错误的。阻塞操作,如WaitGroup.Wait() 或通道接收(当没有值接收时)只会阻塞 goroutine 的执行,它们不会(必然)阻塞用于执行 goroutine (语句)的 OS 线程.

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

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

【讨论】:

  • 我认为阻塞似乎是一个错误的词,因为根据我的经验,它总是参考操作系统线程。无论如何,WaitGroup.Wait() 和通道的操作是否会接收到 Go 调度程序的信号以立即停止这些 goroutine 的执行,还是只会由于限制 goroutine 运行时间的抢先超时而停止?
  • @AjaxLeung:“根据我的经验 [阻塞] 总是参考操作系统线程。” -- 这不是“阻塞”这个词的问题,只是之前没有使用过这种执行模型的结果。
  • @AjaxLeung 在 Go 世界中,几乎所有语句都与 goroutines 相关,而不是线程。如果有人说某事正在阻塞,这意味着它阻塞了 goroutine——除非另有明确说明。
  • 其他语言将任务描述为“暂停”或“暂停”,以免将这个概念与可以阻塞的线程混为一谈。有趣的是,当其他地方使用替代方案时,Go 世界选择使用块。
  • @AjaxLeung Wait() 是一个很好的调度点,但它并不能保证其他 goroutine 会被调度。这取决于调度程序。例如。如果所有Done() 调用都在Wait() 调用之前,那么Wait() 将不会阻塞并且goroutine 可能会继续(没有调度程序调度另一个goroutine)。
猜你喜欢
  • 1970-01-01
  • 2015-12-15
  • 1970-01-01
  • 1970-01-01
  • 2015-03-09
  • 1970-01-01
  • 2017-07-17
  • 1970-01-01
  • 2011-05-02
相关资源
最近更新 更多