协程
协程 Coroutine
一、协程概念:
1.1 为什么要有协程?
- 同步编程 的并发性不高
- 多进程编程 效率受 CPU 核数限制,当任务数量远大于 CPU 核数时,执行效率会降低(分片)
- 多线程编程 需要线程之间的通信,而且需要锁机制来防止共享变量被不同线程乱改,有GIL(全局解释器锁) 实际上也无法做到真正的并行
- 可不可以采用同步的方式来编写异步功能代码?
- 能不能只用单线程就能做到不同任务间的切换?
- 这样就没有了线程切换的时间消耗
- 也不用使用锁机制来削弱多任务并发效率
- 对于IO 密集型任务,可否有更高的处理方式来节省 CPU 等待时间?
产生协程,多进程和多线程是内核级别的程序,而协程是函数级别的程序,可以通过程序员自己控制,因此:
- 仅定义一个单线程的
loop
,所有event
均在一个loop
中 - 是否需要锁机制:否
- 实现机制:事件循环 + 协程
- 总耗时:最耗时 事件的时间
- 应用场景:**IO 密集型 **任务
1.2 什么是协程
协程,又称微线程
|纤程
。英文:Coroutine 可揉挺
一句话说明:协程是一种 用户态
的 轻量级
线程。通过一个线程
,实现代码块相互切换执行,实现麻烦但效率极佳。
用户态:程序员自己控制什么时候切换。利用串行,便不存在
锁
了(也不会有数据不安全
的情况),因此协程好用的多。轻量级:实质上不是利用 CPU 轮询执行,而是用一个线程进行切换,无需 CPU 控制(因此协程说白了是一个单线程)
1.3 协程的优缺点
协程の好处
无需像
线程
一样需要上下文切换的开销例如:执行 100 个字节码,若是计算密集型任务,无 IO 阻塞则会进行大量的切换,CPU 资源大幅消耗
无需有原子操作的
锁定
及同步
的开销是不需要
synchronized(同步)
的是不会被
线程调度
机制打断操作的这种操作一旦开始,就一直运行到结束,中间不会有任何
context switch
(上下文切换:切换到另一个线程)原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不能被打乱,不能被切割(只执行部分)一系列操作是一个整体
- 方便切换控制流,简化编程模型
高并发 + 高扩展性 + 低成本:
一个 CPU 支持上万的协程都不是问题(理论无限大)。所以很适合用于高并发处理。(因此利用的好完全可以替代线程,效率奇高)
协程の缺点
无法利用多核资源:
协程的本质是个
单线程
,它不能同时将 CPU 的多核利用,协程需要和进程
配合才能运行在多 CPU 上- 当然我们日常所编写的绝大部分应用都没有这个必要,除非是 cpu 密集型应用。
线程阻塞 操作
IO Blocking
时会阻塞掉整个程序因此一处协程、处处协程,使用了
async/await
便所有的 IO 函数都应该使用async/await
,不然该程序的同步 IO 部分就会全局阻塞,影响性能
二、协程进化史:
实现前先给协程一个标准定义
,即符合什么条件就能称之为协程
:
1. 必须在只有一个`单线程`里实现并发
2. 修改共享数据不需加锁
3. 用户程序里自己保存多个控制流的上下文栈
4. 一个协程遇到 IO 操作自动切换到其它协程
实现方式有以下几种:
- 生成器
yield
关键字greenlet/gevent
早期模块yield from
实现asyncio
装饰器py3.4async|await
关键字py3.5[主流实现]
2.1 yield 生成器实现
因为 yield
可以实现中断功能,所以起初,协程是用生成器来实现的,此时不是 线程级CPU
的切换,而是 执行顺序
的切换,但原理依旧
def func1():
yield 1
yield from func2() # 切换到func2 执行,并保留上下文
yield 2
def func2():
yield 3
yield 4
f1 = func1()
for item in f1: # 返回了生成器
print(item)
### 输出结果:
# 1
# 3
# 4
# 2
2.2 greenlet 实现协程
是一个用 c
实现的 协程模块
,相比与python
自带的yield
,它能在任意函数之间随意切换,而不需把这个函数先声明为 generator|生成器
安装 greenlet
pip install greenlet
from greenlet import greenlet
def func1():
print(1) # 第2步:打印1
gr2.switch() # 第3步:切换到 func2 函数
print(2) # 第6步:打印2
gr2.switch() # 第7步:切换到 func2 函数,从上一次位置继续
def func2():
print(3) # 第4步:打印3
gr1.switch() # 第5步:切换到 func1 函数,从上一次位置继续
print(4) # 第8步:打印4
gr1 = greenlet(func1)
gr2 = greenlet(func2)
gr1.switch() # 第1步:执行 func1 函数
## 输出结果:
# 1
# 3
# 2
# 4
与生成器相同,此时未实现遇见 IO 阻塞 自动切换,这在 gevent 中实现了sleep
自动切换
2.3 yield from 和装饰器实现
yield from 和 @asyncio.coroutine 是官方python 3.4之后专门提供用于实现异步I/O
的模块
import asyncio
@asyncio.coroutine # 装饰一下,变为协程函数
def func1():
print(1)
yield from asyncio.sleep(1) # 当遇到IO操作时,会自动化切换到tasks中的其他任务执行
print(2)
@asyncio.coroutine
def func2():
print(3)
yield from asyncio.sleep(1) # 当遇到IO操作时,会自动化切换到tasks中的其他任务执行
print(4)
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2())
] # 打包任务
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
### 输出结果
# 1
# 3
# 2
# 4
以上为基于生成器的协程,已弃用 并计划在 Python 3.10 中移除。
@asyncio.coroutine
装饰器是协程函数的标志,等同下文async
yield from
等同下文await
2.4 async/await 实现
把上文的 装饰器
替换为 async
, yield from
替换为 await
即可
async def func1():
print(1)
await asyncio.sleep(1)
print(2)
async def func2():
print(3)
await asyncio.sleep(1)
print(4)
具体见异步 I/O