本节介绍asyncio剩余的一些常用操作:事件循环实现无限循环任务,在事件循环中执行普通函数以及协程锁。
一. 无限循环任务
事件循环的run_until_complete方法运行事件循环时,当其中的全部任务完成后,会自动停止循环;若想无限运行事件循环,可使用asyncio提供的run_forever方法:
import asyncio
import time
from datetime import datetime
async def work(loop, t):
print(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S'), '[work] start')
await asyncio.sleep(t) # 模拟IO操作
print(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S'), '[work] finished')
loop.stop() # 停止事件循环,stop后仍可重新运行
if __name__ == '__main__':
loop = asyncio.get_event_loop() # 创建任务,该任务会自动加入事件循环
task = asyncio.ensure_future(work(loop, 1))
print(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S'), '[main]', task._state)
loop.run_forever() # 无限运行事件循环,直至loop.stop停止
print(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S'), '[main]', task._state)
loop.close() # 关闭事件循环,只有loop处于停止状态才会执行
运行结果:使用loop.run_forever()启动无限循环时,task实例会自动加入事件循环。如果注释掉loop.stop()方法,则loop.run_forever()之后的代码永远不会被执行,因为loop.run_forever()是个无限循环。
以上是单任务事件循环,将loop作为参数传入协程函数创建协程,在协程内部执行loop.stop方法停止事件循环。下面是多任务事件循环,使用回调函数执行loop.stop()停止事件循环:
import time
import asyncio
import functools
from datetime import datetime
def loop_stop(loop, future): # 最后一个参数必须为future或task
print(datetime.strftime(datetime.now(), '%H:%M:%S'), '[callback] stop loop by callback.')
loop.stop()
async def work(t):
print(datetime.strftime(datetime.now(), '%H:%M:%S'), '[work] coroutine start.')
await asyncio.sleep(t)
print(datetime.strftime(datetime.now(), '%H:%M:%S'), '[work] coroutine end.')
def main():
loop = asyncio.get_event_loop()
tasks = asyncio.gather(work(3), work(1))
tasks.add_done_callback(functools.partial(loop_stop, loop))
loop.run_forever()
loop.close()
if __name__ == '__main__':
start = time.time()
main()
end = time.time()
print(f'耗时:{end-start}')
运行结果:asyncio.gather创建的搜集器,参数为任意数量的协程,任务搜集器本身也是task / future对象。任务搜集器的add_done_callback方法用来添加回调函数,该函数只在事件循环中所有的任务都完成后运行一次。注意,add_done_callback的参数为回调函数,当回调函数定义了除future参数之外的任何参数后,必须使用偏函数。此处,使用functools.partial 方法创建偏函数以便将 loop 作为参数加入回调函数。
loop.run_until_complete方法本身也是调用loop.run_forever方法,然后通过回调函数调用loop.stop实现。
二. 在事件循环中加入普函数
2.1 加入普通函数,并立即排定执行顺序
事件循环的call_soon方法可以将普通函数作为任务加入到事件循环,并立即排定任务的执行顺序:
import asyncio
def func(name): # 普通函数
print(f'[func] hello, {name}')
async def work(t, name): # 协程函数
print(f'[work] {name} start.')
await asyncio.sleep(t)
print(f'[work] {name} finished.')
def main():
loop = asyncio.get_event_loop()
asyncio.ensure_future(work(3, 'A'))
loop.call_soon(func, 'word')
loop.create_task(work(2, 'B'))
loop.run_until_complete(work(3, 'C'))
if __name__ == '__main__':
main()
运行结果:loop.call_soon将普通函数当作task加入到事件循环并排定执行顺序,该方法的第一个参数为普通函数的名字,普通函数的参数写在后面。loop.run_until_complete(work(3, 'C')),阻塞启动事件循环,而且又添加了一个任务。
2.2 加入普通函数,并在稍后执行
loop.call_later方法同loop.call_soon一样,可将普通函数作为任务放到事件循环里,不同之处在于,call_laster可设置延迟执行,第一个参数为延迟时间:
import asyncio
def func(name): # 普通函数
print(f'[func] hello, {name}')
async def work(t, name): # 协程函数
print(f'[work] {name} start.')
await asyncio.sleep(t)
print(f'[work] {name} finished.')
def main():
loop = asyncio.get_event_loop()
asyncio.ensure_future(work(4, 'A'))
loop.call_later(1, func, 'word1')
loop.call_soon(func, 'word2')
loop.create_task(work(2, 'B'))
loop.call_later(3, func, 'word3')
loop.run_until_complete(work(2, 'C'))
if __name__ == '__main__':
main()
运行结果:work(2, 'C')完成时,输出hello, word3的普通函数尚未执行,协程任务'A'仍处于暂停状态。
2.3 其它常用方法
call_soon立即执行,call_later延迟执行,call_at在某时刻执行;loop.time是事件循环内部的一个即时方法,返回值是时刻,数据类型为float。将上例中的call_later使用loop.time + call_at实现:
def main():
loop = asyncio.get_event_loop()
start = loop.time() # 时间循环内部时刻
asyncio.ensure_future(work(4, 'A'))
# loop.call_later(1, func, 'word1')
# 上面注释这行等同于下面这行
loop.call_at(start+1, func, 'word1')
loop.call_soon(func, 'word2')
loop.create_task(work(2, 'B'))
# loop.call_later(3, func, 'word3')
loop.call_at(start+3, func, 'word3')
loop.run_until_complete(work(2, 'C'))
运行结果与1.2中的示例一致,不再赘述。这三个call_xxx方法的作用都是将函数作为任务排定到事件循环中,返回值都是asyncio.events.TimerHandle实例,注意它们不是协程任务,不能作为loop.run_until_complete的参数。
三. 协程锁
asyncio.lock从字面意思来讲,该被称为异步IO锁,之所以叫协程锁,是因为它通常写在子协程中,用来将协程内部的一段代码锁住,知道这段代码运行完毕解锁。协程锁的固定用法是使用async with创建协程上下文环境,把需要加锁的代码写入其中。(注:with 是普通上下文管理器关键字,async with 是异步上下文管理器关键字;能够使用 with 关键字的对象须有 __enter__ 和 __exit__ 方法,而能够使用 async with 关键字的对象须有 __aenter__ 和 __aexit__ 方法。async with 会自动运行 lock 的 __aenter__ 方法,该方法会调用 acquire 方法上锁;在语句块结束时自动运行 __aexit__ 方法,该方法会调用 release 方法解锁。这和 with 一样,都是简化 try ... finally 语句)
import asyncio
l = []
lock = asyncio.Lock() # 协程锁
async def coro_work(name):
print(f'coroutine {name} start.')
async with lock:
print(f'{name} run with lock start.')
if 'hi' in l:
return name
await asyncio.sleep(2)
l.append('hi')
print(f'{name} release lock.')
return name
async def one():
name = await coro_work('ONE')
print(f'{name} finished.')
async def two():
name = await coro_work('TWO')
print(f'{name} finished')
def main():
loop = asyncio.get_event_loop()
tasks = asyncio.wait([one(), two()])
loop.run_until_complete(tasks)
if __name__ == '__main__':
main()
运行结果: 当协程TWO运行到await asyncio.sleep(2)处时,将让步CPU的使用权,协程ONE开始执行,但执行到async with lock时,会阻塞,因为TWO还没有释放协程锁,此刻线程进入阻塞状态,开始等待TWO释放协程锁。锁被释放后,协程TWO结束运行,返回值作为await coro_work('TWO')表达式的值,赋值给two中的局部变量name。至此协程ONE开始上锁执行,由于此时if条件判断返回True,将直接return,因此终端不会输出锁的释放提示。
至此关于yield、yield from(await)、@asyncio.coroutine(async)、asyncio的介绍已经完毕,下一节将使用更多丰富的案例介绍协程的应用。