一篇文章学会 asyncio 模块

【实验楼个人教学笔记】
要写个asyncio的服务器,只能抽空学学这个库。

1、协程装饰器

在 Python 3.4 中,asyncio 模块出现,此时创建协程函数须使用 asyncio.coroutine 装饰器标记。此前的包含 yield from 语句的函数既可以称作生成器函数也可以称作协程函数,为了突出协程的重要性,现在使用 asyncio.coroutine 装饰器的函数就是真正的协程函数了。

2、任务和时间循环

  • coroutine 协程
    协程对象,使用 asyncio.coroutine装饰器装饰的函数被称作协程函数,它的调用不会立即执行函数,而是返回一个协程对象,即协程函数的运行结果为协程对象,注意这里说的 “运行结果” 不是 return 值。协程对象需要包装成任务注入到事件循环,由事件循环调用。

  • task 任务
    将协程对象作为参数创建任务,任务是对协程对象的进一步封装,其中包含任务的各种状态

  • event_loop 事件循环
    将多线程比喻为工厂里的多个车间,那么协程就是一个车间内的多台机器。
    在线程级程序中,一台机器开始工作,车间内的其它机器不能同时工作,需要等上一台机器停止,但其它车间内的机器可以同时启动,这样就可以显著提高工作效率。
    在协程程序中,一个车间内的不同机器可以同时运转,启动机器、暂停运转、延时启动、停止机器等操作都可以人为设置。

事件循环能够控制任务运行流程,也就是任务的调用方。

3、一个简单的例子

In [50]: import time

In [51]: import asyncio

In [52]: def one():
    ...:     start = time.time()
    ...:
    ...:     @asyncio.coroutine   # 使用协程装饰器创建协程函数
    ...:     def do_some_work():  # 协程函数
    ...:         print('Start coroutine')
    ...:         time.sleep(0.1)  # 模拟 IO 操作
    ...:         print('This is a coroutine')
    ...:
    ...:     loop = asyncio.get_event_loop()     # 创建事件循环。每个线程中只能有一个事件循环,get_event_loop 方法会获取当前已经存在的事件循环,如果当前线程中没有,新建一个
    ...:     coroutine = do_some_work()          # 调用协程函数获取协程对象
    ...:     loop.run_until_complete(coroutine)  
    ...:	 # 将协程对象注入到事件循环,协程的运行由事件循环控制。事件循环的 run_until_complete 方法会阻塞运行,直到任务全部完成。协程对象作为 run_until_complete 方法的参数,loop 会自动将协程对象包装成任务来运行。后面我们会讲到多个任务注入事件循环的情况
    ...:
    ...:     end = time.time()
    ...:     print('运行耗时:{:.4f}'.format(end - start))  # 打印程序运行耗时
    ...:

In [53]: one()
Start coroutine
This is a coroutine
运行耗时:0.1062

4、任务状态

协程对象不能直接运行,必须放入事件循环中或者由 yield from 语句调用。将协程对象注入事件循环的时候,其实是 run_until_complete 方法将协程包装成了一个任务(task)对象,任务对象保存了协程运行后的状态,用于未来获取协程的结果。
修改之前的代码:

In [56]: def two():
    ...:     start = time.time()
    ...:
    ...:     @asyncio.coroutine
    ...:     def do_some_work():
    ...:         print('Start coroutine')
    ...:         time.sleep(0.1)
    ...:         print('This is a coroutine')
    ...:
    ...:     loop = asyncio.get_event_loop()
    ...:     coroutine = do_some_work()
    ...:     task = loop.create_task(coroutine)  # 事件循环的 create_task 方法可以创建任务,另外 asyncio.ensure_future 方法也可以创建任务,参数须为协程对象
    ...:     print('task 是不是 asyncio.Task 的实例?', isinstance(task, asyncio.Task))  # task 是 asyncio.Task 类的实例,为什么要使用协程对象创建任务?因为在这个过程中 asyncio.Task 做了一些工作,包括预激协程、协程运行中遇到某些异常时的处理
    ...:     print('Task state:', task._state)   # task 对象的 _state 属性保存当前任务的运行状态,任务的运行状态有 PENDING 和 FINISHED 两种
    ...:     loop.run_until_complete(task)       # 将任务注入事件循环,阻塞运行
    ...:     print('Task state:', task._state)
    ...:
    ...:     end = time.time()
    ...:     print('运行耗时:{:.4f}'.format(end - start))
    ...:

In [57]: two()
task 是不是 asyncio.Task 的实例? True
Task state: PENDING
Start coroutine
This is a coroutine
Task state: FINISHED
运行耗时:0.1052

5、async / await 关键字

在 Python 3.5 中新增了 async / await 关键字用来定义协程函数。这两个关键字是一个组合,其作用等同于 asyncio.coroutine 装饰器和 yield from语句。此后协程与生成器就彻底泾渭分明了。

6、绑定回调

假如协程包含一个 IO 操作(这几乎是肯定的),等它处理完数据后,我们希望得到通知,以便下一步数据处理。这一需求可以通过向 future 对象 中添加回调来实现。那么什么是 future 对象?task 对象就是 future 对象,我们可以这样认为,因为asyncio.Taskasyncio.Future 的子类。也就是说,task 对象可以添加回调函数。回调函数的最后一个参数是 futuretask 对象,通过该对象可以获取协程返回值。如果回调需要多个参数,可以通过偏函数导入。

In [64]: def three():
    ...:     start = time.time()
    ...:
    ...:     # @asyncio.coroutine
    ...:     async def corowork():      # 使用 async 关键字替代 asyncio.coroutine 装饰器创建协程函数
    ...:         print('[corowork] Start coroutine')
    ...:         time.sleep(0.1)
    ...:         print('[corowork] This is a coroutine')
    ...:
    ...:     def callback(name, task):  # 回调函数,协程终止后需要顺便运行的代码写入这里,回调函数的参数有要求,最后一个位置参数须为 task 对象
    ...:         print('[callback] Hello {}'.format(name))
    ...:         print('[callback] coroutine state: {}'.format(task._state))
    ...:
    ...:     loop = asyncio.get_event_loop()
    ...:     coroutine = corowork()
    ...:     task = loop.create_task(coroutine)
    ...:	 # task 对象的 add_done_callback 方法可以添加回调函数,注意参数必须是回调函数,这个方法不能传入回调函数的参数,这一点需要通过 functools 模块的 partial 方法解决,将回调函数和其参数 name 作为 partial 方法的参数,此方法的返回值就是偏函数,偏函数可作为 task.add_done_callback 方法的参数
    ...:     task.add_done_callback(functools.partial(callback, 'Shiyanlou'))
    ...:     loop.run_until_complete(task)
    ...:
    ...:     end = time.time()
    ...:     print('运行耗时:{:.4f}'.format(end - start))
    ...:

In [65]: import functools

In [66]: three()
[corowork] Start coroutine
[corowork] This is a coroutine
[callback] Hello Shiyanlou
[callback] coroutine state: FINISHED
运行耗时:0.1051

7、多任务

实际项目中,往往有多个协程创建多个任务对象,同时在一个 loop 里运行。为了把多个协程交给 loop,需要借助 asyncio.gather方法。任务的 result 方法可以获得对应的协程函数的 return 值。

In [67]: def four():
    ...:     start = time.time()
    ...:
    ...:     async def corowork(name, t):
    ...:         print('[corowork] Start coroutine', name)
    ...:         await asyncio.sleep(t)                  # 1
    ...:         print('[corowork] Stop coroutine', name)
    ...:         return 'Coroutine {} OK'.format(name)   # 2
    ...:
    ...:     loop = asyncio.get_event_loop()
    ...:     coroutine1 = corowork('ONE', 3)             # 3
    ...:     coroutine2 = corowork('TWO', 1)             # 3
    ...:     task1 = loop.create_task(coroutine1)        # 4
    ...:     task2 = loop.create_task(coroutine2)        # 4
    ...:     gather = asyncio.gather(task1, task2)       # 5
    ...:     loop.run_until_complete(gather)             # 6
    ...:     print('[task1] ', task1.result())           # 7
    ...:     print('[task2] ', task2.result())           # 7
    ...:
    ...:     end = time.time()
    ...:     print('运行耗时:{:.4f}'.format(end - start))
    ...:

In [68]: four()
[corowork] Start coroutine ONE
[corowork] Start coroutine TWO
[corowork] Stop coroutine TWO
[corowork] Stop coroutine ONE
[task1]  Coroutine ONE OK
[task2]  Coroutine TWO OK
运行耗时:3.0070

代码说明:

  1. await 关键字等同于 Python 3.4 中的 yield from 语句,后面接协程对象。asyncio.sleep 方法的返回值为协程对象,这一步为阻塞运行。asyncio.sleep 与 time.sleep 是不同的,前者阻塞当前协程,即 corowork 函数的运行,而 time.sleep 会阻塞整个线程,所以这里必须用前者,阻塞当前协程,CPU 可以在线程内的其它协程中执行
  2. 协程函数的 return 值可以在协程运行结束后保存到对应的 task 对象的 result 方法中
  3. 创建两个协程对象,在协程内部分别阻塞 3 秒和 1 秒
  4. 创建两个任务对象
  5. 将任务对象作为参数,asyncio.gather 方法创建任务收集器。注意,asyncio.gather 方法中参数的顺序决定了协程的启动顺序
  6. 将任务收集器作为参数传入事件循环的 run_until_complete 方法,阻塞运行,直到全部任务完成
  7. 任务结束后,事件循环停止,打印任务的 result 方法返回值,即协程函数的 return 值

到这一步,大家应该可以看得出,上面的代码已经是异步编程的结构了,在事件循环内部,两个协程是交替运行完成的。简单叙述一下程序协程部分的运行过程:

-> 首先运行 task1
-> 打印 [corowork] Start coroutine ONE
-> 遇到 asyncio.sleep 阻塞
-> 释放 CPU 转到 task2 中执行
-> 打印 [corowork] Start coroutine TWO
-> 再次遇到 asyncio.sleep 阻塞
-> 这次没有其它协程可以运行了,只能等阻塞结束
-> task2 的阻塞时间较短,阻塞 1 秒后先结束,打印 [corowork] Stop coroutine TWO
-> 又过了 2 秒,阻塞 3 秒的 task1 也结束了阻塞,打印 [corowork] Stop coroutine ONE
-> 至此两个任务全部完成,事件循环停止
-> 打印两个任务的 result
-> 打印程序运行时间
-> 程序全部结束

需要额外说明的几点:

  • 1、多数情况下无需调用 task 的 add_done_callback 方法,可以直接把回调函数中的代码写入 await 语句后面,协程是可以暂停和恢复的
  • 2、多数情况下同样无需调用 task 的 result 方法获取协程函数的 return 值,因为事件循环的 run_until_complete 方法的返回值就是协程函数的 return 值
  • 3、事件循环有一个 stop 方法用来停止循环和一个 close 方法用来关闭循环。以上示例中都没有调用 loop.close 方法,似乎并没有什么问题。所以到底要不要调用 loop.close 呢?简单来说,loop 只要不关闭,就还可以再次运行 run_until_complete 方法,关闭后则不可运行。有人会建议调用 loop.close,彻底清理 loop 对象防止误用,其实多数情况下根本没有这个必要。
  • 4、asyncio 模块提供了 asyncio.gather 和 asyncio.wait 两个任务收集方法,它们的作用相同,都是将协程任务按顺序排定,再将返回值作为参数加入到事件循环中。前者在上文已经用到,后者与前者的区别是它可以获取任务的执行状态(PENING & FINISHED),当有一些特别的需求例如在某些情况下取消任务,可以使用 asyncio.wait 方法。

8、取消任务

在事件循环启动之后停止之前,我们可以手动取消任务的执行,注意 PENDING 状态的任务才能被取消,FINISHED 状态的任务已经完成,不能取消。

# File Name: async_cancel.py

import asyncio

async def work(id, t):
    print('Working...')
    await asyncio.sleep(t)
    print('Work {} done'.format(id))

def main():
    loop = asyncio.get_event_loop()
    coroutines = [work(i, i) for i in range(1, 4)]            # 创建一个列表,列表中有 3 个协程对象,协程内部分别阻塞 1 - 3 秒

    try:
        loop.run_until_complete(asyncio.gather(*coroutines))  # 程序运行过程中,快捷键 Ctrl + C 会触发 KeyboardInterrupt 异常。捕获这个异常,在程序终止前完成 # 3 和 # 4 代码的执行
    except KeyboardInterrupt:
        loop.stop()    # 事件循环的 stop 方法取消所有未完成的任务,停止事件循环
    finally:
        loop.close()   # 关闭事件循环

if __name__ == '__main__':
    main()

运行结果:

$ python3 async_cancel.py
Working...
Working...
Working...
Work 1 done
^C%

任务的 cancel方法也可以取消任务,而 asyncio.Task.all_tasks方法可以获得事件循环中的全部任务。修改上文代码中的 main 函数如下:

def main():
    loop = asyncio.get_event_loop()
    coroutines = [work(i, i) for i in range(1, 4)]
    # 程序运行过程中,快捷键 Ctrl + C 会触发 KeyboardInterrupt 异常
    try:
        loop.run_until_complete(asyncio.gather(*coroutines))
    except KeyboardInterrupt:
        print()
        # 每个线程里只能有一个事件循环
        # 此方法可以获得事件循环中的所有任务的集合
        # 任务的状态有 PENDING 和 FINISHED 两种
        tasks = asyncio.Task.all_tasks()
        for i in tasks:
            print('取消任务:{}'.format(i))
            # 任务的 cancel 方法可以取消未完成的任务
            # 取消成功返回 True ,已完成的任务取消失败返回 False
            print('取消状态:{}'.format(i.cancel()))
    finally:
        loop.close()

运行结果:

$ python3 async_cancel.py
Working...
Working...
Working...
Work 1 done
^C
取消任务:<Task finished coro=<work() done, defined at a.py:5> result=None>
取消状态:False
取消任务:<Task pending coro=<work() running at a.py:7> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x102cd8a38>()]> cb=[gather.<locals>._done_callback() at /usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/asyncio/tasks.py:664]>
取消状态:True
取消任务:<Task pending coro=<work() running at a.py:7> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x102cd8a98>()]> cb=[gather.<locals>._done_callback() at /usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/asyncio/tasks.py:664]>
取消状态:True

9、无限循环任务

事件循环的 run_until_complete方法运行事件循环,当其中的全部任务完成后,自动停止事件循环;run_forever方法为无限运行事件循环,需要自定义 loop.stop方法并执行之才会停止。

# File Name: run_forever.py

import asyncio

async def work(loop, t):
    print('start')
    await asyncio.sleep(t)  # 模拟 IO 操作
    print('after {}s stop'.format(t))
    loop.stop()             # 停止事件循环,stop 后仍可重新运行

loop = asyncio.get_event_loop()             # 创建事件循环
task = asyncio.ensure_future(work(loop, 1)) # 创建任务,该任务会自动加入事件循环
loop.run_forever()  # 无限运行事件循环,直至 loop.stop 停止
loop.close()        # 关闭事件循环,只有 loop 处于停止状态才会执行

运行程序:

$ python3 run_forever.py
start
after 1s stop

以上是单任务事件循环,将 loop作为参数传入协程函数创建协程,在协程内部执行loop.stop方法停止事件循环。下面是多任务事件循环,使用回调函数执行 loop.stop 停止事件循环,修改 run_forever.py文件如下:

# File Name: run_forever.py

import time
import asyncio
import functools

def loop_stop(loop, future):    # 函数的最后一个参数须为 future / task
    loop.stop()                 # 停止事件循环,stop 后仍可重新运行

async def work(t):              # 协程函数
    print('start')
    await asyncio.sleep(t)      # 模拟 IO 操作
    print('after {}s stop'.format(t))

def main():
    loop = asyncio.get_event_loop()
    # 创建任务收集器,参数为任意数量的协程,任务收集器本身也是 task / future 对象
    tasks = asyncio.gather(work(1), work(2))
    # 任务收集器的 add_done_callback 方法添加回调函数
    # 当所有任务完成后,自动运行此回调函数
    # 注意 add_done_callback 方法的参数是回调函数
    # 这里使用 functools.partial 方法创建偏函数以便将 loop 作为参数加入
    tasks.add_done_callback(functools.partial(loop_stop, loop))
    loop.run_forever()  # 无限运行事件循环,直至 loop.stop 停止
    loop.close()        # 关闭事件循环

if __name__ == '__main__':
    start = time.time()
    main()
    end = time.time()
    print('耗时:{:.4f}s'.format(end - start))

运行结果:

$ python3 run_forever.py
start
start
after 1s stop
after 2s stop
耗时:2.0064s

loop.run_until_complete方法本身也是调用loop.run_forever方法,然后通过回调函数调用 loop.stop 方法实现的。

10、加入普通函数

事件循环的 call_soon方法可以将普通函数作为任务加入到事件循环并立即排定任务的执行顺序。

# File Name: call_soon.py

import asyncio
import time

def hello(name):          # 普通函数
    print('[hello] Hello, {}'.format(name))

async def work(t, name):  # 协程函数
    print('[work ] start', name)
    await asyncio.sleep(t)
    print('[work ] {} after {}s stop'.format(name, t))

def main():
    loop = asyncio.get_event_loop() 
    # 向事件循环中添加任务
    asyncio.ensure_future(work(1, 'A'))     # 第 1 个执行
    # call_soon 将普通函数当作 task 加入到事件循环并排定执行顺序
    # 该方法的第一个参数为普通函数名字,普通函数的参数写在后面
    loop.call_soon(hello, 'Tom')            # 第 2 个执行
    # 向事件循环中添加任务
    loop.create_task(work(2, 'B'))          # 第 3 个执行
    # 阻塞启动事件循环,顺便再添加一个任务  
    loop.run_until_complete(work(3, 'C'))   # 第 4 个执行

if __name__ == '__main__':
    main()

运行结果:

$ python3 call_soon.py
[work ] start A
[hello] Hello, Tom
[work ] start B
[work ] start C
[work ] A after 1s stop
[work ] B after 2s stop
[work ] C after 3s stop

11、稍后执行普通函数

loop.call_later此方法同 loop.call_soon 一样,可将普通函数作为任务放到事件循环里,不同之处在于此方法可延时执行,第一个参数为延时时间。

# File Name: call_later.py

import asyncio
import functools

def hello(name):            # 普通函数
    print('[hello]  Hello, {}'.format(name))

async def work(t, name):    # 协程函数
    print('[work{}]  start'.format(name))
    await asyncio.sleep(t)
    print('[work{}]  stop'.format(name))

def main():
    loop = asyncio.get_event_loop()
    asyncio.ensure_future(work(1, 'A'))         # 任务 1
    loop.call_later(1.2, hello, 'Tom')          # 任务 2
    loop.call_soon(hello, 'Kitty')              # 任务 3
    task4 = loop.create_task(work(2, 'B'))      # 任务 4
    loop.call_later(1, hello, 'Jerry')          # 任务 5
    loop.run_until_complete(task4)

if __name__ == '__main__':
    main()

运行结果:

$ python3 call_later.py
[workA]  start
[hello]  Hello, Kitty
[workB]  start
[hello]  Hello, Jerry
[workA]  stop
[hello]  Hello, Tom
[workB]  stop

12、其他常用方法

call_soon 立刻执行call_later 延时执行,call_at 在某时刻执行
loop.time 就是事件循环内部的一个计时方法,返回值是时刻,数据类型是 float

def main():
    loop = asyncio.get_event_loop()
    start = loop.time()                         # 事件循环内部时刻
    asyncio.ensure_future(work(1, 'A'))         # 任务 1
    # loop.call_later(1.2, hello, 'Tom')
    # 上面注释这行等同于下面这行
    loop.call_at(start+1.2, hello, 'Tom')       # 任务 2
    loop.call_soon(hello, 'Kitty')              # 任务 3
    task4 = loop.create_task(work(2, 'B'))      # 任务 4
    # loop.call_later(1, hello, 'Jerry')
    # 上面注释这行等同于下面这行
    loop.call_at(start+1, hello, 'Jerry')       # 任务 5

    loop.run_until_complete(task4)

运行文件结果与 call_later.py 一致,不再展示。

这三个 call_xxx 方法的作用都是将普通函数作为任务排定到事件循环中,返回值都是 asyncio.events.TimerHandle实例,注意它们不是协程任务 ,不能作为 loop.run_until_complete的参数。

13、协程锁

按照字面意思来看,asyncio.lock 应该叫做异步 IO 锁,之所以叫协程锁,是因为它通常使用在子协程中,其作用是将协程内部的一段代码锁住,直到这段代码运行完毕解锁。协程锁的固定用法是使用 async with 创建协程锁的上下文环境,将代码块写入其中。

import asyncio

l = []
lock = asyncio.Lock()   # 协程锁

async def work(name):
    print('lalalalalalalala')     # 打印此信息是为了测试协程锁的控制范围
    # 这里加个锁,第一次调用该协程,运行到这个语句块,上锁
    # 当语句块结束后解锁,开锁前该语句块不可被运行第二次
    # 如果上锁后有其它任务调用了这个协程函数,运行到这步会被阻塞,直至解锁
    # with 是普通上下文管理器关键字,async with 是异步上下文管理器关键字
    # 能够使用 with 关键字的对象须有 __enter__ 和 __exit__ 方法
    # 能够使用 async with 关键字的对象须有 __aenter__ 和 __aexit__ 方法
    # async with 会自动运行 lock 的 __aenter__ 方法,该方法会调用 acquire 方法上锁
    # 在语句块结束时自动运行 __aexit__ 方法,该方法会调用 release 方法解锁
    # 这和 with 一样,都是简化 try ... finally 语句
    async with lock:
        print('{} start'.format(name))  # 头一次运行该协程时打印
        if 'x' in l:                    # 如果判断成功
            return name                 # 直接返回结束协程,不再向下执行
        await asyncio.sleep(0); print('----------')  # 阻塞 0 秒,切换协程
        l.append('x')
        print('{} end'.format(name))
        return name

async def one():
    name = await work('one')
    print('{} ok'.format(name))

async def two():
    name = await work('two')
    print('{} ok'.format(name))

def main():
    loop = asyncio.get_event_loop()
    tasks = asyncio.wait([one(), two()])
    loop.run_until_complete(tasks)

if __name__ == '__main__':
    main()

运行结果:

$ python3 async_lock.py
lalalalalalalala
one start
lalalalalalalala
----------
one end
one ok
two start
two ok
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值