总结python3.8版本中asyncio的常用模块及使用方法(一. 协程和任务)

3,382 阅读8分钟

刷了一遍3.8文档中asyncio的部分, 总结一些基本的知识点, python后续版本中, 这个模块更新目测也会比较频繁, 虽然到3.6为止, 这个模块还不是很好用, 但从目前来看, python最新版本更新的asycnio模块已经越来越趋于稳定了.

3.8版本中有一部分的api已经宣布不推荐使用, 并会在python3.10版本中弃用, 本文章中已过滤掉将会弃用的函数,并在文章最后简要概括.

本文内容基于python3.8官方文档编写, 很多内容与python3.8官方文档相同, 英语较好的童鞋请直接阅读官方文档:

docs.python.org/3.8/library…

ok~ 开始扯皮~

先解释一下高级函数和低级函数:

越高级的函数封装的越完整, 越有可能被用户使用.

越低级的函数, 则越接触底层, 越有可能用户基本用不到.

1.1 协程

使用async/await语法声明的协程是编写异步应用程序的首选. 例如, 以下代码段(需要python3.7+)打印"hello", 等待1秒钟, 然后打印"hello"

>>> import asyncio
>>> async def main():...     
    print('hello')...     
    await asyncio.sleep(1)...     
    print('world')
>>> asyncio.run(main()) 
hello
world

请注意,仅调用协程不会调度它的执行:

>>> main() <coroutine object main at 0x1053bb7c8>

为了实际运行协程,asyncio提供了三种主要机制:

  1. 使用asyncio.run()函数来运行顶层入口函数"main"(请参照上面的例子)

  2. 等待协程. 下面的代码片段将在等待1秒钟后显示“ hello”,然后在等待另外 2秒钟后显示“ world” :

import asyncio
import time
async def say_after(delay, what):    
    await asyncio.sleep(delay)    
    print(what)
async def main():    
    print(f"started at {time.strftime('%X')}")    
    await say_after(1, 'hello')    
    await say_after(2, 'world')    
    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

预期的输出:

started at 17:13:52

hello world 

finished at 17:13:55
  1. asyncio.create_task() 以异步任务的形式并发运行协程

让我们修改上面的例子,并发地运行两个say_after协程:

async def main():
    
    task1 = asyncio.create_task(say_after(1, 'hello'))

    
    task2 = asyncio.create_task(say_after(2, 'world'))    
    print(f"started at {time.strftime('%X')}")    # 等待,直到两个任务都完成(应该花费大概2秒钟)
    
    await task1    
    await task2    
    print(f"finished at {time.strftime('%X')}")


注意,预期的输出现在显示代码段比以前快了1秒:

started at 17:14:32

hello world 

finished at 17:14:34

注: 协程的基本写法, 需要通过asyncio.create_task来将异步函数封装成任务, 然后通过asyncio.run来执行协程函数

1.2 异步

如果一个对象可以用在一个await表达式中,我们就说它是一个awaitable对象。许多异步api被设计成awaitable对象。

有三种主要类型的可访问对象:协程、任务和结果。

协程(conoutines):

Python协程是可等待的,因此可以从其他协程等待:

import asyncioasync def nested():    return 42async def main():
    #  如果只调用“nested()”,则什么也不会发生。
    # 一个协程对象被创建,但不是等待,
    # 所以它根本不会运行。
    nested()    # 让我们现在换一种方式来等待他的执行:
    print(await nested())  # 将会输出 "42".asyncio.run(main())

这里有一个很重要的概念是,术语“协程”与两个概念密切相关:

  1. 协程函数:异步def函数;

  2. 协程对象:通过调用协同程序函数返回的对象。

异步还支持基于生成器的协同程序。

任务(task):

任务用于并发地调度协程。

当协程被包装到一个任务中,并带有asyncio.create_task()这样的函数时,协程会被自动调度且很快运行:

import asyncio
async def nested():    
    return 42
async def main():    # 在 "main()" 中调用nested()以尽快并发执行
    
    task = asyncio.create_task(nested())    # "task" 现在可以被用来取消 "nested()" 或者可以简单地等待直到它完成:
    
    await task

asyncio.run(main())

结果(future):

Future是一个特殊的低级可访问对象,它表示异步操作的最终结果。

当一个Future对象被等待时,这意味着协程将等待,直到将来在其他地方被解析。

异步中的Future对象需要允许基于回调的代码与异步/等待一起使用。

通常不需要在应用程序级代码中创建Future的对象。

Future对象,有时通过库和一些asyncio的api暴露,是可以等待的对象:

async def main():    
await function_that_returns_a_future_object()    
# 这也是有效的:
await asyncio.gather(function_that_returns_a_future_object(),
        some_python_coroutine())

loop.run_in_executor()

是返回Future对象的低级函数的一个好例子。

注: 1. 一个异步函数只有被封装成awaitable或任务的形式才会被真正的执行 2. 建议将简单的逻辑通过await函数封装, 复杂的函数通过create_task函数封装

简单示例如下:


import asyncio
async def main():
    result_await = await function_data("t1")  # 会一直阻塞到函数结束
    result_create_task = asyncio.create_task(function_data("t2"))  # 继续, 可以通过下边方式或其他方式检测任务状态
    while not result_create_task.done():
        print("waiting")
        await asyncio.sleep(1)
    else:
        print(result_create_task.done())
        print(result_create_task.result())
asyncio.run(main())


1.3 运行异步程序:

asyncio.run(coro, *, debug=False)

执行协程 coro并返回结果。

这个函数运行传递过来的协程参数,负责管理异步事件循环并完成异步生成器。

当另一个异步事件循环在同一线程中运行时,不能调用此函数。

如果debug设置为True,则事件循环将在调试模式下运行。

这个函数总是创建一个新的事件循环并在结束时关闭它。它应该用作异步程序的主要入口点,并且在理想情况下应该只调用一次。

示例:


async def main():    
    await asyncio.sleep(1)    
    print('hello')
asyncio.run(main())

注: 这个函数是3.7新增加的函数, 建议作为异步程序的唯一主入口函数

1.4 创建任务

asyncio.create_task(coro, *, name=None)

将协程参数coro封装到任务中,并安排其执行。返回任务对象。

如果name不是None,则使用task .set_name()将其设置为任务的名称。

任务在get_running_loop()返回的loop中执行,如果当前线程中没有运行loop,则会引发RuntimeError。

这个函数是在Python 3.7中添加的。在Python 3.7之前,可以使用低级别的asyncio.ensure_future()函数: 在版本3.8中, 新增名称参数

async def coro():    ...# In Python 3.7+
    task = asyncio.create_task(coro())... # 这适用于所有Python版本,但可读性较差
    task = asyncio.ensure_future(coro())

注: 这个函数也是3.7新增加的函数, 可以将异步函数作为参数放入此函数中封装成任务, 并通过上边的run函数来执行任务

1.5 从其他线程调度

asyncio.run_coroutine_threadsafe(coro, loop)

将协程提交给给定的事件loop。该函数是线程安全的。

返回一个concurrent.futures.Future 去接收另一个OS线程的结果。

这个函数应该从不同的OS线程调用,而不是在事件循环运行的线程中调用。

示例:


# 创建一个协程
coro = asyncio.sleep(1, result=3)
# 将协程提交给给定的loop
future = asyncio.run_coroutine_threadsafe(coro, loop)
# 等待带有可选超时参数的结果
assert future.result(timeout) == 3

如果在协程中引发异常,将通知返回的Future。也可以用来取消事件循环中的任务:


try:
    
    result = future.result(timeout)
except asyncio.TimeoutError:    
    print('协程花了太长时间,取消了任务…')
    
    future.cancel()
except Exception as exc:    
    print(f'协程引发了一个异常: {exc!r}')
else:    
    print(f'返回的协程: {result!r}')

与其他异步函数不同,此函数需要显式传递loop参数。

注: 可以通过该函数做到不同线程间的任务调度

1.6 自省

asyncio.current_task(loop=None)

返回当前正在运行的任务实例,如果没有任务正在运行,则返回None。

如果loop为空,则使用get_running_loop()获取当前循环。

注: python3.7中新增的api, 是非常实用的函数

asyncio.all_tasks(loop=None)¶

返回一组由loop运行的尚未完成的任务对象。

如果loop为None,则使用get_running_loop()获取当前的loop。

注: python3.7中新增的api, 是非常实用的函数

emmmm, 第一部分的主要内容就是如上了, 上述模块也是asyncio目前最新版本中最核心的功能api, 请牢记用法.

现在记录一下目前还存在, 但是预计在python3.10中删掉的api:

1. 睡眠

asyncio.sleep(delay, result=None, *, loop=None)

用来挂起当前任务, 等待其他任务

2. 并发运行的任务

asyncio.gather(*aws, loop=None, return_exceptions=False)¶

在aws中并发运行可异步的对象

3. 屏蔽取消

asyncio.shield(aw, *, loop=None)

保护一个可被取消的对象。

4. 超时

asyncio.wait_for(aw, timeout, *, loop=None)¶

等待一个可异步的程序完成并设置超时

5. 等待

asyncio.wait(aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED)¶

并发地运行aws中的可异步对象,并阻塞该对象,直到返回指定的条件为止。

asyncio.as_completed(aws, *, loop=None, timeout=None)

并发地运行aws集合中的可访问对象。返回Future对象的迭代器。返回的每个Future对象中表示剩余等待对象集合的最早结果。

6. task对象

class asyncio.Task(coro, *, loop=None, name=None)

运行Python协程的类似Future的对象。不是线程安全的。

7. 基于协程的生成器

基于生成器的协程先于异步/等待语法。它们是Python生成器,使用表达式的方式来等待Future和其他协程。

如果本篇内容能够帮助到您, 希望点个关注, 共同成长~