Go语言的协程,系统线程以及CPU管理

本文介绍了Go语言如何通过M,P,G模型实现高效的协程调度,以及在系统调用和线程管理上的优化策略。Go语言会根据逻辑CPU数量创建处理器(P),每个协程在P上运行,而P与系统线程(M)关联。系统调用时,Go会自动解除上下文绑定,避免阻塞,确保资源的有效利用。同时,文章还探讨了Go对系统线程数的限制策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

创建系统线程以及在系统线程间切换,会对程序的内存和性能造成较大的开销。
Go 的目标是尽量利用 CPU 多核资源。设计之初就考虑了高并发性。

M,P,G 模型

为了达到这个目标,Go拥有一个将协程调度到系统线程执行的调度器

这个调度器定义了三个核心概念,在Go源码中是这样解释的:

  • M - worker thread, or machine. 工作线程
  • P - processor(逻辑处理器), 执行 Go 代码时所必须的上下文环境。
  • G - goroutinue 协程

M必须有一个相关联的P才能执行Go代码。

以下是 P,M,G 模型的示意图:
在这里插入图片描述

每个协程(G)在一个分配给逻辑处理器(P)的系统线程(M)上运行。

example

func main(){
	var wg sync.WaitGroup
	wg.add(2)
	
	go func(){
		fmt.Println("hello")
		wg.Done()
	}()
	
	go func(){
		fmt.Println("world")
		wg.Done()
	}()
	
	wg.Wait()
}

execute stream
在这里插入图片描述

首先,Go 会根据当前机器的逻辑 CPU 个数来创建相应数量的 P,并将它们存放在一张空闲 P 列表中

在这里插入图片描述

然后,新创建并等待被运行的 goroutine 协程会唤醒一个 P 来执行这个任务,这个 P 会创建一个和系统内核级线程相关联的用户级线程 M

在这里插入图片描述

P 一样,如果一个 M 没有工作可做了,该 M 会被放入空闲 M 链表中

在这里插入图片描述

在程序启动时,Go 会预先创建一些内核级线程以及相关联的用户级线程 M
在上面的小例子中,第一个打印 hello 的协程会使用主协程,而第二个打印world的协程会从空闲列表中获取到一个 M 和一个 P

以上,我们有了一张管理协程内核线程的全局图,让我们进一步看看 Go 在什么情况下会使用更多的 MP,以及系统调用时协程是如何被管理的。

系统调用

Go 对系统调用做了优化,具体做法是在运行时对系统调用做了封装(不管系统调用是否会造成阻塞)。
该部分封装代码会自动将上下文环境 P 与用户线程 M 解除绑定,使得另一个用户线程 M 可以在这个 P 上运行。

让我们来看一个读取文件的例子:

func main(){
	buf:=make([]byte,0,2)
	
	fd,_ := os.Open("number.txt")
	fd.Read(buf)
	defer fd.Close()
	
	fmt.Printlb(string(buf))
}

以下是打开文件的流程:
在这里插入图片描述
G 执行系统调用,P0 被放入空闲列表中,可被使用。
当系统调用结束之后,Go顺序执行如下流程直到其中一条规则被满足:

  1. 试图获取同一个 P,在我们上面的例子就是 P0,如果获取到,则恢复执行。
  2. 试图在空闲列表中获取一个 P,如果获取到,则恢复执行。
  3. G(协程)放入全局队列中,将相关的 M 放入空闲列表中。

Go 使用非阻塞 I/O 模式,对资源还没有就绪的情况也做了处理,比如说 http 请求。
这种情况下首先也遵循上面所说的系统调用的流程,之后如果底层的系统调用由于资源没有就绪而返回失败时,Go 会强制使用 network poller,并且将该协程挂起。

以下是例子:

func main(){
	http.Get("https://httpstat.us/200")
}

在这里插入图片描述

  • 当底层的系统调用返回并且显式表示资源没有就绪时,协程将被挂起,直到 network poller 通知它资源已经就绪。
  • 这种情况下,线程M不会被阻塞,而是被调度去执行其它G

在这里插入图片描述

  • Go调度器重新调度时,之前的那个G(协程)将被重新运行。
  • 调度器会询问network poller是否存在之前在等待资源并且现在资源已经就绪G协程。
  • 如果有多个G(协程)就绪了,其它的G(协程)会被放入全局等待执行队列中,稍后会被调度执行。

关于系统线程数的限制

当使用了系统调用时,Go并不限制这些可能被阻塞的系统线程的数量,以下是Go代码中的注释说明:

GOMAXPROCS变量限制的是用户层面Go代码的系统线程数量。
对于可能造成阻塞的系统调用的线程数是不做限制的;它们不计算在GOMAXPROCS限制之中。

以下是一个例子

func main(){
	var wg sync.WaitGroup
	for i:=0;i<100;i++ {
		wg.Add(1)
		
		go func(){
			http.Get(`https://httpstat.us/200?sleep=10000`)
			wg.Done()
		}()
	}

	wg.Wait()
}

值得一提,由于 Go 可以复用内核线程,所以工具查看到的线程数要小于例子中 for 循环的次数。

以下是使用tracing工具,查看程序创建的线程数量:
在这里插入图片描述

100 个协程被映射到了 2 个线程上

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值