Python-asyncio的使用-1

asyncio模块提供了使用协程构建并发应用的工具。它使用一种单线程单进程的方式实现并发,应用的各个部分彼此合作,可以显示的切换任务,一般会在程序阻塞I/O操作的时候发生上下文切换如等待读写文件,或者请求网络。同时asyncio也支持调度代码在将来的某个特定事件运行,从而支持一个协程等待另一个协程完成,以处理系统信号和识别其他一些事件。

对于其他的并发模型大多数采用的都是线性的方式编写。并且依赖于语言运行时系统或操作系统底层的底层线程或进程来适当的改变上下文,而基于asyncio的应用要求应用代码显示的处理上下文切换。asyncio提供的框架以事件循环为中心,程序开启一个无限的循环,程序会把一些函数注册到事件循环当中,当满足事件发生的时候,调用相应的协程函数。

异步方法

使用asyncio也就意味着你需要一直写异步方法。一个标准方法是这样的:

def regular_double(x):
    return 2 * x

而一个异步方法:

async def async_double(x):
    return 2 * x

从外观上来看,异步方法和标准方法没什么区别,只是前面多加了一个asyncasyncasynchronous的简写,为了区别于异步函数,我们称标准函数为同步函数。要调用异步函数需要在前面增加一个await关键字,但是不能在同步函数中使用await,否则会报错,需在异步函数中使用!

错误写法:

def print_result():
    print(await async_double(3))

正确写法:

async print_result():
    print(await async_double(3))

协程

启动一个协程

一般异步方法被称之为协程(Coroutine)。asyncio事件循环可以通过多种不同的方法启动一个协程。如下:

async def print_message():
    print("这是一个协程")
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        print("开始运行协程")
        coro = print_message()
        print("进入事件循环")
        loop.run_until_complete(coro)
    finally:
        print("关闭事件循环")
        loop.close()

输出结果如下:

开始运行协程
进入事件循环
这是一个协程
关闭事件循环

这是一个协程的简单例子:第一步首先获得一个事件循环的应用就是定义的对象loop。可以使用默认的事件循环,也可以实例化一个特定的循环类(比如uvloop),这里使用了默认的循环loop.run_until_complete(coro)方法用这个协程启动这个循环,协程返回时这个方法将停止循环。run_until_complete的参数是一个fetrue对象,当传入一个协程,其内部会自动封装成一个task,其中task是fetrue的子类

从协程中返回值

我们将上面的代码进行修改,如下:

async def print_message():
    print("这是一个协程")
    return "协程返回值"
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        print("开始运行协程")
        coro = print_message()
        print("进入事件循环")
        result = loop.run_until_complete(coro)
        print(f"result: {result}")
    finally:
        print("关闭事件循环")
        loop.close()

输出结果如下:

开始运行协程
进入事件循环
这是一个协程
result: 协程返回值
关闭事件循环

run_until_complete可以获取协程的返回值,如果没有给定返回值,就像普通函数一样,默认返回None

协程调用协程

一个协程启动另外一个协程,从而可以任务根据工作内容封装到不同的协程当中,我们可以使用await关键字,链式的调度协程,来形成一个协程任务流。如下:

async def main():
    print("这是一个主协程")
    print("等待coroutine_1执行")
    result_1 = await coroutine_1()
    print("等待coroutine_2执行")
    result_2 = await coroutine_2(result_1)
    return result_1, result_2
    
async def coroutine_1():
    print("这是coroutine_1协程")
    return "coroutine_1"
    
async def coroutine_2(args):
    print("这是coroutine_2协程")
    return f"coroutine_1 + {args}"
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        result = loop.run_until_complete(main())
        print(f"result: {result}")
    finally:
        print("关闭事件循环")
        loop.close()

输出结果如下:

这是一个主协程
等待coroutine_1执行
这是coroutine_1协程
等待coroutine_2执行
这是coroutine_2协程
result: ('coroutine_1', 'coroutine_1 + coroutine_1')
关闭事件循环

协程中调用普通函数

在协程中可以通过一些方法去调用普通的函数。可以使用的关键字有call_sooncall_latercall_at

call_soon

call_soon(callback, *args, context=None)

在下一个迭代的事件循环中立刻调用回调函数,大部分的回调函数都支持位置参数,而不支持“关键字参数”,如果想要使用关键字参数,则推荐使用functools.partial()方法来进行包装。请看下面例子:

import asyncio
import functools


def call_back(*args, **kwargs):
    print(f"回调函数的参数, args: {args}, kwargs: {kwargs}")
    
async def main(loop):
    print("注册callback")
    loop.call_soon(call_back, 1)
    wrapper = functools.partial(call_back, name="laozhang")
    loop.call_soon(wrapper, 1)
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        print("关闭事件循环")
        loop.close()

输出结果如下:

注册callback
回调函数的参数, args: (1,), kwargs: {}
回调函数的参数, args: (1,), kwargs: {'name': 'laozhang'}
关闭事件循环

有时,我们不想立即调用一个函数,此时我们就可以调用call_later延时去调用一个函数了

call_soon_threadsafe

call_soon_threadsafe(callback, *args)

类似call_soon,但是线程安全

call_later

call_later(delay, callback, *args, context=None)

首先简单说一下它的含义,就是事件循环在delay多长时间之后才执行callback函数,配合上面的call_soon让我们看一个小例子:

import time
import asyncio

def call_back(*args, **kwargs):
    t = time.time()
    print(f"args: {args}, time: {t}")
    
async def main(loop):
    print("注册call_back", time.time())
    loop.call_later(1, call_back, "process-1")
    loop.call_later(2, call_back, "process-2")
    loop.call_soon(call_back, "process-3")
    await asyncio.sleep(5)
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

输出结果如下:

注册call_back 1584685555.1941936
args: ('process-3',), time: 1584685555.1951926
args: ('process-1',), time: 1584685556.1960201
args: ('process-2',), time: 1584685557.196542

从输出结果可以看出,call_soon是立即执行的,两个call_later是并发执行的!如果没有最后的await asyncio.sleep(5),那么两句call_later将不会执行,只会执行call_soon!

call_soon会在call_later之前执行,和它的位置在哪无关,call_later的第一个参数越小,越先执行!

call_at

call_at(when, call_back, context=None)

call_at第一个参数的含义代表的是一个单调时间,它和我们平时说的系统时间有点差异,这里的时间指的是时间循环内部的时间,可以通过loop.time()获取,然后可以在此基础上进行操作。后面的参数和前面的两个方法一样,实际上call_later内部就是调用的call_at

import asyncio

def call_back(num, loop):
    print(f"call_back函数, num: {num}, time: {loop.time()}")
    
async def main(loop):
    now = loop.time()
    print("注册call_back函数", now)
    loop.call_at(now + 1, call_back, 1, loop)
    loop.call_at(now + 2, call_back, 2, loop)
    loop.call_soon(call_back, 3, loop)
    await asyncio.sleep(5)
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

输出结果如下:

注册call_back函数 242044.906
call_back函数, num: 3, time: 242044.906
call_back函数, num: 1, time: 242045.906
call_back函数, num: 2, time: 242046.906

Future

获取Future的结果

future表示还没有完成的工作结果,事件循环可以通过监视一个future对象的状态来指示它已经完成。future对象有几个状态:

  • pending
  • running
  • done
  • canceled

创建Future的时候,taskpending,事件循环调用执行的时候自然就是running,调用完毕自然就是done。如果要停止事件循环,就需要先把task取消,状态为cancel

import asyncio

def call_back(future, result):
    print(f"进入call_back函数, future: {future}, result: {result}")
    future.set_result(result)
    print(f"此时future的结果为: {future}")

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        future = asyncio.Future()
        loop.call_soon(call_back, future, "result is finished")
        result = loop.run_until_complete(future)
        print(f"run_until_complete, result: {result}")
    finally:
        loop.close()

输出结果如下:

进入call_back函数, future: <Future pending cb=[_run_until_complete_cb() at ...]>, result: result is finished
此时future的结果为: <Future finished result='result is finished'>
run_until_complete, result: result is finished

通过输出结果可以发现,调用set_result之后,对象的状态会由pending变为finishedfuture的实例会保留提供给方法的结果,可以在后续使用。

future对象使用await

future可以像协程一样使用await关键字获取结果

import asyncio

def call_back(future, result):
    print(f"进入call_back函数, future: {future}, result: {result}")
    future.set_result(result)

async def main(loop):
    print(f"进入main异步函数")
    future = asyncio.Future()
    loop.call_soon(call_back, future, "result is finished")
    print(await future)
    
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

输出结果如下:

进入main异步函数
进入call_back函数, future: <Future pending cb=[<TaskWakeupMethWrapper object at 0x0000028A3A932468>()]>, result: result is finished
result is finished

future回调

future在完成的时候可以执行一些回调函数,回调函数按照注册时的顺序进行调用

import asyncio
import functools

def call_back(future, n):
    print(f"进入call_back函数, future: {future}, n: {n}")
    
async def register_callbacks(future):
    print("注册call_back")
    future.add_done_callback(functools.partial(call_back, n=1))
    future.add_done_callback(functools.partial(call_back, n=2))
    
async def main(future):
    await register_callbacks(future)
    print("设置future的结果")
    future.set_result("result is finished")

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        future = asyncio.Future()
        loop.run_until_complete(main(future))
    finally:
        loop.close()

输出结果如下:

注册call_back
设置future的结果
进入call_back函数, future: <Future finished result='result is finished'>, n: 1
进入call_back函数, future: <Future finished result='result is finished'>, n: 2

通过add_done_callback方法给future添加回调函数,当future执行完成的时候,就会调用回调函数。并通过future获取协程执行的结果

并发的执行任务

任务(Task)是与事件循环交互的主要途径条件之一。任务可以包装成为协程,可以跟踪协程何时完成,任务是Future的子类,所以使用方法和Future一样。协程可以等待任务,每个任务都有一个结果,在它完成之后可以获取这个结果。因为协程是无状态的,我们通过使用create_task方法,可以将协程包装成有状态的任务。还可以在任务运行的过程中取消任务。

import asyncio

async def child():
    print("进入child协程")
    return "result is ok!"
    
async def main(loop):
    print("进入main协程")
    task = loop.create_task(child())
    print("cancel task")
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("取消任务抛出CancelledError")
    else:
        print("任务的结果: ", task.result())
        
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

输出结果如下:

进入main协程
cancel task
取消任务抛出CancelledError

如果我们把上面的task.cancel()注释掉,那么我们就可以获得正常请看下的结果了,如下:

进入main协程
cancel task
进入child协程
任务的结果:  result is ok!

同样,我们可以使用await关键字来获取任务的结果!

res = await task

除了create_task,我们还可以使用asyncio.ensure_future(coroutine)创建一个task

组合协程

一系列的协程可以通过await链式的调用,但是有的时候我们需要在一个协程里等待多个协程,比如我们在一个协程里等待1000多个异步网络请求,对于访问次序没有要求的时候,就可以使用另外的关键字awaitgather来解决了。await可以暂停一个协程,直到后台操作完成。

等待多个协程

import asyncio

async def child(n):
    print("进入child协程")
    try:
        await asyncio.sleep(n * 0.1)
        return n
    except asyncio.CancelledError:
        print("数字{n}被取消".format(n=n))
        raise
         
async def main():
    tasks = [child(i) for i in range(10)]
    complete, pending = await asyncio.wait(tasks, timeout=0.5)
    print("complete", complete)
    print("pending", pending)
    for task in complete:
        print("当前数字为: {result}".format(result=task.result()))
        
    for task in pending:
        task.cancel()
        
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

输出结果如下:

进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
complete {<Task finished coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> result=4>, <Task finished coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> result=0>, <Task finished coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> result=3>, <Task finished coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> result=1>, <Task finished coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> result=5>, <Task finished coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> result=2>}
pending {<Task pending coro=<child() running at D:/PycharmProject/demo/code-8.py:67> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000001D350A30798>()]>>, <Task pending coro=<child() running at D:/PycharmProject/demo/code-8.py:67> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000001D350A30738>()]>>, <Task pending coro=<child() running at D:/PycharmProject/demo/code-8.py:67> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000001D350A30858>()]>>, <Task pending coro=<child() running at D:/PycharmProject/demo/code-8.py:67> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000001D350A30918>()]>>}
当前数字为: 4
当前数字为: 0
当前数字为: 3
当前数字为: 1
当前数字为: 5
当前数字为: 2
数字7被取消
数字6被取消
数字8被取消
数字9被取消

可以发现我们的结果并没有按照数字的顺序显示,在内部await()使用一个set()保存它创建的Task实例。因为set是无序的,所以这也就是我们的任务没有按顺序执行的原因。await的返回值是一个元组,包括两个集合,分别表示已完成任务和未完成任务。wait函数中timeout参数表示超时值,达到这个超时时间之后,未完成的任务状态变为pending,当程序退出时还有任务没有完成时,就会看到如下的错误提示:

Task was destroyed but it is pending!
task: <Task pending coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x00000295335E0738>()]>>
Task was destroyed but it is pending!
task: <Task pending coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x00000295335E0798>()]>>
Task was destroyed but it is pending!
task: <Task pending coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x00000295335E0858>()]>>
Task was destroyed but it is pending!
task: <Task pending coro=<child() done, defined at D:/PycharmProject/demo/code-8.py:64> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x00000295335E0918>()]>>

我们可以通过迭代调用cancel方法来取消任务,也就是如下这段代码:

for task in pending:
    task.cancel()

gather的作用和wait类似,但不同的是:

  1. gather任务无法取消
  2. 返回值是一个结果列表
  3. 可以按照传入的参数的顺序,顺序输出

我们将上面的代码进行修改一些,使用gather的方式:

import asyncio

async def child(n):
    print("进入child协程")
    try:
        await asyncio.sleep(n * 0.1)
        return n
    except asyncio.CancelledError:
        print("数字{n}被取消".format(n=n))
        raise
        
async def main():
    tasks = [child(i) for i in range(10)]
    complete = await asyncio.gather(*tasks)
    print(complete)
    for result in complete:
        print("当前数字为: {result}".format(result=result))
        
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

输出结果如下:

进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
当前数字为: 0
当前数字为: 1
当前数字为: 2
当前数字为: 3
当前数字为: 4
当前数字为: 5
当前数字为: 6
当前数字为: 7
当前数字为: 8
当前数字为: 9

gather通常被用来阶段性的一个操作,做完第一步才能做第二步,比如下面操作:

import asyncio 
import time

async def child_1(timeout, now):
    await asyncio.sleep(timeout)
    print("第一阶段完成, 需时: {}".format(time.time() - now))
    return timeout

async def child_2(timeout, now):
    await asyncio.sleep(timeout)
    print("第二阶段完成, 需时:{}".format(time.time() - now))    
    return timeout

async def main():
    now = time.time()
    result_list = await asyncio.gather(child_1(5, now), child_2(2, now))
    for result in result_list:
        print("result: {}".format(result))

    print("总需时: {}".format(time.time() - now))

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

输出结果如下:

第二阶段完成, 需时:2.0005438327789307
第一阶段完成, 需时: 5.0014026165008545
result: 5
result: 2
总需时: 5.0014026165008545

通过上面的输出结果可以得到以下结论:

  1. child_1与child_2是并发运行的
  2. gather会等那个最耗时的任务完成之后才返回结果,耗时总时间取决于其中任务最长时间的那个

任务完成时进行处理

as_completed方法返回一个生成器,会管理一个指定的任务列表,并生成它们的结果。每个协程结束运行时一次生成一个结果,与wait一样,as_completed不能保证顺序,不过执行其他动作之前没有必要等待所有后台操作完成

import asyncio

async def foo(n):
    print("Waiting: ", n)
    await asyncio.sleep(2)
    return n
    
async def main():
    tasks = [asyncio.ensure_future(foo(i)) for i in range(10)]
    result_list = asyncio.as_completed(tasks)
    print("result_list", result_list)
    for result in result_list:
        print("Task Res: {}, Content: {}".format(result, await result))
        
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

输出结果如下:

result_list <generator object as_completed at 0x0000013D1FF36308>
Waiting:  0
Waiting:  1
Waiting:  2
Waiting:  3
Waiting:  4
Waiting:  5
Waiting:  6
Waiting:  7
Waiting:  8
Waiting:  9
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF36360>, Content: 0
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF363B8>, Content: 2
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF36360>, Content: 6
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF363B8>, Content: 9
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF36360>, Content: 8
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF363B8>, Content: 5
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF36360>, Content: 7
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF363B8>, Content: 4
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF36360>, Content: 1
Task Res: <generator object as_completed.<locals>._wait_for_one at 0x0000013D1FF363B8>, Content: 3
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章