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)
}
相关推荐
yagamiraito_2 小时前
757. 设置交集大小至少为2 (leetcode每日一题)
算法·leetcode·go
k***1953 小时前
自动驾驶---E2E架构演进
人工智能·架构·自动驾驶
Filotimo_4 小时前
Spring Boot 整合 JdbcTemplate(持久层)
java·spring boot·后端
半桶水专家4 小时前
Go 语言时间处理(time 包)详解
开发语言·后端·golang
编程点滴4 小时前
Go 重试机制终极指南:基于 go-retry 打造可靠容错系统
开发语言·后端·golang
Mr_sun.4 小时前
Day11——微服务高级
微服务·云原生·架构
小毅&Nora4 小时前
【AI微服务】【Spring AI Alibaba】 ① 技术内核全解析:架构、组件与无缝扩展新模型能力
人工智能·微服务·架构
q***76664 小时前
SDN架构详解
架构
码事漫谈4 小时前
AI编程规模化实践:从1到100的工程化之道
后端
码事漫谈5 小时前
AI编程:更适合0到1的创意爆发,还是1到100的精雕细琢?
后端