Python3中asyncio异步详解一

在学习asyncio相关的知识前,如果有同学没有读到Python3中yield与yield from详解的话,还请先浏览一下,以便能对asyncio有更好的理解。至于如何学习asyncio呢?我想从以下几个方面来阐述:

一:asyncio工作原理和重要概念
        1.1: 事件循环 -  Eventloop
        1.2: 协程对象 -  Coroutine
        1.3: Future & Task 对象
        1.4: async/await 原生协程
        1.5: asyncio 如何正确启动

一: asyncio工作原理和重要概念

那么在学习asyncio之前,有个插曲,其实你应该要知道操作系统中的网络编程的相关知识,比如阻塞IO、非阻塞IO、IO多路复用、异步IO等,具体可参考 Linux IO模式及 select、poll、epoll详解,对其有个大概的理解,方便我们对asyncio的理解。下面我们将讲解几个asyncio 相关的概念问题:

1.1:事件循环 -  Eventloop

Eventloop 可以说是 asyncio 应用的核心,是中央总控。Eventloop 实例提供了注册、取消和执行任务和回调的方法。程序开启一个无限的循环,程序员会把一些函数(协程)注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。

把一些异步函数 (就是任务,Task,一会就会说到) 注册到这个事件循环上,事件循环会循环执行这些函数 (但同时只能执行一个),当执行到某个函数时,如果它正在等待 I/O 返回,事件循环会暂停它的执行去执行其他的函数;当某个函数完成 I/O 后会恢复,下次循环到它的时候继续执行。因此,这些异步函数可以协同 (Cooperative) 运行:这就是事件循环的目标。

(1): 事件循环是执行我们的异步代码并决定如何在异步函数之间切换的对象。如果某个协程在等待某些资源,我们需要暂停它的执行,在事件循环中注册这个事件,以便当事件发生的时候,能再次唤醒该协程的执行。
(2): 运行异步函数我们首先需要创建一个协程,然后创建future或task对象,将它们添加到事件循环中,到目前为止,我们的异步函数中没有任何代码被执行过,只有调用loop.run_until_completed启动事件循环,才会开始执行future或task对象,loop.run_until_completed会阻塞程序直到所有的协程对象都执行完毕。
备注:事件循环的本质就是通过 await Coroutinec_or_task_ or_future 将程序的控制权还给CPU, 然后CPU执行其它的任务,当再次遇到 await xxx,CPU又获取到了控制权,再次选择执行其它任务。当某个任务(或称事件)完成了,唤醒CPU, 让其获取控制权执行后面的代码。


1、事件循环是在线程中执行
2、从队列中取得任务
3、每个任务在协程中执行下一步动作
4、如果在一个协程中调用另一个协程(await ),会触发上下文切换,挂起当前协程,并保存现场环境(变量,状态),然后载入被调用协程
5、如果协程的执行到阻塞部分(阻塞I/O,Sleep),当前协程会挂起,并将控制权返回到线程的消息循环中,然后消息循环继续从队列中执行下一个任务...以此类推
6、队列中的所有任务执行完毕后,消息循环返回第一个任务

  1.2: 协程对象 -  Coroutine

协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。协程 (Coroutine) 本质上是一个函数.

1:async 关键定义的函数(称协程),关键字await 只能在协程内部中使用, 其它地方不能使用
2:协程内部不能使用 yield from,报语法错误,但可以使用 yield (但很少使用, 自python3.7之后只有使用 async/await定义的函数才是原生协程,会逐渐抛弃yield与asyncio.coroutine这种zai Python 3.5装饰器生成的协程)
3:被 async 关键定义的协程,实际上是一个Coroutine对象,而Coroutine又继承Awaitable 
        from collections.abc import Coroutine, Awaitable
4:定义协程示例:
        async  def  async_test():
                await asyncio.sleep(2)
                ......

Awaitable 对象: await 关键字用于将程序控制权移交给事件循环并中断当前协程的执行。它有以下几个使用规则:

a: 只能用在由 async def 修饰的函数中,在普通函数中使用会抛出异常。
b: 调用一个协程函数后,就必须等待其执行完成并返回结果。
c: await func() 中的 func() 必须是一个 awaitable 对象。即一个协程函数或者一个在内部实现了 __await__() 方法的对象,该方法会返回一个生成器

Awaitable 对象包含协程、Task 和 Future 等。

  1.3:  Future & Task 对象

Future 对象: 

代表将来执行或没有执行的任务的结果,它和task上没有本质的区别。

它代表了一个「未来」对象,异步操作结束后会把最终结果设置到这个 Future 对象上。Future 是对协程的封装,不过日常开发基本是不需要直接用这个底层 Future 类的。

Future对象封装了一个未来会被计算的可调用的异步执行对象,他们能被放入队列,他们的状态、结果或者异常能被查询。 Future对象有一个result属性,用于存放未来的执行结果。还有个set_result()方法,是用于设置result的,并且会在给result绑定值以后运行事先给Future对象添加的回调。回调是通过Future对象的add_done_callback()方法添加的。

重要的是Future对象不能被我们创建,只能被异步框架创建,有两种方法:

# 该函数在 Python 3.7 中被加入,更加高层次的函数,返回Task对象

future1= asyncio.create_task(my_coroutine)

# 在Python 3.7 之前,是更加低级的函数,返回Future对象或者Task对象

future2= asyncio.ensure_future(my_coroutine)

第一种方法在循环中添加一个协程并返回一个task对象,task对象是future的子类型。第二种方法非常相似,当传入协程对象时返回一个Task对象,唯一的区别是它也可以接受Future对象或Task对象,在这种情况下它不会做任何事情并且返回Future对象或者Task对象不变。

Future对象有几个状态:

Pending:就绪

Running:运行

Done:完成

Cancelled:取消

创建Future对象的时候,状态为pending,事件循环调用执行的时候就是running,调用完毕就是done,如果需要取消Future对象的调度执行,可调用Future对象的cancel()函数。

除此之外,Future对象还有下面一些常用的方法:

result():立即返回Future对象运行结果或者抛出执行时的异常,没有timeout参数,如果Future没有完成,不会阻塞等待结果,而是直接抛出InvalidStateError异常。最好的方式是通过await获取运行结果,await会自动等待Future完成返回结果,也不会阻塞事件循环,因为在asyncio中,await被用来将控制权返回给事件循环。

done():非阻塞的返回Future对象是否成功取消或者运行结束或被设置异常,而不是查看future是否已经执行完成。

cancelled():判断Future对象是否被取消。

add_done_callback():传入一个可回调对象,当Future对象done时被调用。

exception():获取Future对象中的异常信息,只有当Future对象done时才会返回。

get_loop():获取当前Future对象绑定的事件循环。

需要注意的是,当在协程内部引发未处理的异常时,它不会像正常的同步编程那样破坏我们的程序,相反,它存储在future内部,如果在程序退出之前没有处理异常,则会出现以下错误:

Task  exception  was  never  retrieved

有两种方法可以解决此问题,在访问future对象的结果时捕获异常或调用future对象的异常函数:

try:
        # 调用结果时捕获异常
        my_promise.result()
catchException:
        pass
# 获取在协程执行过程中抛出的异常
my_promise.exception()

Task对象

一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。Task 对象是 Future 的子类,它将 coroutine 和 Future 联系在一起,将 coroutine 封装成一个 Future 对象。

Future 是协程的封装,Future 对象提供了很多任务方法 (如完成后的回调、取消、设置任务结果等等),但是开发者并不需要直接操作 Future 这种底层对象,而是用 Future 的子类 Task 协同的调度协程以实现并发。

Task对象被用来在事件循环中运行协程。如果一个协程在等待一个Future对象,Task对象会挂起该协程的执行并等待该Future对象完成。当该Future对象完成,被暂停的协程将恢复执行。事件循环使用协作调度: 一个事件循环每次运行一个Task对象。当一个Task对象等待一个Future对象完成时,该事件循环会运行其他Task、回调或执行IO操作。

使用高层级的asyncio.create_task()函数来创建Task对象,也可用低层级的loop.create_task()或ensure_future()函数。不建议手动实例化 Task 对象。

Python 的例子:

import asyncio

import time

async  def  compute(x, y):
        print("Compute {} + {}...".format(x, y))
        await  asyncio.sleep(2.0)
        return x+y

async  def  print_sum(x, y):
        result =await compute(x, y)
        print("{} + {} = {}".format(x, y, result))

start = time.time()     
loop = asyncio.get_event_loop() 
tasks = [ 
        asyncio.ensure_future(print_sum(0,0)),
        asyncio.ensure_future(print_sum(1,1)),
        asyncio.ensure_future(print_sum(2,2)),
]
loop.run_until_complete(asyncio.wait(tasks)) 
loop.close() 
print("Total elapsed time {}".format(time.time() - start))

上面的代码的执行流程是:

详细的流程如下:

 1.4: async/await 原生协程

Python设计者们在 3.5 中新增了async/await语法(PEP 492),将协程作为原生Python语言特性,并且将他们与生成器明确的区分开。它避免了生成器/协程中间的混淆,方便编写出不依赖于特定库的协程代码,称之为原生协程。async/await 和 yield from这两种风格的协程底层复用共同的实现,而且相互兼容。在Python 3.6 中asyncio库“转正”,不再是实验性质的,成为标准库的正式一员。

Python 3.5 添加了types.coroutine修饰器,也可以像 asyncio.coroutine 一样将生成器标记为协程。你可以用 async def 来定义一个协程函数,虽然这个函数不能包含任何形式的 yield 语句;只有 return 和 await 可以从协程中返回值。

 * 句法 async def 引入了原生协程或者说异步生成器。表达式 async with 和 async for 也是允许的,稍后就可以看到。

* 关键字 await 将控制器传递给时间循环。(挂起当前运行的协程。)Python执行的时候,在g() 函数范围内如果遇到表达式 await f(),就是 await 在告诉事件循环“挂起 g() 函数,直到 f() 返回结果,在此期间,可以运行其他函数。”

上述第二点在代码中大致如下:

async def g():
        # 暂停,知道f()在返回到g()
        await  f()

关于要不要用 async/await,以及何时使用,如何使用,都有一套严格的规则。无论你是在使用语法还是已经使用 async/await,这些规则都会很方便:

1. 协程是引入了 async def 的函数。你可能会用到 await,return 或者 yield,但是这些都是可选的。Python允许使用 async def noop(): pass 声明:
        1.1. 使用 await 与 return 的组合创建协程函数。想要调用一个协程函数,必须使用 await等待返回结果。
        1.2. 在 async def 代码块中使用 yield 的情况并不多见(只有Python的近期版本才可用)。当你使用 async for 进行迭代的时候,会创建一个异步生成器。暂时先忘掉异步生成器,将目光放在使用 await 与 return 的组合创建协程函数的语法上。
        1.3. 在任何使用 async def 定义的地方都不可以使用 yield from,这会引发异常 SyntaxError。

2. 一如在 def 定义的函数之外使用 yield 会引发异常 SyntaxError,在 async def 定义的协程之外使用 await 也会引发异常 SyntaxError。你只能在协程内部使用 await。

这里有一个简介的例子,总结了上面的几条规则:

最后,当你使用 await f() 时,要求 f() 是一个可等待的对象。但这并没有什么用。现在,只需要知道可等待对象要么是(1)其他的协程,要么就是(2)定义了 .await() 函数且返回迭代器的对象。如果你正在编写程序,绝大多数情况只需要关注案例#1。

这给我们带来了一些技术上的差异:将一个函数标记为协程的旧的一个方式是使用 @asyncio.coroutine 装饰一个普通的函数。这是基于生成器的协程。但是这种方式自Python 3.5中出现了 async/await 语法后就已经过时了。

下面两个协程基本上是等价的(都是可等待的),但第一个是基于生成器的,而第二个是原生协程

如果你写代码的时候更趋向于显式声明而不是隐式声明,那么最好是使用原生协程。基于生成器的协程将会在Python 3.10版本移除。

本教程的后半部分,我们会再涉及一些基于生成器的协程的优点。为了使协程成为Python中独立的标准功能,并与常规生成器区分开,以减少歧义,Python引入 async/await。

不要沉迷于基于生成器的协程,它已经被 async/await 取代了。如果你要使用 async/await 语法的话,注意它的一些特有的规则(比如,await 不能用于基于生成器的协程),这些规则很大程度上与基于生成器的协程不兼容。

协程的主要属性包括:

1:async def函数始终为协程,即使它不包含await表达式。

2:如果在async函数中使用yield或者yield from表达式会产生SyntaxError错误。

3:在内部,引入了两个新的代码对象标记:
        CO_COROUTINE用于标记原生协程(和新语法一起定义)
        CO_ITERABLE_COROUTINE用于标记基于生成器的协程,兼容原生协程。(通过types.coroutine()函数设置)

4:常规生成器在调用时会返回一个genertor对象,同理,协程在调用时会返回一个coroutine对象。

5:协程不再抛出StopIteration异常,而是替代为RuntimeError。常规生成器实现类似的行为需要进行引入future(PEP-3156)

6:当协程进行垃圾回收时,一个从未被await的协程会抛出RuntimeWarning异常

types.coroutine():在types模块中新添加了一个函数coroutine(fn)用于asyncio中基于生成器的协程与本PEP中引入的原生携协程互通。使用它,“生成器实现的协程”和“原生协程”之间可以进行互操作。

@types.coroutine
def  process_data(db):
        data =yieldfromread_data(db)
        ...

这个函数将生成器函数对象设置CO_ITERABLE_COROUTINE标记,将返回对象变为coroutine对象。如果fn不是一个生成器函数,那么它会对其进行封装。如果它返回一个生成器,那么它会封装一个awaitable代理对象。

注意:CO_COROUTINE标记不能通过types.coroutine()进行设置,这就可以将新语法定义的原生协程与基于生成器的协程进行区分。

await与yield from相似,await关键字的行为类似标记了一个断点,挂起协程的执行直到其他awaitable对象完成并返回结果数据。它复用了yield from的实现,并且添加了额外的验证参数。await只接受以下之一的awaitable对象:

一个原生协程函数返回的原生协程对象。

一个使用types.coroutine()修饰器的函数返回的基于生成器的协程对象。这种用法已经被废弃

一个包含返回迭代器的await方法的对象。

协程链:协程的一个关键特性是它们可以组成协程链,就像函数调用链一样,一个协程对象是awaitable的,因此其他协程可以await另一个协程对象。

任意一个yield from链都会以一个yield结束,这是Future实现的基本机制。因此,协程在内部中是一种特殊的生成器。每个await最终会被await调用链条上的某个yield语句挂起。

关于基于生成器的协程和async定义的原生协程之间的差异,关键点是只有基于生成器的协程可以真正的暂停执行并强制性返回给事件循环。所以每个await最终会被await调用链条上的某个由types.coroutine()装饰的包含yield语句的协程函数挂起。

为了启用协程的这一特点,一个新的魔术方法__await__被添加进来。在asyncio中,对于对象在await语句启用Future对象只需要添加await = iter这行到asyncio.Future类中。带有await方法的对象也叫做Future-like对象。

另外还新增了异步上下文管理 async with 和异步迭代器 async for。异步生成器和异步推导式都让迭代变得并发,他们所做的只是提供同步对应的外观,但是有问题的循环能够放弃对事件循环的控制,以便运行其他协程。

关于何时以及如何能够和不能使用async / await,有一套严格的规则:

使用async关键字创建一个协程函数,里面包含await或者return,调用协程函数,必须使用await获得函数返回结果。

在async异步函数中使用yield并不常见,这会创建一个异步生成器,可以使用async for来迭代异步生成器。

在async异步函数中使用yield from会抛出语法错误。同样在普通函数中使用await也是语法错误。

Python异步编程详解

Python 3.5中async/await的工作机制

Python Async/Await入门指南

  1.5: asyncio 如何正确启动

协程完整的工作流程是这样的

定义/创建协程对象

将协程转为task任务

定义事件循环对象容器

将task任务扔进事件循环对象中触发

1.5.1 错误姿势:看似是异步,其实是同步

async def coro_a():        
        print("Suspending coro_a")        
        await asyncio.sleep(3)        
        print("running coro_a")        
        return 100

async def coro_b():    
        print("Suspending coro_b")   
        await asyncio.sleep(1)    
        print("running coro_b")    
        return [1, 23, 45]

async def sync_run():

        """ 其实是同步执行 """
        await coro_a()
        await coro_b()

def show_perf(func):
        start = time.perf_counter()
        asyncio.run(func())
        print(f'{func.__name__} Cost: {time.perf_counter() - start}')

>>> show_perf(sync_run) # 同步
>>> Suspending coro_a
>>> running coro_a
>>> Suspending coro_b
>>> running coro_b
>>> ync_run Cost: 4.0023612

同步的原因解析:运行 asyncio.run(func()) 后,将 func() 转化成future对象,并放入事件循环中,随后事件循环执行的是sync_run() 里面的代码,程序运行至 await coro_a()处,暂停并阻塞,并将CPU控制权移交出去,直到coro_a() 执行完成并重新获取CPU控制权执行下面的代码coro_b(),一直如此,待整个程序结束, 所以这种和同步没什么区别。
(1):await coro  作如上解释
(2):await task_or_future 不会阻塞

1.5.2 正确姿势一:使用asyncio.gather 或 asyncio.wait

async def async_run():
        await asyncio.gather(coro_a(), coro_b())      # 异步
        # await asyncio.wait([coro_a(), coro_b()])         # 异步 Py3.11 will removal asyncio.wait

>>> show_perf(async_run) # 异步
>>> Suspending coro_a
>>> Suspending coro_b
>>> running coro_b
>>> running coro_a
>>> async_run Cost: 3.0031817

asyncio.gather

gather的参数为 coroutines_or_futures, 即协程 或 task 或 future 的可变参数:
(1): tasks = await asyncio.gather(*[coro1, coro2, coro3])  | asyncio.gather(coro1, coro2, coro3)
(2):  tasks = awaitasyncio.gather(*[task1, task2,task3])  |  asyncio.gather( task1,task2,task3)
(3):  tasks = awaitasyncio.gather(*[futu1, futu2,futu3])  |   asyncio.gather( futu1, futu2, futu3)
(4):  tasks = awaitasyncio.gather(*[coro1, task2, futu3])  |   asyncio.gather( coro1 ,  task2, futu3)

返回的是已完成Task的result。

asyncio.wait

wait的参数为 fs, 即协程 或 task 或 future  的单个列表或元祖:
(1):  tasks =  await asyncio.wait([coro1, coro2, coro3]) 
(2):  tasks = await asyncio.wait([task1, task2,task3])     
(3):  tasks = await asyncio.wait([futu1, futu2, futu3])     
(4): tasks = await asyncio.wait([coro1,  task2, futu3])   

async.wait会返回两个值:done和pending,done为已完成的协程Task,pending为超时未完成的协程Task,需通过future.result调用Task的result。

1.5.3 正确姿势二:await task_or_future

async def async_run():
        # 异步
        task_a = asyncio.create_task(coro_a())
        task_b = asyncio.create_task(coro_b())
        await task_a
        await task_b

>>> show_perf(async_run) # 异步OK
>>> Suspending coro_a
>>> Suspending coro_b
>>>  running coro_b
>>> running coro_aasync_run
>>> Cost: 3.0027209000000004

1.5.4 错误姿势三:await task_or_future, 直接await task不会对并发有帮助

async def async_run():
        asyncio.create_task(coro_a())
        asyncio.create_task(coro_b())

>>> show_perf(async_run) # 同步
>>> Suspending coro_a
>>> running coro_a
>>> Suspending coro_b
>>> running coro_basync_run
>>> Cost: 4.0028486

1.5.5 正确姿势四:先 await coro, 再 await  task_or_future

async def async_run():
         # 也可以用:asyncio.ensure_future(coro_a()) 或者 loop.create_task(coro_a())
        task_a= asyncio.create_task(coro_a())         #  py3.7推荐用法
        await coro_b()        # 先协程
        await task_a            # 再task

>>> show_perf(async_run)      # 异步OK
>>> Suspending coro_b
>>> Suspending coro_a
>>> running coro_b
>>> running coro_aasync_run
>>> Cost: 3.0015209

======================错误姿势=======================
  async def async_run():        
        # 也可以用:asyncio.ensure_future(coro_a()) 或者 loop.create_task(coro_a())
        task_a = asyncio.create_task(coro_a()) 
        await task_a            #  先task 
        await coro_b()        # 再协程        

>>> show_perf(async_run)      # 同步
>>> async_run Cost: 4.004120800000001
====================== 错误姿势 =======================

疑问:正常来说一般 await coro_task_or_future 程序会挂起,将cpu控制权移交给其他程序,但是在一个协程里多次直接 await有次序之分,那么对于 await 后程序挂起的理论就不能完全按上文提到的那样理解,await究其机制到此我还并未真正探究明白,等后面补上,暂时只要能区分1.5.1 、 1.5.31.5.41.5.5 中各种姿势的对错与否即可

你可能感兴趣的:(Python3中asyncio异步详解一)