Golang并发编程进阶指南:探讨Goroutines的抢占式调度

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资源。

同时,对于阻塞操作的使用需要特别注意,合理地设置阻塞操作是提高并发效率的一个关键因素。

后端开发标签