1. Goroutines是什么?
Goroutines是Go语言中轻量级的线程实现,它是Go语言并发编程的核心。
每个Goroutine都是一个受Go语言调度器控制的运行过程,Go语言的并发模型采用了抢占式调度,也就是说Go语言调度器决定哪个Goroutine运行。一个Goroutine不会阻断其他Goroutine的运行,遇到I/O操作或阻塞的情况时,Goroutine会自动释放CPU资源。
func main() {
go func() {
fmt.Println("Goroutine1")
}()
go func() {
fmt.Println("Goroutine2")
}()
time.Sleep(time.Second)
}
上述代码中,我们启动了两个Goroutine,它们并发地打印出"Goroutine1"和"Goroutine2"。由于Go语言采用抢占式调度,我们无法预测Goroutine1和Goroutine2的打印顺序,它们之间可能会交替打印。
2. Goroutines的抢占式调度
2.1 调度器的几个概念
在深入探讨Goroutines的抢占式调度前,我们需要先了解调度器的几个概念。
GOMAXPROCS
GOMAXPROCS表示可以同时运行的最大CPU数量,它能够控制调度器并行工作的数量。默认情况下,GOMAXPROCS的值等于CPU核心数。
P
P即processor,是执行Goroutines的线程,每个P都有一个对应的队列M(machine),M是一个实际的线程,通过它来执行Goroutines。多个P和M共同组成Go语言的调度器结构。
G
G即goroutine,在Go语言中所有的并发操作都是以Goroutine的形式完成的,GORoutine抢占式调度的时候,由于Goroutine很轻量,所以消耗资源很低,可以同时启动大量的Goroutine。
2.2 Goroutines的抢占式调度流程
了解了上述概念后,我们可以来看一下Goroutines的抢占式调度流程。
Goroutine进入Runnable状态
当一个Goroutine创建时,它处于Runnable状态,等待调度器的执行。
Goroutine被调度器调度到P
当一个Goroutine被调度时,调度器会选择一个P,并将该Goroutine添加到该P的队列中。
P执行Goroutine
P会从自己的队列中弹出一个Goroutine并执行。在执行Goroutine时,如果Goroutine遇到了I/O操作或阻塞的情况,它会自动释放P并进入Wait状态,等待下一次调度。
调度器释放P
当P释放Goroutine时,调度器会将该Goroutine放回队列中,等待被其他P调度执行。
2.3 需谨慎的阻塞操作
由于Go语言采用的是抢占式调度,因此在使用阻塞操作时需要特别注意,习惯性使用阻塞操作可能会影响并发效率。
阻塞操作会将Goroutine从P中剥离,并进入Wait状态,当等待条件满足后,Goroutine才进入Runnable状态,等待调度器调度。在等待条件满足的这段时间内,Goroutine所绑定的P无法执行其他任务。
因此,如果只是简单的阻塞等待,可以使用无阻塞方式等待或使用channel传递消息等方式实现。
// 错误示例:使用time.Sleep()作为简单的等待方式
func main() {
go func() {
fmt.Println("Goroutine1")
time.Sleep(time.Second) // 阻塞等待
fmt.Println("Goroutine1 Done")
}()
go func() {
fmt.Println("Goroutine2")
time.Sleep(time.Second) // 阻塞等待
fmt.Println("Goroutine2 Done")
}()
time.Sleep(time.Second * 2)
}
// 正确示例:使用channel传递消息作为等待方式
func main() {
var wg sync.WaitGroup
done := make(chan bool)
wg.Add(2)
go func() {
fmt.Println("Goroutine1")
<-done // 无阻塞等待
fmt.Println("Goroutine1 Done")
wg.Done()
}()
go func() {
fmt.Println("Goroutine2")
<-done // 无阻塞等待
fmt.Println("Goroutine2 Done")
wg.Done()
}()
time.Sleep(time.Second)
close(done)
wg.Wait()
}
3. 总结
抢占式调度是Go语言并发编程的重要特性之一,通过合理的使用Goroutines,我们可以使程序更加高效地利用CPU资源。
同时,对于阻塞操作的使用需要特别注意,合理地设置阻塞操作是提高并发效率的一个关键因素。