【问题标题】:"async with" within asyncio.Task not workingasyncio.Task 中的“async with”不起作用
【发布时间】:2021-09-03 17:14:52
【问题描述】:

我正在学习 python asyncio 并使用它们测试大量代码。
下面是我尝试使用 asyncio 和 aiohttp 订阅多个 Websocket 流的代码。

我不明白为什么当coro(item1, item2): 作为任务执行时,它没有进入async with ... 块。 (即打印了“A”,但没有打印“B”)。
谁能帮我理解这个原因?

(我已经得到了working code 但是,我只是想了解这背后的机制是什么。)

代码

import aiohttp
import asyncio
import json

async def coro(
               item1,
               item2):
    print("A")

    async with aiohttp.ClientSession() as session:
        async with session.ws_connect(url='URL') as ws:
            print("B")

            await asyncio.gather(ws.send_json(item1),
                                 ws.send_json(item2))
            print("C")

            async for msg in ws:
                print(msg)

async def ws_connect(item1,
                     item2):
    task = asyncio.create_task(coro(item1, item2))
    return task

async def main():

    item1 = {
        "method": "subscribe",
        "params": {'channel': "..."}
    }
    item2 = {
        "method": "subscribe",
        "params": {'channel': "..."}
    }

    ws_task = await ws_connect(item1, item2)
    print("D")

asyncio.run(main())

输出

D
A

【问题讨论】:

    标签: python asynchronous websocket python-asyncio aiohttp


    【解决方案1】:

    B 永远不会打印,因为您永远不会等待返回的任务,只有返回它的方法。

    微妙的错误在return task 后面跟着await ws_connect(item1, item2)。

    TL;DR; return await task.

    理解程序输出的关键是要知道异步事件循环中的上下文切换只能在少数地方发生,尤其是在await 表达式处。此时,事件循环可能暂停当前协程并继续另一个。

    首先,您创建一个 ws_connect 协程并立即等待它,这会强制事件循环暂停 main 并实际运行 ws_connect,因为没有其他要运行的东西。

    由于ws_connect 不包含任何允许上下文切换的点,coro() 函数实际上永远不会启动。

    create_task 唯一要做的就是将协程绑定到任务对象并将其添加到事件循环的队列中。但是你从不等待它,你只是将它作为任何普通的返回值返回。好的,现在ws_connect() 完成并且事件循环可以选择运行任何任务,它选择继续main 可能是因为它一直在等待ws_connect()。

    好的,main 打印 D 并返回。现在呢?

    asyncio.run 中有一些额外的await,这给了coro() 一个开始的机会 - 因此打印了 A(但仅在 D 之后)但什么也没有强制asyncio.run 等待coro(),因此当coro 通过async with 返回上下文循环时,run 完成并且程序退出,这使得coro() 未完成。

    如果您在print('D') 之后添加一个额外的await asyncio.sleep(1),循环将再次暂停main 至少一段时间并继续coro(),如果URL 正确,则会打印B。

    实际上,上下文切换稍微复杂一些,因为协程上的普通await 通常不会切换,除非执行确实需要阻塞 IO 或 await asyncio.sleep(0) 或 yield* 保证真正的上下文切换,而无需额外的阻塞。

    *yield 来自__await__ 方法。

    这里的教训很简单——永远不要从async 方法返回等待对象,它会导致这种错误。始终默认使用return await,如果返回的对象实际上不是可等待的(如return await some_string),最坏的情况会出现运行时错误,并且很容易被发现和修复。

    另一方面,从普通函数返回可等待对象是可以的,并使其表现得像函数是异步的。尽管在混合这两种方法时应该小心。就我个人而言,我更喜欢第一种方法,因为它将责任转移到函数的编写者身上,而不是用户会被警告 linter,这些 linter 通常会检测到未等待的 corountine 调用,但不会检测到返回的 awaitables。所以另一种解决方案是将ws_connect 设为普通函数,然后await ws_connect 中的await 将应用于返回值(=任务),而不是函数本身。

    【讨论】:

    • 我不知道的一件事是“... coro 通过async with...返回上下文循环” “上下文循环”是指事件吗?环形?为什么它将控制权交还给循环?(是因为session 是一个awaitable 对象吗?)
    • 是的,对不起,我的意思是事件循环。这是由于异步上下文管理器的工作方式,它们也有一对等待的__aenter__ 和__aexit__ async 方法。见PEP 492 async with syntax。你可以看到VAR = await aenter(mgr),它可以根据__aenter__ 的实现方式进行上下文切换。深入研究aiohttp,它会深入,但aiohttp/client.py:754 中有一个阻塞等待,向URL 发出实际请求,这是一个阻塞IO 调用,因为它肯定使用下面的套接字。
    • @koyamashinji 您可以使用像pdb 这样的调试器自己检查大部分事件循环,但这不是一个可读性很强的代码,并且一些异步内容隐藏在类似于async with 的语法糖后面
    • 我越深入代码,级别就越低,天哪,这太难理解了!无论如何,感谢您提供有用的答案和cmets,我会进一步研究。
    • @koyamashinji 是的,它不适合我们凡人;)关键是您从未等待协程,并且在程序退出之前它没有足够的机会完成。尽管异步方法是一种协作式多任务处理 - 您可以控制函数在哪里中断,但它们的调度仍然是不确定的,不能依赖。
    猜你喜欢
    • 2018-06-09
    • 2018-12-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-07
    相关资源
    最近更新 更多