Python实现协程(六)

本节介绍 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_later 可设置延迟执行,第一个参数为延迟时间:

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'))

运行结果与 2.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)
    print(l)


if __name__ == '__main__':
    main()

运行结果: 当协程 ONE 运行到 await asyncio.sleep(2) 处时,将让步 CPU 的使用权,协程 TWO 开始执行,但执行到 async with lock 时,会阻塞,因为 ONE 还没有释放协程锁,此刻线程进入阻塞状态,开始等待 ONE 释放协程锁。

锁被释放后,协程 ONE 结束运行,返回值作为 await coro_work('ONE') 表达式的值,赋值给 one 函数中的局部变量 name

至此协程 TWO 开始上锁执行,由于此时 if 条件判断返回 True ,将直接 return ,因此终端不会输出锁的释放提示。

至此关于 yieldyield from(await)@asyncio.coroutine(async)asyncio 的介绍已经完毕!

你可能感兴趣的:(Python实现协程(六))