Python-100-Days/番外篇/一个小例子助你彻底理解协程.md

3.9 KiB
Raw Blame History

一个小例子助你彻底理解协程

协程可能是Python中最让初学者困惑的知识点之一它也是Python中实现并发编程的一种重要方式。Python中可以使用多线程和多进程来实现并发这两种方式相对来说是大家比较熟悉的。事实上还有一种实现并发的方式叫做异步编程而协程就是实现异步编程的必要方式。

所谓协程可以简单的理解为多个相互协作的子程序。在同一个线程中当一个子程序阻塞时我们可以让程序马上从一个子程序切换到另一个子程序从而避免CPU因程序阻塞而闲置这样就可以提升CPU的利用率相当于用一种协作的方式加速了程序的执行。所以我们可以言简意赅的说协程实现了协作式并发

接下来用一个小例子帮助大家理解什么是协作式并发,先看看下面的代码。

import time


def display(num):
    time.sleep(1)
    print(num)


for num in range(10):
    display(num)

上面这段代码相信大家很容看懂程序会输出0到9的数字每隔1秒中输出一个数字因此整个程序的执行需要大约10秒时间。值得注意的是因为没有使用多线程或多进程程序中只有一个执行单元time.sleep(1)的休眠操作会让整个线程停滞1秒钟对于上面的代码来说在这段时间里面CPU是完全闲置的没有做什么事情。

我们再来看看使用协程会发生什么事情。从Python 3.5开始,使用协程实现协作式编发有了更为便捷的语法,我们可以使用async来定义异步函数,可以使用await让一个阻塞的子程序将CPU让给与它协作的子程序。在Python 3.7中,asyancawait成为了正式的关键字,让开发者有一种喜大普奔的感觉。我们先看看如何定义一个异步函数。

import asyncio


async def display(num):
    await asyncio.sleep(1)
    print(num)

接下来敲黑板说重点。异步函数不同于普通函数调用普通函数会得到返回值而调用异步函数会得到一个协程对象。我们需要将协程对象放到一个事件循环中才能达到与其他协程对象协作的效果因为事件循环会负责处理子程序切换的操作简单的说就是让阻塞的子程序让出CPU给可以执行的子程序。

我们先通过下面的列表生成式来代码10个协程对象跟刚才在循环中调用display函数的道理一致。

coroutines = [display(num) for num in range(10)]

通过下面的代码可以获取事件循环并将协程对象放入事件循环中。

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(coroutines))
loop.close()

执行上面的代码会发现10个分别会阻塞1秒钟的协程总共只阻塞了约1秒种的时间这就说明协程对象一旦阻塞会将CPU让出而不是让CPU处于闲置状态,这样就大大的提升了CPU的利用率。而且我们还会注意到0到9的数字并不是按照我们创建协程对象的顺序打印出来的这正是我们想要的结果啊另外多次执行该程序会发现每次输出的结果都不太一样这正是并发程序本身执行顺序不确定性造成的结果。

上面的例子来自于著名的“花书”《Python高级并发编程》为了让大家对协程的体会更加深刻对原书的代码做了小的改动这个例子虽然简单但是它已经让你体会到了协作式并发的魅力。在商业项目中如果需要使用协作式并发还可以将系统默认的事件循环替换为uvloop提供的事件循环,这样会获得更好的性能,因为uvloop是基于著名的跨平台异步I/O库libuv实现的。另外如果要做基于HTTP的网络编程三方库aiohttp是不错的选择它基于asyncio实现了异步的HTTP服务器和客户端。