【实验楼个人教学笔记】
要写个asyncio的服务器,只能抽空学学这个库。
在 Python 3.4 中,asyncio 模块出现,此时创建协程函数须使用 asyncio.coroutine
装饰器标记。此前的包含 yield from
语句的函数既可以称作生成器函数也可以称作协程函数,为了突出协程的重要性,现在使用 asyncio.coroutine
装饰器的函数就是真正的协程函数了。
coroutine 协程
协程对象,使用 asyncio.coroutine
装饰器装饰的函数被称作协程函数,它的调用不会立即执行函数,而是返回一个协程对象,即协程函数的运行结果为协程对象,注意这里说的 “运行结果” 不是 return 值。协程对象需要包装成任务注入到事件循环,由事件循环调用。
task 任务
将协程对象作为参数创建任务,任务是对协程对象的进一步封装,其中包含任务的各种状态。
event_loop 事件循环
将多线程比喻为工厂里的多个车间,那么协程就是一个车间内的多台机器。
在线程级程序中,一台机器开始工作,车间内的其它机器不能同时工作,需要等上一台机器停止,但其它车间内的机器可以同时启动,这样就可以显著提高工作效率。
在协程程序中,一个车间内的不同机器可以同时运转,启动机器、暂停运转、延时启动、停止机器等操作都可以人为设置。
事件循环能够控制任务运行流程,也就是任务的调用方。
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
协程对象不能直接运行,必须放入事件循环中或者由 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
async
/ await
关键字在 Python 3.5 中新增了 async
/ await
关键字用来定义协程函数。这两个关键字是一个组合,其作用等同于 asyncio.coroutine
装饰器和 yield from
语句。此后协程与生成器就彻底泾渭分明了。
假如协程包含一个 IO 操作(这几乎是肯定的),等它处理完数据后,我们希望得到通知,以便下一步数据处理。这一需求可以通过向 future 对象
中添加回调来实现。那么什么是 future 对象?task 对象就是 future 对象,我们可以这样认为,因为asyncio.Task
是 asyncio.Future
的子类。也就是说,task 对象可以添加回调函数。回调函数的最后一个参数是 future
或 task 对象
,通过该对象可以获取协程返回值。如果回调需要多个参数,可以通过偏函数导入。
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
实际项目中,往往有多个协程创建多个任务对象,同时在一个 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
代码说明:
到这一步,大家应该可以看得出,上面的代码已经是异步编程的结构了,在事件循环内部,两个协程是交替运行完成的。简单叙述一下程序协程部分的运行过程:
-> 首先运行 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
-> 打印程序运行时间
-> 程序全部结束
需要额外说明的几点:
在事件循环启动之后停止之前,我们可以手动取消任务的执行,注意 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
事件循环的 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
方法实现的。
事件循环的 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
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
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
的参数。
按照字面意思来看,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