我们在学习Python时候,协程(Coroutine)可能是最让初学者困惑的知识点之一了,它也是Python中实现并发编程的一种重要方式。Python中可以使用多线程和多进程来实现并发,对于计算型任务由于GIL的存在我们通常使用多进程来实现,而对于IO型任务我们可以通过线程调度来让线程在执行IO任务时让出GIL,从而实现表面上的并发。其实对于IO型任务我们还有一种选择就是协程,协程是运行在单线程当中的"并发",协程相比多线程一大优势就是省去了多线程之间的切换开销,获得了更大的运行效率(PS:协程是实现异步编程的一种方式)。
协程简单理解就是为多个相互协作的子程序。在同一个线程中,当一个子程序阻塞时,我们可以让程序马上从一个子程序切换到另一个子程序,从而避免CPU因程序阻塞而闲置,这样就可以提升CPU的利用率,相当于用一种协作的方式加速了程序的执行。所以说:协程实现了协作式并发。实现了协作式多任务,可以在程序执行内部中断,转而执行其他协程。
协程与线程的区别
协程效率比线程高。线程间切换需要开销,而协程间切换是由程序自身控制的,不需要开销。
协程不需要多线程的锁机制。协程是在一个线程内进行切换,所以不存在同时写变量冲突,不需要给共享资源加锁,只需要判断状态。
PS:如果是多CPU的话,可以使用进程+协程方式实现并发。
协程又称为微线程,协程是一种用户态的轻量级线程。
协程拥有自己的寄存器和栈。协程调度切换的时候,将寄存器上下文和栈都保存到其他地方,在切换回来的时候,恢复到先前保存的寄存器上下文和栈,因此:协程能保留上一次调用状态,每次过程重入时,就相当于进入上一次调用的状态。
协程优点:
无需线程上下文切换的开销(还是单线程)
无需原子操作(一个线程改一个变量,改一个变量的过程就可以称为原子操作)的锁定和同步的开销
方便切换控制流,简化编程模型
高并发+高扩展+低成本:一个CPU支持上万的协程,适合用于高并发处理
高效处理IO密集型程序
协程缺点:
无法利用多核的资源,协程本身是单线程,不能同时将单个CPU的多核用上,协程需要和进程配合才能运用到多CPU上(协程是跑在线程上的)
不擅长处理CPU密集型程序
Python协程发展历程:
Python2.x对协程的支持比较有限,生成器yield+send实现了一部分但不完全,gevent模块倒是有比较好的实现。
Python3.4引入标准库 asynico,提供 @asyncio.coroutine 和 yield from,还是以生成器对象为基础。
Python3.5添加了 types.coroutine 装饰器以及 async/await 关键字,确定了协程的语法。
Python3.6将asyncio更加完善和稳定。
PS:除 asyncio 外,tornado 和 gevent 都实现了异步编程的功能。
案例1:
#!/usr/bin/env python
#-*- coding: utf-8 -*-
import time
def display(num):
time.sleep(1)
print(num)
for num in range(10):
display(num)
程序会每隔1秒中输出一个数字(输出0到9的数字),因此整个程序的执行需要大约10秒时间。值得注意的是,因为没有使用多线程或多进程,程序中只有一个执行单元,而time.sleep(1)的休眠操作会让整个线程停滞1秒钟,对于上面的代码来说,在这段时间里面CPU是完全闲置的没有做什么事情。
从Python3.5开始,使用协程实现协作式编发有了更为便捷的语法,我们可以使用async来定义异步函数,可以使用await让一个阻塞的子程序将CPU让给与它协作的子程序。在Python3.7中,asyanc和await成为了正式的关键字。
案例2:
import asyncio
async def display(num):
await asyncio.sleep(1)
print(num)
# 生成10个协程对象
coroutines = [display(num) for num in range(10)]
# 获取事件循环并将协程对象放入事件循环中
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(coroutines))
loop.close()
异步函数不同于普通函数,调用普通函数会得到返回值,而调用异步函数会得到一个协程对象。我们需要将协程对象放到一个事件循环中才能达到与其他协程对象协作的效果,因为事件循环会负责处理子程序切换的操作,简单的说就是让阻塞的子程序让出CPU给可以执行的子程序。
只阻塞了约1秒种的时间,说明协程对象一旦阻塞会将CPU让出而不是让CPU处于闲置状态,这样大大的提升了CPU的利用率。
另外0到9的数字并不是顺序打印出来的,这正是并发程序本身执行顺序不确定性造成的结果。
PS:在项目中如果需要使用协作式并发,还可以将系统默认的事件循环替换为uvloop提供的事件循环,这样会获得更好的性能,因为uvloop是基于著名的跨平台异步I/O库libuv实现的。另外如果要做基于HTTP的网络编程,三方库aiohttp也是个不错的选择,它基于asyncio实现了异步的HTTP服务器和客户端。
参考:
《Python高级并发编程》