深入理解asyncio(一)

深入理解asyncio(一)

前言

这几天看asyncio相关的pycon视频又重温了asyncio 的官方文档,收获很多。之前asyncio被吐槽的一点就是文档写的不好,Python 3.7 时 asyncio 的官方文档被 Andrew Svetlov 以及 Yury Selivanov 等核心开发者重写了,新的版本我觉得已经好很多了。借着这篇笔记记录一下我对asyncio的一些理解。

核心概念

asyncio里面主要有4个需要关注的基本概念

Eventloop

Eventloop可以说是asyncio应用的核心,是中央总控。Eventloop实例提供了注册、取消和执行任务和回调的方法。

把一些异步函数(就是任务,Task,一会就会说到)注册到这个事件循环上,事件循环会循环执行这些函数(但同时只能执行一个),当执行到某个函数时,如果它正在等待I/O返回,事件循环会暂停它的执行去执行其他的函数;当某个函数完成I/O后会恢复,下次循环到它的时候继续执行。因此,这些异步函数可以协同(Cooperative)运行:这就是事件循环的目标。

Coroutine

协程(Coroutine)本质上是一个函数,特点是在代码块中可以将执行权交给其他协程:

import asyncio
import nest_asyncio
nest_asyncio.apply()

async def a():
    print('Suspending a')
    await asyncio.sleep(0)
    print("Resuming a")

async def b():
    print("in b")

async def main():
    await asyncio.gather(a(),b())

if __name__ == '__main__':
    asyncio.run(main())

这里面有4个重要关键点:

  1. 协程要用 asyncdef声明,Python 3.5时的装饰器写法已经过时,我就不列出来了。
  2. asyncio.gather用来并发运行任务,在这里表示协同的执行a和b2个协程。
  3. 在协程a中,有一句awaitasyncio.sleep(0),await表示调用协程,sleep(0)并不会真的sleep(因为时间为0),但是却可以把控制权交出去了。
  4. asyncio.run是Python 3.7新加的接口,要不然你得这么写:
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

好了,我们先运行一下看看:

Suspending a
In b
Resuming a

看到了吧,在并发执行中,协程a被挂起又恢复过。

Future

接着说Future,它代表了一个「未来」对象,异步操作结束后会把最终结果设置到这个Future对象上。Future是对协程的封装,不过日常开发基本是不需要直接用这个底层Future类的。

Task

Eventloop除了支持协程,还支持注册Future和Task2种类型的对象,那为什么要存在Future和Task这2种类型呢?

先回忆前面的例子,Future是协程的封装,Future对象提供了很多任务方法(如完成后的回调、取消、设置任务结果等等),但是开发者并不需要直接操作Future这种底层对象,而是用Future的子类Task协同的调度协程以实现并发。

Task非常容易创建和使用:

task = asyncio.ensure_future(a())

输出结果:

task
<Task finished coro=<a() done, defined at <ipython-input-1-7f52d64120fd>:6> result=None>

asyncio并发的正确/错误姿势

在代码中使用async/await是不是就能发挥asyncio的并发优势么,其实是不对的,我们先看个例子:

async def a():
    print('Suspending a')
    await asyncio.sleep(3)
    print('Suspending a')
    
async def b():
    print('Suspending b')
    await asyncio.sleep(1)
    print('Suspending b')
    
async def s1():
    await a()
    await b()

有2个协程a和b,分别sleep1秒和3秒,如果协程可以并发执行,那么执行时间应该是sleep最大的那个值(3秒),现在它们都在s1协程里面被调用。大家先猜一下s1会运行几秒?
我们写个小程序验证一下:

import time
def show_perf(func):
    print('*'*20)
    start = time.perf_counter()
    asyncio.run(func())
    print(f'{func.__name__}Cost:{time.perf_counter() - start}')

大家注意我这个时间计数用的方法,没有用time.time,而是用了Python 3.3新增的time.perf_counter它是现在推荐的用法。我们在IPython里面验证下:

In :show_perf(s1)
********************
Suspending a
Suspending a
Suspending b
Suspending b
s1Cost:4.001041395999891

看到了吧,4秒!!!,相当于串行的执行了(sleep 3 + 1)。这是错误的用法,应该怎么用呢,前面的asyncio.gather就可以:

async def c1():
    await asyncio.gather(a(),b())

In :show_perf(c1)
********************
Suspending a
Suspending b
Suspending b
Suspending a
s2Cost:3.0015872030003266

看到了吧,3秒!另外一个是asyncio.wait:

async def c2():
   await asyncio.wait([a(),b()])
In : show_perf(s2)
********************
Suspending a
Suspending b
Suspending b
Suspending a
s2Cost:3.00240059799998

同样是3秒。先别着急,gather和wait下篇文章还会继续对比。还有一个方案就是用asyncio.create_task:

async def c3():
    task1 = asyncio.create_task(a())
    task2 = asyncio.create_task(b())
    await task1
    await task2
    
async def c4():
    task = asyncio.create_task(b())
    await a()
    await task
===========================================   
In :show_perf(c3)
********************
Suspending a
Suspending b
Suspending b
Suspending a
c3Cost:3.0008804760000203

In :show_perf(c4)
********************
Suspending a
Suspending b
Suspending b
Suspending a
c4Cost:3.002528893999852    

都是3秒。asyncio.create_task相当于把协程封装成Task。不过大家要注意一个错误的用法:

async def s2():
    await asyncio.create_task(a())
    await asyncio.create_task(b())
    
In :show_perf(s2)
********************
Suspending a
Suspending a
Suspending b
Suspending b
s2Cost:4.000821826999982

直接await asyncio.create_task不会对并发有帮助。asyncio.createtask是Python 3.7新增的高阶API,是推荐的用法,其实你还可以用asyncio.ensure_future和loop.createtask:

async def c5():
    task = asyncio.ensure_future(b())
    await a()
    await task
    
async def c6():
    loop = asyncio.get_event_loop()
    task = loop.create_task(b())
    await a()
    await task
    
In :show_perf(c5)
********************
Suspending a
Suspending b
Suspending b
Suspending a
c5Cost:2.987646211000083

In :show_perf(c6)
********************
Suspending a
Suspending b
Suspending b
Suspending a
c6Cost:2.9867309130004287

到这里,我们一共看到2种错误的,6种正确的写法。你学到了么?


延伸阅读
  • https://www.python.org/dev/peps/pep-0492/
  • https://github.com/python/cpython/blob/3.7/Lib/asyncio/futures.py#L365

你可能感兴趣的:(异步,python)