我一直在研究Golang,并通过其创新的goroutines构造来观察其并发性与仅执行协程通道模型的执行情况。
我立即发现麻烦的一件事是使用Wait()
方法,该方法用于等待直到在父goroutine中产生的多个未完成的goroutine完成为止。引用Golang docs
等待可用于阻止,直到所有goroutine完成为止
许多Go开发人员prescribe Wait()
作为实现并发的首选方法的事实似乎与Golang的使开发人员能够编写高效软件的使命相反,因为阻塞是低效的,真正的异步代码永远不会阻塞。
被阻塞的进程[或线程]是等待某个事件的事件,例如资源变得可用或I / O操作完成。
换句话说,被阻塞的线程将花费CPU周期做无用的事情,只是反复检查以查看其当前正在运行的任务是否可以停止等待并继续执行。
在 truly 异步代码中,当协程遇到无法继续运行直到结果到达的情况时,它必须将其执行交给调度程序代替进行阻塞将其状态从运行切换为等待,以便调度程序可以开始从 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)
}
}
我可以理解,第二个可能比第一个更容易理解,但是第一个是异步的,其中没有协程阻塞,而第二个有一个协程阻塞:一个运行主函数。 Here是Wait()
是公认的方法的另一个示例。
如果Wait()
创建了效率低下的阻塞线程,为什么Go社区不认为{{1}}是反模式?在这种情况下,为什么通道不是大多数人所喜欢的,因为它们可以用来使所有代码保持异步并优化线程?
答案 0 :(得分:12)
您对“阻止”的理解不正确。 WaitGroup.Wait()
之类的阻塞操作或通道接收(当没有值可接收时)仅阻塞goroutine的执行,它们不会(有必要)阻塞用于执行( )goroutine。
无论何时遇到阻塞操作(例如上述操作),goroutine调度程序都可能(并且它将)切换到另一个可以继续运行的goroutine。在WaitGroup.Wait()
调用过程中,不会丢失(大量)CPU周期,如果还有其他goroutines可以继续运行,它们将继续运行。