我们本节即将学习的 Python asyncio 包,使用基于事件循环驱动的协程实现并发。这是 Python 中最大,也是最具雄心壮志的库之一。
既然 asyncio 基于事件驱动,那么让我们首先来了解下事件驱动编程,再进入正题。
一. 事件驱动
1.1 单线程、多进程以及事件驱动编程模型的比较
事件驱动编程是一种编程范式,程序的执行流程由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时,使用一种回调机制来触发相应的处理。
此外,单线程同步以及多进程也是常见的编程范式。下图对比了单线程、多线程以及事件驱动编程模型。
上图中,这个程序有 A / B / C
个任务需要完成,每个任务在执行过程中都存在 IO 阻塞,阻塞的时间使用黑色块表示。
单线程同步模型:多个任务按序执行。一旦某个任务因为 I/O 而阻塞,其他所有的任务都必须等待,直到前面的任务完成之后它们才能依次执行。即使任务之间并没有互相依赖,仍然需要等待,使得程序不必要的降低了运行速度。
多进程同步模型中:各个任务分别在独立的进程中执行。进程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。与单线程同步程序相比,多进程的效率更高,但同时创建进程的资源消耗也比较大。
多线程操作共享资源时,还需要考虑同步互斥机制,而且 CPython 解释器无法利用计算机多核的特性。
事件驱动编程模型中:多个任务在一个单独的线程中交错执行。当遇到 I/O 操作时,注册一个回调到事件循环中,然后当 I/O 操作完成时继续执行。
事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。因此,一个任务在遇到 IO 阻塞时,可以让步出 CPU 的使用权,让其它任务继续执行,而不是一直等待。事件驱动编程模型不需要关心线程安全问题。
我们之前介绍的 IO 多路复用,使用的就是事件驱动编程模型,利用 select / poll / epoll
将 IO 事件交给系统内核监控,当某个 IO
描述符结束阻塞准备就绪时,就将其返回。
1.2 协程的引入
事件驱动编程模型有诸多好处,但在嵌套多层回调时,可读性较差,出现异常排查也很困难,非常不利于后期的维护。
于是,我们引入协程来解决上面的问题,允许我们 采用同步的方式去编写异步的代码,使代码的可读性提升,既操作简单,速度又快。
协程使用单线程去切换任务,性能远高于线程切换,且不需要加锁,并发性高。
进程、线程以及协程的关系可以使用下图描述:
进程可以包含多个线程,多个线程共享进程的资源,因此线程比进程更轻量;而协程的本质是一个函数,一个线程可以包含多个协程,协程比线程更轻量。
1.3 相关概念
- 并发:CPU 在多个任务之间不断切换,比如在一秒内 CPU 切换了 100 个进程,就可以认为 CPU 的并发是 100。
- 并行:在多核 CPU 中,多个任务在不同的 CPU 上同时运行;并行数量和 CPU 数量是一致的。
- 同步:必须等待前一个调用完成后,再开始新的的调用。
- 异步:不必等待前一个操作的完成,就开始新的的调用。
- 阻塞:调用函数的时候,当前线程被挂起。
- 非阻塞:调用函数的时候,当前线程不会被挂起,而是立即返回结果(不管什么样的结果)。
二. asyncio 模块
Python3.4 中引入 asyncio 模块,创建协程函数时使用@asyncio.coroutine
装饰器装饰。
我们前面介绍的
yield from
是python3.4
前的用法,即包含yield from
语句的函数即可作为生成器函数,也可以称作协程函数。
Python3.4 之后,使用 @asyncio.coroutine
装饰的函数即可称作协程函数。关于 asyncio
中的基本概念总结如下:
术语 | 说明 |
---|---|
coroutine 协程对象 |
使用 @asyncio.coroutine 装饰器装饰的函数被称作协程函数,它的调用不会立即执行,而是返回一个协程对象。协程对象需要包装成任务注入到事件循环,由事件循环调用。 |
task 任务 |
使用协程对象作为参数创建任务,任务是协程对象的进一步封装,其包含任务的各种状态 |
event_loop 事件循环 |
协程函数必须添加到事件循环中,由事件循环去运行,因为直接调用协程函数返回的是协程对象,协程函数并不会真正开始运行。事件循环控制任务运行流程,是任务的调用方。 |
示例 asyncio 实现协程的简单示例
import time
import asyncio
@asyncio.coroutine
def do_some_work():
print('Coroutine Start.')
time.sleep(3) # 模拟IO操作
print('Print in coroutine.')
def main():
start = time.time()
loop = asyncio.get_event_loop()
coroutine = do_some_work()
loop.run_until_complete(coroutine)
end = time.time()
print('运行耗时:{:.2f}'.format(end - start)) # 打印程序运行耗时
if __name__ == '__main__':
main()
运行结果:首先使用协程装饰器 @asyncio.coroutine
创建协程函数,协程函数中使用 time.sleep(3)
模拟一个耗时的IO操作。
asyncio.get_event_loop()
用来创建事件循环;每个线程中只能有一个事件循环,get_event_loop
获取当前已经存在的事件循环,如果当前线程中没有,则新建一个事件循环。
loop.run_until_complete(coroutine)
将协程对象注入到事件循环,协程的运行由事件循环控制。事件循环的 run_until_complete
方法会阻塞运行,直到任务全部完成。
协程对象作为 run_until_complete
方法的参数,loop
会自动将协程对象包装成任务来运行。下节我们会讲到多个任务注入事件循环的情况。