这里将简单阐述多线程的故事,协程的故事,和 go goroutine 的故事
(1)原始的单线程操作系统
同时只能执行一个线程,简单的单线程
问题:
- 单一执行,计算机智能一个一个处理
- 进程阻塞浪费:如果一个进程阻塞了,整个系统就阻塞了
- CPU浪费:CPU 只是在计算的时候会用到,比如程序的 IO ,CPU 利用率低下,大部分时间都是空闲的
- 现在几乎没有这种单线程的模式了
(2) 常见多线程操作系统
单线程的问题,我们用多线程的解决方案来解决
操作系统的多线程将线程用时间来切片(比如每个线程执行 5ms 然后切换到下一个线程,然后重宏观角度上来看,就是很多应用并发执行的)
解决了 阻塞和CPU利用率低下的问题
但是
产生问题:
- 线程切换成本:切换需要CPU调用系统调用,资源的拷贝复制,都会浪费很多时间,而且线程越多,越浪费时间
- 内存消耗:进程占用的内存(32位)最多是 4G,线程大约占用 4M 左右,线程越多,越是浪费内存
- 开发困难:多线程就必须要解决变量统一问题(锁)和资源竞争问题,开发变得越来越困难
(3)go 协程模型 GMP 模型
解释 GMP 模型
- 蓝色的 M 都是内核线程,线程是操作系统调度的最小单位,协程是 P 调度器调度的
- 黄色的 P 就是 Processor 调度器,用来调度协程给线程运行
- 绿色的 G 是 go协程 goroutine,负责运行具体的协程代码,就是你写的代码就在这上面运行
- P的本地队列,是 P 调度器负责调度的协程存放的队列,对列里面的协程是抢占式执行(比如一个协程执行 5ms 就换到下一个协程)
- 全局队列是「等待队列」:当所有的 P 本地协程队列都满了之后,放不下的都放到全局队列里面,全局队列是加锁的
多加一层 P:
多加一层的概念在计算机里面十分常见,比如 JDBC 就是为了统一数据库连接,比如 MVC 三层架构…多加一层是十分常见的
这里面就是用了多加一层的手法多加了一层 P 调度器
轻便
goroutine 只有 几KB 是十分轻便的
不用大量切换线程浪费资源
协程本地队列,中协程的切换而不是线程的切换,让操作系统的线程最大程度的处于计算状态
(4)go 调度器的 4 种策略,解决阻塞并让线程最大程度的计算
1.复用线程
复用线程有 2 个机制
- 1.1 work stealing 偷取:如果一个 P 上的 G协程 队列为空,他会优先偷取其他 P 上的 G 协程队列,其他都为空,才会去全局队列中去拿
- 1.2 hand off 切换:如果有一个协程阻塞了,并且所有的其他队列都在有,没有必要去偷取
1.1 work stealing 偷取机制
1.2 hand off 切换机制
如果正在运行的协程 G1 阻塞了,但是其他的调度器都在处理自己的业务,没有时间去偷 work stealing 这阻塞的 G1 上面队列中的其他协程
我们就:唤醒一个线程,或者开启一个线程,将我们之前的在调度器和其他没有阻塞的协程切换过去
G 阻塞接受后,如果还要运行,就放到其他调度器 P 上面的协程队列中,如果不执行,就将 M1 线程休眠或销毁即可
2. 利用并行
利用 GOMAXPROCES 最大 P的数量限定,P的个数比如 CPU数/2
抢占
M -> P ->Gs队列,G队列中是一个抢占策略,如果超过时间还没有执行完,就切换其他线程执行,就像是 多线程操作系统的模型
全局队列
- 将 P 中的本地队列放满了,才会放到全局队列,
- 只有 steal 偷取策略偷不到了,才回去,全局队列去拿,全局队列是加锁的
参考视频教程
https://www.bilibili.com/video/BV1gf4y1r79E?p=26