Python 实现协程(四)

我们本节即将学习的 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 frompython3.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 会自动将协程对象包装成任务来运行。下节我们会讲到多个任务注入事件循环的情况。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容