Python 协程 asyncio 之 ABC_a coroutine object is require

19 阅读7分钟

    await say_after(1, 'hello')     await say_after(2, 'world')

    print(f"finished at {time.strftime('%X')}")

if name == 'main':     loop = asyncio.get_event_loop()     # 阻塞直到hello world()协程结束时返回     loop.run_until_complete(main())     loop.close()


或者我们可以通过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')}")

    # 等待两个子任务完成     await task1     await task2     print(f"finished at {time.strftime('%X')}")


#### Awaitables


我们说,如果一个对象可以用在await表达式中,那么它就是Awaitables的对象。  
 可等待对象主要有三种类型:coroutines, Tasks, and Futures.


Coroutines


前面的代码中演示了协程的运作方式,这里主要强调两点。


* 协程函数:asyc def定义的函数;
* 协程对象:通过调用协程函数返回的对象。


Tasks


任务对协程进一步封装,其中包含任务的各种状态。  
 协程对象不能直接运行,在注册事件循环的时候,其实是run\_until\_complete方法将协程包装成为了一个任务(task)对象。



import asyncio

async def nested():     await asyncio.sleep(2)     print("等待2s")

async def main():     # 将协程包装成任务含有状态     # task = asyncio.create_task(nested())     task = asyncio.ensure_future(nested())     print(task)     # "task" can now be used to cancel "nested()", or     # can simply be awaited to wait until it is complete:     await task     print(task)     print(task.done())

if name == 'main':     loop = asyncio.get_event_loop()     try:         loop.run_until_complete(main())     except KeyboardInterrupt as e:         for task in asyncio.Task.all_tasks():             print(task)             task.cancel()             print(task)         loop.run_forever()  # restart loop     finally:         loop.close()


可以看到



<Task pending coro=<nested() running at /Users/chennan/pythonproject/asyncproject/asyncio-cn/1-2-1.py:9>> 等待2s <Task finished coro=<nested() done, defined at /Users/chennan/pythonproject/asyncproject/asyncio-cn/1-2-1.py:9> result=None> True


创建task后,task在加入事件循环之前是pending状态然后调用nested函数等待2s之后打印task为finished状态。asyncio.ensure\_future(coroutine) 和 loop.create\_task(coroutine)都可以创建一个task,python3.7增加了asyncio.create\_task(coro)。其中task是Future的一个子类


### Future


future:代表将来执行或没有执行的任务的结果。它和task上没有本质的区别  
 通常不需要在应用程序级别代码中创建Future对象。  
 future对象有几个状态:


* Pending
* Running
* Done
* Cancelled


通过上面的代码可以知道创建future的时候,task为pending,事件循环调用执行的时候是running,调用完毕自然就是done于是调用task.done()打印了true。


如果在命令行中运行上述代码,ctrl+c后会发现  
 输出以下内容



<Task pending coro=<nested() running at 1-2-1.py:9>> ^C<Task pending coro=<main() running at 1-2-1.py:21> wait_for=<Task pending coro=<nested() running at 1-2-1.py:10> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x10d342978>()]> cb=[<TaskWakeupMethWrapper object at 0x10d342918>()]>> <Task pending coro=<main() running at 1-2-1.py:21> wait_for=<Task pending coro=<nested() running at 1-2-1.py:10> wait_for=<Future cancelled> cb=[<TaskWakeupMethWrapper object at 0x10d342918>()]>> <Task pending coro=<nested() running at 1-2-1.py:10> wait_for=<Future cancelled> cb=[<TaskWakeupMethWrapper object at 0x10d342918>()]> <Task cancelling coro=<nested() running at 1-2-1.py:10> wait_for=<Future cancelled> cb=[<TaskWakeupMethWrapper object at 0x10d342918>()]>


因为我们调用了task.cancel() 所以可以看到此时的任务状态为取消状态。


并发的执行任务


通过使用await+asyncio.gather可以完成并发的操作。  
 asyncio.gather用法如下。  
**asyncio.gather(\*aws, loop=None, return\_exceptions=False)**  
 \*\*aws是一系列协程,协程都成功完成,就返回值一个结果列表。结果值的顺序与aws中添加协程的顺序相对应。  
 return\_exceptions=False,其实就是如果有一个任务失败了,就直接抛出异常。如果等于True就把错误信息作为结果返回回来。  
 首先来一个正常情况不出错的例子:



import asyncio

async def factorial(name, number):     f = 1     for i in range(2, number + 1):         print(f"Task {name}: Compute factorial({i})...")         if number == 2:             1 / 0         await asyncio.sleep(1)         f *= i     print(f"Task {name}: factorial({number}) = {f}")

async def main():     # Schedule three calls concurrently:     res = await asyncio.gather(         *[factorial("A", 2),           factorial("B", 3),           factorial("C", 4)]         , return_exceptions=True)     for item in res:         print(item)

if name == 'main':     loop = asyncio.get_event_loop()     try:         loop.run_until_complete(main())     except KeyboardInterrupt as e:         for task in asyncio.Task.all_tasks():             print(task)             task.cancel()             print(task)         loop.run_forever()  # restart loop     finally:         loop.close()


输入以下内容:



Task A: Compute factorial(2)... Task B: Compute factorial(2)... Task C: Compute factorial(2)... Task B: Compute factorial(3)... Task C: Compute factorial(3)... Task B: factorial(3) = 6 Task C: Compute factorial(4)... Task C: factorial(4) = 24 division by zero None None


可以发现async.gather最后会返回一系列的结果,如果出现了错误就把错误信息作为返回结果,这里我当数字为2时人为加了异常操作1/0,于是返回了结果division by zero,对于其他的任务因为没有返回值所以是None。这里return\_exceptions=True来保证了如果其中一个任务出现异常,其他任务不会受其影响会执行到结束。


**asyncio.wait**



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


asyncio.wait和async.gather用法差不多只是async.wait接收的是个列表。  
 第三个参数和async.gather有点区别.




| 参数名 | 含义 |
| --- | --- |
| FIRST\_COMPLETED | 任何一个future完成或取消时返回 |
| FIRST\_EXCEPTION | 任何一个future出现错误将返回,如果出现异常等价于ALL\_COMPLETED |
| ALL\_COMPLETED | 当所有任务完成或者被取消时返回结果,默认值。 |


#### Timeouts


通过使用asyncio.wait\_for来完成一个超时函数回调操作,如果函数规定时间内未完成则报错。  
**asyncio.wait\_for(aw, timeout, \*, loop=None)**  
 aw代表一个协程,timeout单位秒。



async def eternity():     # Sleep for one hour     await asyncio.sleep(3600)     print('yay!')

async def main():     # Wait for at most 1 second     try:         await asyncio.wait_for(eternity(), timeout=1.0)     except asyncio.TimeoutError:         print('timeout!')

asyncio.run(main())

# Expected output:

#     timeout!


1秒内eternity没有完成就报错了。  
 python3.7中发生更改:当aw由于超时而被取消时,不再显示异常而是等待aw被取消。  
 说到timeout的,如果仅仅是对一个代码块做timeout操作而不是等待某个协程此时推荐第三方模块async\_timeout


#### async\_timeout


安装



pip installa async_timeout


使用方法很简单如下



async with async_timeout.timeout(1.5) as cm:     await inner() print(cm.expired)


如果1.5s可以运行完打印true,否则打印false,表示超时。


#### asyncio.as\_completed


**asyncio.as\_completed(aws, \*, loop=None, timeout=None)**  
 使用as\_completed会返回一个可以迭代的future对象,同样可以获取协程的运行结果,使用方法如下:



async def main():     coroutine1 = do_some_work(1)     coroutine2 = do_some_work(2)     coroutine3 = do_some_work(4)

    tasks = [         asyncio.ensure_future(coroutine1),         asyncio.ensure_future(coroutine2),         asyncio.ensure_future(coroutine3)     ]     for task in asyncio.as_completed(tasks):         result = await task         print('Task ret: {}'.format(result))

start = now()

loop = asyncio.get_event_loop() done = loop.run_until_complete(main()) print('TIME: ', now() - start)


#### 协程嵌套


使用async可以定义协程,协程用于耗时的io操作,我们也可以封装更多的io操作过程,这样就实现了嵌套的协程,即一个协程中await了另外一个协程,如此连接起来  
 官网实例:


![](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a17cda15a5f44136999dd617c0fc092b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407800&x-signature=w%2BPRMx2e9E%2Bnn7c8u08Rq0Kvv3s%3D)


  
 图解:


![](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cf82c8b0c0c846d68169072adc3fd771~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407800&x-signature=awR1ZKZDy3VIpRJmT27OWGhR4Ww%3D)


  
  1、run\_until\_complete运行,会注册task(协程:print\_sum)并开启事件循环 →


 2print\_sum协程中嵌套了子协程,此时print\_sum协程暂停(类似委托生成器),转到子协程(协程:compute)中运行代码,期间子协程需sleep1秒钟,直接将结果反馈到event loop中,即将控制权转回调用方,而中间的print\_sum暂停不操作 →


 31秒后,调用方将控制权给到子协程(调用方与子协程直接通信),子协程执行接下来的代码,直到再遇到wait(此实例没有)→


 4、 最后执行到return语句,子协程向上级协程(print\_sum抛出异常:StopIteration),同时将return返回的值返回给上级协程(print\_sum中的result接收值),print\_sum继续执行暂时时后续的代码,直到遇到return语句 →


 5、向 event loop 抛出StopIteration异常,此时协程任务都已经执行完毕,事件循环执行完成(event loop :the loop is stopped),close事件循环。


#### 调度线程


**asyncio.run\_coroutine\_threadsafe(coro, loop)**  
 等待其他线程返回一个concurrent.futures.Future对象,这是一个线程安全的方法。  
 这个函数应该从不同的OS线程调用,而不是从事件循环所在的线程调用。



def start_loop(loop):     asyncio.set_event_loop(loop)     loop.run_forever()

async def do_some_work(x):     print('Waiting {}'.format(x))     await asyncio.sleep(x)     print('Done after {}s'.format(x))

def more_work(x):     print('More work {}'.format(x))     time.sleep(x)     print('Finished more work {}'.format(x))

start = now() new_loop = asyncio.new_event_loop() t = Thread(target=start_loop, args=(new_loop,)) t.start() print('TIME: {}'.format(time.time() - start))

asyncio.run_coroutine_threadsafe(do_some_work(6), new_loop) asyncio.run_coroutine_threadsafe(do_some_work(4), new_loop)


上述的例子,主线程中创建一个new\_loop,然后在另外的子线程中开启一个无限事件循环。主线程通过run\_coroutine\_threadsafe新注册协程对象。这样就能在子线程中进行事件循环的并发操作,同时主线程又不会被block。一共执行的时间大概在6s左右。  
**run\_in\_executor**



import time import asyncio

async def main():     print(f'{time.ctime()} Hello')     await asyncio.sleep(1.0)     print(f'{time.ctime()} Goodbye')     loop.stop()

def blocking():  # 1     time.sleep(0.5)  # 2     print(f'{time.ctime()} Hello from a thread!')

收集整理了一份《2024年最新Python全套学习资料》免费送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。 img img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Python知识点,真正体系化!

了解详情:docs.qq.com/doc/DSnl3ZG…