地址:https://www.bilibili.com/video/BV19r4y1w7Nx
所有进程只能顺序执行,产生问题:
以时间片轮询的机制并发执行程序 (并行与CPU核数有关),很显然多线程/多进程可以解决1.1
中进程阻塞导致CPU浪费的问题(即是你程序阻塞,时间片到了,也会强制释放CPU)。产生问题:
CPU高消耗
以时间片轮询机制而言,为了保存原有线程的系统调用或者相关资源环境等,必然涉及到拷贝复制的过程,就会涉及到切换成本,造成CPU浪费。因此线程越多,切换过程就会更加频繁,所以线程并不是越多越好(线程越多抢占CPU会越有利)。
内存高占用
在32bit操作系统中,一个进程占用虚拟内存4GB;而一个线程占用4MB左右;因此多进程/多线程会导致内存高占用的问题。(而协程占用KB级别)
为了解决1.2
的CPU高消耗和内存高占用问题,而内核态无法修改,所以尝试修改用户态。将线程分为用户线程和内核线程。而内核线程称为线程,用户线程称为协程,由于CPU视野只有内存空间,因此协程的开辟对CPU来说是无感的。
线程通过协程调度器绑定多个协程,而CPU视野只有内核空间,所以对CPU而言只有单一线程即进程,因此此方法可有效解决CPU高消耗的问题。
每个语言对协程进行不同处理。Golang对协程进行相应优化:对协程co-routine重命名为goroutine;修改协程内存大小,每个goroutine只有几KB大小,因此可以大量创建;可灵活调度,切换成本较低。所以最后重点就落到了优化协程调度器上面。
各个线程首先需要去全局G队列拿锁,才能去执行协程挂载的任务,此时该线程不释放锁就导致其他线程无法去执行协程上的任务。
缺点:
G ------ goroutine协程 P ------ 协程调度器 M ------ 线程
每个 P
保存了当前执行的协程G
内部资源信息(堆栈地址和变量参数等),所以M
要先去获取P
才能去执行G
。创建的G
会优先存放在本地队列,如果本地队列满了(最多256个G
),会存放至全局G
队列。
P
的个数,可由环境变量中$GOMAXPROCS
设置;或在程序中可通过runtime.GOMAXPROCS()
设置。
复用线程可避免创建与销毁线程中进行的资源消耗;
实现的两种机制:
work stealing
机制
当线程M1
和P
绑定,正在执行协程G1
,而此时线程M2
空闲,此时M2
的协程调度器P
会从M1
的本地协程队列中偷取协程G
到自己这边执行。
hand off
机制
当此时M1
和M2
正常执行协程所挂载的任务,突然协程G1
发生阻塞现象(比如read/write/channel阻塞等),这时系统会尝试唤醒/创建一个线程M3
(优先唤醒,符合复用线程的思想),并把与当前阻塞线程M1
绑定的协程调度器P
转移到新的线程M3
上,并把原来阻塞线程M1
所占用的CPU进行释放。后续执行完成后,如果G1
需要执行会被重新加入队列进行执行,M1
会被睡眠或者销毁。
可充分发挥多核优势,通过设置GOMAXPROCS
设置协程调度器的个数,通常并不会挂满,设置为CPU核数/2。
相较于老的调度器而言,老调度器中只有当当前协程释放CPU,另一个协程才去执行;现在调度器以时间片而言,一个时间片到了后会强制释放CPU给其他协程使用。
当线程空闲时,会首先从其他线程对应协程本地队列偷取(即work stealing机制),如果偷不到,会从全局G队列进行获取(前提要先去获取锁)。
开始:
go func()
会创建一个协程G
;G
优先会被调度到创建G
线程对应的本地队列,如果本地队列已满,则G
会被加入到全局队列;M
会通过协程调度器P
获取协程G
执行。执行go func()
之前,如果本地队列为空,优先会从其他线程对应的本地队列偷取G
执行,即是work sealing机制
;若其他线程对应的本地队列为空,则会从全局队列获取G
进行执行;go func()
对应的执行代码;即上图中(4-调度,5-执行,6-时间片返回,时间片到了会重新加入到本地队列)如执行go func()
代码产生阻塞现象:
为了节省资源,提高CPU利用率
5.系统首先会从本地休眠线程队列中唤醒一个线程M
来接管当前正阻塞的线程对应的P
和本地G
队列;如果本地休眠线程队列没有,则会新创建一个线程M
,即hand off 机制
;阻塞协程G
会和当前线程M
进行绑定;
6.当阻塞完成后,线程M
会被加入到休眠线程队列或者被销毁掉,而G
则会被加入到其他本地队列,如果本地队列都满了,则会被加入到全局队列;
`
地址:https://www.bilibili.com/video/BV19r4y1w7Nx?p=7&spm_id_from=pageDriver