Skip to main content

scheduler

Go 语言中,go func 是并发的单元,chan 是协调并发单元的机制,panic 和 recover 是出错处理的机制,而 defer 是神来之笔,大大简化了出错的管理。

服务启动时候,会创建一个初始进程并且启动一个线程。线程是可以去创建更多的线程,这些线程可以独立地执行,CPU在这一层进行调度,而非进程。

操作系统的调度器保证CPU不闲着,所有可执行的线程都能得到执行。另外还要保持高优先级的线程执行机会大于低优先级的线程,但也不能让低优先级的线程始终得不到执行的机会。

线程切换

线程三种状态:Waiting、Runnable、Executing。

这三种状态一直在切换。保证线程的执行

线程主要是做:计算型、IO 型。

操作系统调度器保证每个线程都能得到执行,就需要切换线程,保存线程的数据。待到下次CPU执行时能够接着执行。

函数调用过程

ESP 寄存器:栈指针 EBP 寄存器:基址指针或者帧指针

调用描述

若函数 A 调用函数 B,那么 A 函数一般叫做调用者,B 函数一般为被调用者,函数调用过程可以做如下描述:

  1. 现将函数A的堆栈基址ebp入栈,用于保存之前任务信息
  2. 然后将函数A的栈顶指针esp的值赋给ebp,用作新的基址(这里就是函数B的栈底)
  3. 紧接着在新的ebp基础上开辟相应的空间当做被调用者B的栈空间,开辟空间一般用sub指令;
  4. 函数B返回后,从当前栈底ebp恢复为调用者A的栈顶esp,使得栈顶恢复成函数B被调用前的位置;
  5. 最后调用者A从恢复的栈顶弹出之前的ebp值(因为在函数调用前一步被压入堆栈);这样ebp和esp都变成了调用函数B前的位置;
  6. 栈是往下增长的,所以 esp 指向下面👇。
  7. 在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp);

![[Pasted image 20250707165142.png]]

goroutine vs thread 区别

三个角度看:内存消耗、创建与销毁、切换

内存占用: 创建一个 goroutine 栈内存消耗为 2 KB,实际运行过程中,栈空间可以自动进行扩容。而创建一个 thread 需要消耗 1 MB 栈内存,还需要与其他 thread 的栈空间进行隔离。

创建于销毁: Thread 的创建与销毁都会有巨大的消耗,因为要和操作系统打交道,是内核级,通常解决办法就是线程池。而 goroutine 是由 Go runtime 负责管理的,创建和销毁的消耗都非常小,是用户级的。

切换: Thread 切换时,需要保存各种寄存器,以便将来恢复。而 goroutine 切换只需保存三个主要的寄存器:Program Counter,Stack Pointer and BP。线程切换大概 100 ~ 1500 ns,goroutine 约为 200 ns。

M:N 模型

Go runtime 会在程序启动时,创建 M 个线程(CPU执行调度的单位),之后创建 N 个 goroutine 都会依附在这 M 个线程上执行。这就是 M:N 模型。

Go 运行时调度

GPM 模型:

  • G :代表 goroutine;
  • P:代表虚拟的 Processor ,维护 goroutine 队列
  • M:代表内核线程,用于运行 goroutine;

Runtime 起始时会启动一些 G:垃圾回收的 G ,执行调度的 G,运行用户代码的 G;并且会创建一个 M 用来开始 G 的运行。随着时间的推移,更多的 G 会被创建出来,更多的 M 也会被创建出来。

核心思想:

  1. 重用 threads;
  2. 限制同时运行的线程数为;
  3. 线程执行过程中,当空闲时可以从其他线程 stealing goroutine 来运行。

全局可运行队列和本地可运行队列

其中本地可运行队列由 P 来管理。

goroutine 状态也有三种: Waiting 、Runnable、Executing 三种状态。

![[bb79f73adc5ce4ccd63cfcde242d0931.jpg]]

goroutine 可能会进行系统调用,会导致 M 被阻塞。