本节介绍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()是个无限循环。

怎么让python程序不停止 python怎样让程序一直执行_事件循环

        以上是单任务事件循环,将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 作为参数加入回调函数。

怎么让python程序不停止 python怎样让程序一直执行_无限循环_02

        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')),阻塞启动事件循环,而且又添加了一个任务。

 

怎么让python程序不停止 python怎样让程序一直执行_事件循环_03

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'仍处于暂停状态。

怎么让python程序不停止 python怎样让程序一直执行_事件循环_04

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,因此终端不会输出锁的释放提示。

怎么让python程序不停止 python怎样让程序一直执行_无限循环_05

        至此关于yield、yield from(await)、@asyncio.coroutine(async)、asyncio的介绍已经完毕,下一节将使用更多丰富的案例介绍协程的应用。