什么是协程
协程(co-routine,又称微线程、纤程)是一种多方协同的工作方式。协程不是进程或线程,其执行过程类似于 Python 函数调用,Python 的 asyncio 模块实现的异步IO编程框架中,协程是对使用 async 关键字定义的异步函数的调用。当前执行者在某个时刻主动让出(yield)控制流,并记住自身当前的状态,以便在控制流返回时能从上次让出的位置恢复(resume)执行。
一个进程包含多个线程,类似于一个人体组织有多种细胞在工作,同样,一个程序可以包含多个协程。多个线程相对独立,线程的切换受系统控制。
同样,多个协程也相对独立,但是其切换由程序自己控制。简而言之,协程的核心思想就在于执行者对控制流的 “主动让出” 和 “恢复”。相对于,线程此类的 “抢占式调度” 而言,协程是一种 “协作式调度” 方式,协程之间执行任务按照一定顺序交替执行。
Python 对协程的支持经历了多个版本:
Python2.x 对协程的支持比较有限,通过 yield 关键字支持的生成器实现了一部分协程的功能但不完全。
第三方库 gevent 对协程有更好的支持。
Python3.4 中提供了 asyncio 模块。
Python3.5 中引入了 async/await 关键字。
Python3.6 中 asyncio 模块更加完善和稳定。
Python3.7 中内置了 async/await 关键字。
gevent 是对greenlet进行的封装,而greenlet 又是对yield进行封装。
一、协程实现方法:
1、greenlet,早期模块
greenlet包是一个Stackless(无栈化的)CPython版本,支持微线程(tasklet)。tasklet可以伪并行的运行并且同步的在信道上交换数据。
①首先要先安装greenlet模块
pip install greenlet
"""* @Author: xiaofang
* @software: PyCharm
* @Description:
"""
from greenlet import greenlet
def func1():
print(1) # 第1步 输出1
# 该方法遇到阻塞可以切换到函数2中进行使用
gr2.switch() # 第2步:切换到func2中 并执行
print(2) # 第五步 输出2
gr2.switch() # 第六步 切换 func2
def func2():
print(3) # 第三步:输出3
gr1.switch() # 第四步:切换回func1 并执行
print(4) # 第七步:输出4
gr1 = greenlet(func1)
gr2 = greenlet(func2)
gr1.switch() # 第0步,切换func1并执行
运行结果:
2、yield关键字(Python2.x开始)
"""* @Author: xiaofang
* @software: PyCharm
* @Description:
"""
def func1():
yield 1
yield from func2()
yield 2
def func2():
yield 3
yield 4
f1 = func1()
for item in f1:
print(item)
运行结果:
这里可以思考对比一下yield和return
3、asyncio装饰器(Python 3.4开始)
"""* @Author: xiaofang
* @software: PyCharm
* @Description:
"""
# asyncio(在python3.4之后的版本)
# 遇到IO等耗时操作会自动切换
import asyncio
import time
@asyncio.coroutine
def func1():
print(1)
yield from asyncio.sleep(3) # 遇到耗时后会自动切换到其他函数中执行
print(2)
@asyncio.coroutine
def func2():
print(3)
yield from asyncio.sleep(2)
print(4)
@asyncio.coroutine
def func3():
print(5)
yield from asyncio.sleep(2)
print(6)
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2()),
asyncio.ensure_future(func3())
]
# 协程函数使用 func1()这种方式是执行不了的
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
# loop.run_until_complete(func1()) 执行一个函数
end = time.time()
print(end - start) # 只会等待3秒
运行结果:
4、async、await关键字(Python 3.5开始)
"""* @Author: xiaofang
* @software: PyCharm
* @Description:
"""
import asyncio
import time
async def func1():
print(1)
await asyncio.sleep(3) # 遇到耗时后会自动切换到其他函数中执行
print(2)
async def func2():
print(3)
await asyncio.sleep(2)
print(4)
async def func3():
print(5)
await asyncio.sleep(2)
print(6)
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2()),
asyncio.ensure_future(func3())
]
# 协程函数使用 func1()这种方式是执行不了的
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
# loop.run_until_complete(func1()) 执行一个函数
end = time.time()
print(end - start) # 只会等待3秒
运行结果:
5、gevent
"""* @Author: xiaofang
* @software: PyCharm
* @Description:
"""
import gevent
def f1():
for i in range(1, 6):
print('f1', i)
gevent.sleep(0)
def f2():
for i in range(6, 11):
print('f2', i)
gevent.sleep(0)
t1 = gevent.spawn(f1)
t2 = gevent.spawn(f2)
gevent.joinall([t1, t2])
运行结果:
gevent的优势不仅仅是在代码中调用方便,厉害的是它拥有的monkey机制。假设你不愿意修改原来已经写好的python代码,但是又想充分利用gevent机制,那么你就可以用monkey来做到这一点。
你所要做的就是在文件开头打一个patch,那么它就会自动替换你原来的thread、socket、time、multiprocessing等代码,全部变成gevent框架。这一切都是由gevent自动完成的。注意这个patch是在所有module都import了之后再打,否则没有效果。
甚至在编写的Web App代码的时候,不需要引入gevent的包,也不需要改任何代码,仅仅在部署的时候,用一个支持gevent的WSGI服务器,就可以获得数倍的性能提升。
二、协程的运行原理
当程序运行时,操作系统会为每个程序分配一块同等大小的虚拟内存空间,并将程序的代码和所有静态数据加载到其中。然后,创建和初始化 Stack 存储,用于储存程序的局部变量,函数参数和返回地址;创建和初始化 Heap 内存;创建和初始化 I/O 相关的任务。当前期准备工作完成后,操作系统将 CPU 的控制权移交给新创建的进程,进程开始运行。
一个进程可以有一个或多个线程,同一进程中的多个线程将共享该进程中的全部系统资源,如:虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈和线程本地存储。
协程是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由用户态程序所控制。协程与线程以及进程的关系如下图所示。可见,协程自身无法利用多核,需要配合进程来使用才可以在多核平台上发挥作用。
协程之间的切换不需要涉及任何 System Call(系统调用)或任何阻塞调用。
协程只在一个线程中执行,切换由用户态控制,而线程的阻塞状态是由操作系统内核来完成的,因此协程相比线程节省线程创建和切换的开销。
协程中不存在同时写变量的冲突,因此,也就不需要用来守卫关键区块的同步性原语,比如:互斥锁、信号量等,并且不需要来自操作系统的支持。
三、协程应用场景
1、抢占式调度的缺点
在 I/O 密集型场景中,抢占式调度的解决方案是 “异步 + 回调” 机制。
其存在的问题是,在某些场景中会使得整个程序的可读性非常差。以图片下载为例,图片服务中台提供了异步接口,发起者请求之后立即返回,图片服务此时给了发起者一个唯一标识ID,等图片服务完成下载后把结果放到一个消息队列,此时需要发起者不断消费这个 MQ 才能拿到下载是否完成的结果。
可见,整体的逻辑被拆分为了好几个部分,各个子部分都会存在状态的迁移,日后必然是 BUG 的高发地。
2、用户态协同调度的优势
而随着网络技术的发展和高并发要求,协程所能够提供的用户态协同调度机制的优势,在网络操作、文件操作、数据库操作、消息队列操作等重 I/O 操作场景中逐渐被挖掘。
四、协程使用注意事项
协程只有和异步IO结合起来才能发挥出最大的威力
假设协程运行在线程之上,并且协程调用了一个阻塞IO操作,这时候会发生什么?实际上操作系统并不知道协程的存在,它只知道线程,因此在协程调用阻塞IO操作的时候,操作系统会让线程进入阻塞状态,当前的协程和其它绑定在该线程之上的协程都会陷入阻塞而得不到调度。
因此,在协程中尽量不要调用阻塞IO的方法,比如打印,读取文件,Socket接口等,除非改为异步调用的方式,并且协程只有在IO密集型的任务中才会发挥作用。