我一直在研究Golang,看看它通过其创新的goroutines构造实现了一个仅限协程通道的模型,它的并发性有多好。
我立即发现令人不安的一件事是使用Wait()
方法,用于等待父母goroutine中产生的多个突出的goroutine已经完成。引用Golang docs
等待可以用来阻止所有goroutines完成
事实上许多人将开发人员prescribe Wait()
作为实现并发性的首选方式,这似乎与Golang使开发人员编写高效软件的使命相对立,因为阻塞效率低下,真正的异步代码永远不会阻塞。
被阻止的进程[或线程]是等待某个事件的进程[或线程],例如资源变为可用或完成I / O操作。
换句话说,被阻塞的线程将花费CPU周期无用,只需反复检查以查看其当前正在运行的任务是否可以停止等待并继续执行。
在真正的异步代码中,当协同程序遇到无法继续直到结果到达的情况时,它必须通过将其状态从运行切换到等待来将其执行发送到调度程序而不是阻塞,因此调度程序可以开始执行下一个 - 来自可运行队列的内联协程。只有在需要的结果到达时,等待的协程应该将其状态从等待运行变为可运行。
因此,由于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)
}
}
我可以理解,第二个可能比第一个更容易理解,但第一个是异步,其中没有协同程序阻塞,第二个有一个coroutine阻塞:运行main函数的协同程序。 Here是Wait()
作为普遍接受的方法的另一个例子。
为什么Wait()
被Go社区视为反模式,如果它创建了一个效率低下的阻塞线程?在这种情况下,为什么大多数通道都不是首选,因为它们可以用来保持所有代码的异步和线程优化?
您对“阻止”的理解不正确。阻塞操作(例如WaitGroup.Wait()
或通道接收(当没有值接收时)仅阻止执行goroutine时,它们(必然)不会阻塞用于执行goroutine(的语句)的OS线程。
每当遇到阻塞操作(例如上面提到的)时,goroutine调度程序可以(并且它将)切换到可以继续运行的另一个goroutine。在WaitGroup.Wait()
调用期间没有(重大的)CPU周期丢失,如果有其他goroutine可能继续运行,他们会。