1. 由 yield 函数 引来协程
我们先看一段python代码
def count():
c = 1
for i in range(5):
print(c)
c += 1
count()
print("###")
执行结果:
先执行 count 函数 打印出 1-5, 最后再打印 ###
而这时我们在 count 函数内部加入 一个 yield 关键字
def count():
c = 1
for i in range(5):
print(c)
yield c
c += 1
count()
print("###")
再次执行:
发现 count() 执行不了了。这是因为在Python中含有 yield关键字 的函数是一种特殊函数,称为 生成器函数,它调用返回将不再是执行到函数return,而是返回一个生成器对象(迭代器对象)。
生成器对象
- 可以使用next函数驱动它执行,但执行到yield就暂停函数执行
- 也可以使用for循环迭代它,相当于连续的next,直到不可迭代为止
我们试着去使用它吧:
def count():
c = 1
for i in range(5):
print(c)
yield c
c += 1
t = count() # 迭代器对象
next(t)
print("###")
结果如下:
输出结果为1,说明函数在第5行处暂停执行了(实际上这个函数没有执行完),且能继续向下执行到11 行,打印了3个#。
那如果有两个迭代器呢
import string
def count():
c = 1
for i in range(5):
print(c)
yield c
c += 1
def chars():
s = string.ascii_lowercase
for c in s:
print(c)
yield c
t1 = count() # 迭代器对象
t2 = chars()
next(t1)
next(t2)
next(t1)
next(t2)
print("###")
执行结果:
可以通过上面的代码看到2个任务交替进行,而这个函数的交替,完全是靠程序员的代码实现的,而不是 靠多线程的时间片用完操作系统强行切换,而且这种切换是在同一个线程中完成的。
交替执行任务是可以由程序员在一个线程内完成,这个任务被封装后就是协程。关键点是,在适当 的时候要暂停一个正在运行的任务,让出来执行另外一个任务。
2.GMP 模型
2.1 三大将
Go协程调度中,有三个重要角色:
- M:Machine,内核线程
- 所有代码都要在系统线程上跑,协程最终也是代码,也不例外
- G:Goroutine,Go协程。存储了协程的执行栈信息、状态和任务函数等。初始栈大小约为2~4k, 理论上开启百万个Goroutine不是问题
- P:Processor,虚拟处理器
- 代表 M 所需要的上下文环境,是处理用户级代码逻辑的处理器
- 可以通过环境变量
GOMAXPROCS或runtime.GOMAXPROCS()设置,默认为CPU核心数 - P的数量决定着最大可并行的G的数量 P有自己的队列(长度256),里面放着待执行的G
- M和P需要绑定在一起,这样P队列中的G才能正在有地方执行
2.2 调度模型
- 使用
go func创建一个Goroutineg1 - 当前
P为p1,将 g1 放入当前 P 的本地队列,如果满了,就加入到 全局队列 - p1 和 m1 绑定,m1 从 p1 的 本地队列 中请求
G,如果没有,就从 全局队列 中请求G,如果还没有,就从别的P的 本地队列中偷 - 假设 m1 拿到了 g1
- 让 g1 的代码在 m1 线程上运行,如果遇到了同步阻塞调用:
- m1 和 p1 分离, m1 带着 g1 阻塞着。
- 查看休眠线程队列有无空闲线程
- 有:和 p1 绑定,并从 p1 队列中获取
G来执行 - 无:就创建一个线程提供给 p1 。
- 有:和 p1 绑定,并从 p1 队列中获取
- 如果 m1 阻塞结束,需要和一个空闲的
P绑定- 优先和原来的 p1 绑定,其次是 空闲的
P - 如果没有空闲的
P,g1 会放到 全局队列 中,m1 加入到休眠线程队列中。
- 优先和原来的 p1 绑定,其次是 空闲的
- 如果是异步阻塞调用:
- g1绑定到network poller上,等到调用结束,回到P上,而M不被阻塞,可继续执行
2.3### 为什么要有P
GM直接绑定不就好了。
- M没有本地队列
- 创建销毁调度G都需要和全局G队列打交道,使用全局锁。
- 要求每个M都能执行新创建的G。创建的子协程最好和父协程在同一个M上执行,但M为了执行父协程只能把子协程交给全局队列让其他M执行。
- 频繁的系统调用会使线程经常被阻塞和解阻塞
有了P后加入本地队列,减轻对全局队列的直接依赖,p还能从全局队列或其他P的本地队列拿取G,保证每个P的平衡
2.4为什么不直接在M上加本地队列和偷取算法?
- M的数量一般比P(默认CPU核心数)多,而只要系统调用阻塞M导致不够用就会一直增加M,这样本地队列的管理和偷取算法的性能会大规模下降。
- P和G都在用户空间,而M是内核线程,上下文交由P管理比M强