Go高并发背后的功臣:Goroutine调度器详解

Goroutine调度器概览

在编程中,"调度"这个概念,我们最熟悉的莫过于操作系统对进程和线程的调度。简单来说,就是操作系统的调度器会按照特定算法,将多个线程合理地分配到各个物理 CPU 核心上执行。

像 Java、C++ 这类传统语言,其并发能力正是构建在这种操作系统线程模型之上的。程序通过调用库函数来创建线程,而线程的调度和管理则完全交由操作系统内核负责。这种方式的弊端在于,内核级线程的创建、销毁以及上下文切换都是重量级操作,需要深入内核态,成本很高,这在一定程度上限制了程序的高并发能力。

为了突破这一瓶颈,Go 语言选择了一条不同的路:它在用户层实现了一种轻量级的线程,称之为 goroutine。每个 goroutine 占用的资源极少,因此在一个 Go 程序中创建成千上万个并发执行的 goroutine 是轻而易举的。

既然 goroutine 是用户级的,操作系统并不可见,那么将它们高效地调度到 CPU 上执行的任务,就不能再由操作系统代劳了。这个重任便落在了 goroutine 调度器的肩上。对于操作系统而言,一个 Go 程序只是一个普通的进程,它只关心这个进程下的线程。因此,如何将数万个 goroutine 高效地映射到少量操作系统线程上执行,并充分发挥多核优势,这一切复杂的调度工作,都需要 Go 自身的调度器独立完成。这正是 Go 高并发能力的核心所在。

Goroutine调度模型简介

Goroutine调度模型(GPM模型)是其高并发能力的基石。通过将轻量级线程Goroutine、逻辑处理器Processor和系统线程M有机结合,使得Go语言在处理高并发场景时表现出色。

GPM模型是Goroutine调度器实现的理论设计,它由三个关键组件组成:

  1. G- Goroutine:Go语言的轻量级线程
  2. P- Processor:逻辑处理器,负责调度Goroutine
  3. M- Machine:系统线程,真正执行代码的实体

Goroutine的GPM模型详解

每个goroutine对应于运行时中的一个抽象结构------G(goroutine),而被视作"物理CPU"的操作系统线程则被抽象为另一个结构------M(machine),从Goroutine(G)的视角看,它所在的P(逻辑处理器)就是其运行时的"CPU",但站在调度器的全局高度,真正的"CPU"是操作系统线程(M)。因此,调度器的关键任务就在于将P与M进行绑定,从而让P本地队列中的G能够通过M真正运行起来。这种P与M动态的、多对多的绑定关系,构成了Go调度器高效并发的基石。

另外一点就是Goroutine的并发模型是建立在操作系统并发能力之上的增强而非替代,Goroutine调度器解决了"海量任务如何在少量线程上高效协作"的问题,操作系统调度器解决"多个线程如何在有限CPU上公平运行"的问题。

Go并发代码学习示例

go 复制代码
package main

import "time"

func Check1(id int) int {
	check1TmCost := 200
	time.Sleep(time.Millisecond * time.Duration(check1TmCost))
	print("\tgoroutine-", id, ": Check1 ok\n")
	return check1TmCost
}

func Check2(id int) int {
	check2TmCost := 300
	time.Sleep(time.Millisecond * time.Duration(check2TmCost))
	print("\tgoroutine-", id, ": Check2 ok\n")
	return check2TmCost
}

func Check3(id int) int {
	check3TmCost := 400
	time.Sleep(time.Millisecond * time.Duration(check3TmCost))
	print("\tgoroutine-", id, ": Check3 ok\n")
	return check3TmCost
}

func CheckProcess(id int) int {
	total := 0

	total += Check1(id)
	total += Check2(id)
	total += Check3(id)
	return total
}

func start(id int, f func(int) int, queue <-chan struct{}) <-chan int {
	c := make(chan int)
	go func() {
		total := 0
		for {
                _, ok := <-queue
                if !ok {
                        c <- total
                        return
                }
                total += f(id)
		}
	}()
	return c
}

func max(args ...int) int {
	n := 0
	for _, v := range args {
		if v > n {
			n = v
		}
	}
	return n
}
//main函数也是一个gorountine
func main() {
	total := 0
	num := 11
	c := make(chan struct{})
	//创建gorountine,go调度器执行调度
	c1 := start(1, CheckProcess, c)
	c2 := start(2, CheckProcess, c)
	c3 := start(3, CheckProcess, c)

	for i := 0; i < num; i++ {
		//向channel发送信号
		c <- struct{}{}
	}
   //关闭channel
	close(c)

	total = max(<-c1, <-c2, <-c3)
	println("total time cost:", total)
}
相关推荐
晚霞的不甘18 小时前
Flutter for OpenHarmony 实现计算几何:Graham Scan 凸包算法的可视化演示
人工智能·算法·flutter·架构·开源·音视频
Tadas-Gao18 小时前
TCP粘包现象的深度解析:从协议本质到工程实践
网络·网络协议·云原生·架构·tcp
苏三说技术18 小时前
xxl-job 和 elastic-job,哪个更好?
后端
三小河18 小时前
Agent Skill与Rules的区别——以Cursor为例
前端·javascript·后端
三小河19 小时前
前端视角详解 Agent Skill
前端·javascript·后端
牛奔19 小时前
Go 是如何做抢占式调度的?
开发语言·后端·golang
礼拜天没时间.19 小时前
深入Docker架构——C/S模式解析
linux·docker·容器·架构·centos
颜酱19 小时前
二叉树遍历思维实战
javascript·后端·算法
啊森要自信19 小时前
CANN runtime 深度解析:异构计算架构下运行时组件的性能保障与功能增强实现逻辑
深度学习·架构·transformer·cann
WindrunnerMax19 小时前
从零实现富文本编辑器#11-Immutable状态维护与增量渲染
前端·架构·前端框架