在现代操作系统中,线程分为“内核态“线程和”用户态“线程,一个“用户态线程”必须绑定一个“内核态线程”, 但是CPU并不知道有用户态编程的存在,它只是知道它运行的是一个“内核态线程”(Linux的PCB进程控制的)
image.png
这样,内核态线程依然叫“线程”,用户线程叫做协程(co-routine)

  1. <br /> ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2332713/1624985079118-50a0e122-d8f6-4fd5-99c9-ded8ce25d0b7.png#clientId=u627a17b6-d6fb-4&from=paste&height=426&id=u5e5eccb9&margin=%5Bobject%20Object%5D&name=image.png&originHeight=624&originWidth=732&originalType=binary&ratio=2&size=106552&status=done&style=none&taskId=udd7d744b-314b-409c-95e4-6827019e5ce&width=500)<br />协程和线程是多对多的关系。协程和线程是有区别的,线程是CPU调度,是抢占式的。协程是由用户态调度,是协作式的,一个协程让出CPU之后,才执行下一个协程,

在新调度器中,出列M(thread)和G(goroutine),又引进了P(Processor)。

  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2332713/1624985715181-9eda6827-fe4f-422b-899e-f3a2bad5d141.png#clientId=u627a17b6-d6fb-4&from=paste&height=410&id=u9d6017a4&margin=%5Bobject%20Object%5D&name=image.png&originHeight=600&originWidth=732&originalType=binary&ratio=2&size=74677&status=done&style=none&taskId=u89d4f64d-cf0e-4bfe-ba8e-36cd7854ad0&width=500)<br />在Go中,线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上。
  2. <br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2332713/1624985957844-b7804505-ba5c-4f18-9776-198de66332f2.png#clientId=u627a17b6-d6fb-4&from=paste&height=375&id=u85ce8e50&margin=%5Bobject%20Object%5D&name=image.png&originHeight=549&originWidth=732&originalType=binary&ratio=2&size=194310&status=done&style=none&taskId=ua8af2a8c-89c4-44df-b5cc-b61d1b78ea8&width=500)
  1. 全局队列,存放等待运行的G
  2. P的本地队列,同全局队列类似,存放的也是等待运行的G。存的数量有限,不超过256
  3. P列表,所有的P都在程序启动的时候创建,并保存在数组中,最多有GOMAXPROCS个
  4. M:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。

Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行。

有关P和M的个数问题

P的数量

由启动时环境变量$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()决定。这意味着在程序执行的任意时刻都只有$GOMAXPROCS个goroutine在同时运行

M的数量

  1. go 语言本身的限制,go程序启动时,会设置M的最大数量,默认10000但是内核很难支持这么多线程数,所以这个限制可以忽略
  2. runtime/debug中时SetMaxThreads函数,设置M的最大数量
  3. 一个M阻塞了,会创建新的

M和P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另外一个M。所以,即使P的默认数量时1,也有可能创建很多个M出来

P和M何时会被创建

  1. 在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P
  2. 当没有足够的M来关联P并运行其中可运行的G,比如所有的M此时都阻塞了,而P中还有很多就绪任务,就去寻找空闲的M,而没有空闲的,就会去创建新的M

调度器的设计策略

复用线程

避免频繁的创建,销毁线程,而是对线程复用

working stealing机制

  1. 当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程

    hand off机制

  2. 当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行

  3. 利用并行策略,GOMAXROCS设置P的数量,最多有GOMAXROCS个线程分布在多个CPU上同时运行。GOMAXROCS也限制了并发的程度,比如GOMAXROCS=核数/2,则最多利用了一半的CPU核进行并行
  4. 抢占,在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的一个地方。
  5. 全局G队列:在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G