0%

Golang调度

GMP调度模型演进

线程、进程切换

最初的操作系统是单进程的,及多个进程排队执行。在这种情况,若遇到前序进程因为某些原因阻塞,则后面的进程长时间获取不到时间片,则会陷于饥饿状态。后面人们为了解决这个问题,就涉及许多调度算法,例如多级轮转队列等,来让不同的进程抢占时间片。但后面人们发现,不同进程之间的切换成本太高了(需要保存寄存器地址,和栈现场等)。于是线程概念由此诞生。

相较于进程,线程有着更小的栈内存,这意味着创建、切换和销毁成本更低。但是由于线程的切换还是发生在内核态,仍然需要内核调用,切换成本包含:

1)一个内核线程的大小通常达到1M,因为需要分配内存来存放用户栈和内核栈的数据;

2)在一个线程执行系统调用(发生 IO 事件如网络请求或读写文件)不占用 CPU 时,需要及时让出 CPU,交给其他线程执行,这时会发生线程之间的切换;

3)线程在 CPU 上进行切换时,需要保持当前线程的上下文,将待执行的线程的上下文恢复到寄存器中,还需要向操作系统内核申请资源;

在高并发的情况下,大量线程的创建、使用、切换、销毁会占用大量的内存,并浪费较多的 CPU 时间在非工作任务的执行上,导致程序并发处理事务的能力降低。

![[thread_switch.png]]

用户态线程(用户态协程)

为了解决传统内核级的线程的创建、切换、销毁开销较大的问题,Go 语言将线程分为了两种类型:内核级线程 M (Machine),轻量级的用户态的协程 Goroutine,至此,Go 语言调度器的三个核心概念出现了两个:

M: Machine的缩写,代表了内核线程 OS Thread,CPU调度的基本单元;

G: Goroutine的缩写,用户态、轻量级的协程,一个 G 代表了对一段需要被执行的 Go 语言程序的封装;每个 Goroutine 都有自己独立的栈存放自己程序的运行状态;分配的栈大小 2KB,可以按需扩缩容;
![[1v1.png]]