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描述符结束阻塞准备就绪时,就将其返回。此处不再赘述,有需要的读者可以访问 python实现IO多路复用 了解select/poll/epoll的用法。

1.2 协程的引入

        事件驱动编程模型有诸多好处,但在嵌套多层回调时,可读性较差,出现异常排查也很困难,非常不利于后期的维护。于是,我们引入协程来解决上面的问题,允许我们采用同步的方式去编写异步的代码,使代码的可读性提升,既操作简单,速度又快。协程使用单线程去切换任务,性能远高于线程切换,且不需要加锁,并发性高。

       进程、线程以及协程的关系可以使用下图描述: 

        进程可以包含多个线程,多个线程共享进程的资源,因此线程比进程更轻量;而协程的本质是一个函数,一个线程可以包含多个协程,协程比线程更轻量。

1.3 相关概念

       下面的概念在 python处理并发导读与目录 中已详细区分,此处只作为回顾,简单概述。

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

发布了132 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Geroge_lmx/article/details/105167645