Go语言中的goroutine调度是如何实现的?

11 阅读3分钟

在Go语言中,goroutine是一种轻量级的线程,它使得并发编程变得更加简单和高效。而goroutine的调度则是Go运行时(runtime)系统负责的一个核心任务,它决定了goroutine如何被分配到CPU上执行。

一、M:N调度模型

Go的goroutine调度器采用了M:N的调度模型,即多个goroutine映射到少量的线程(M)上执行。这种模型的好处是可以有效地利用多核CPU资源,同时减少线程切换的开销。

二、GMP模型

为了实现M:N调度模型,Go引入了GMP模型:

  • G(Goroutine):代表Go语言的协程,即轻量级线程。
  • M(Machine):代表操作系统的线程,是goroutine运行的载体。
  • P(Processor):代表逻辑处理器,它负责调度goroutine到M上执行,并维护本地运行队列。

在GMP模型中,P的数量通常等于CPU的核数,而M的数量则可能大于P的数量。当P没有可用的G时,M会阻塞等待新的G;当G很多时,P会创建更多的M来执行G。

三、调度过程

  1. 全局队列:所有新创建的goroutine都会被放入全局队列中等待调度。
  2. 本地队列:每个P都维护一个本地队列,用于存放待执行的goroutine。当M执行完当前G后,会尝试从P的本地队列中取下一个G执行。
  3. 工作窃取:如果P的本地队列为空,它会尝试从其他P的本地队列中“窃取”G来执行,以实现负载均衡。
  4. 系统调用:当G进行系统调用时,M会被阻塞,P会转而调度其他M执行其他G。当系统调用返回时,M会尝试获取一个空闲的P继续执行G。

四、调度优化

为了进一步提高调度效率,Go的调度器还采用了一些优化手段,如:

  • 批量窃取:允许一个P一次性窃取多个G,减少窃取操作的开销。
  • 自适应调整:根据系统负载动态调整P和M的数量,以达到更好的性能。

五、示例代码

下面是一个简单的示例代码,展示了如何创建和使用goroutine:

package main

import (
 "fmt"
 "sync"
 "time"
)

func worker(id int, wg *sync.WaitGroup) {
 defer wg.Done()
 fmt.Printf("Worker %d started\n", id)
 time.Sleep(time.Second) // 模拟耗时操作
 fmt.Printf("Worker %d finished\n", id)
}

func main() {
 var wg sync.WaitGroup
 for i := 0; i < 5; i++ {
 wg.Add(1)
 go worker(i, &wg) // 创建goroutine并执行worker函数
 }
 wg.Wait() // 等待所有goroutine执行完毕
 fmt.Println("All workers finished")
}

在上面的代码中,我们创建了5个goroutine来执行worker函数。每个worker函数模拟了一个耗时操作,并通过sync.WaitGroup来等待所有goroutine执行完毕。由于Go的调度器会自动管理goroutine的调度和执行,我们无需关心底层的线程切换和调度细节。

总结

Go语言的goroutine调度器通过GMP模型和一系列优化手段,实现了高效、灵活的并发执行。这使得在Go中编写并发程序变得更加简单和直观。通过合理利用goroutine和channel等特性,我们可以轻松地构建出高性能、高并发的应用程序。


掘金小册


相关推荐