【发布时间】: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() 不被认为是一种反模式……如果它创建了一个低效的阻塞线程?” -- 仅仅因为它不会创建一个低效的阻塞线程。
-
<- channel也被屏蔽了。 -
“非阻塞等待方式”在术语上是矛盾的。阻止意味着等待。
-
这个问题的一个主要误解是阻塞的 goroutine 会花费 CPU 周期反复检查当前的 goroutine 是否可以停止等待并继续执行。这不是它的工作方式。在重新安排运行之前,goroutine 不会消耗任何周期。
标签: go concurrency blocking channel coroutine