关于 Python event loop

2021-02-03 16:46:23 +08:00
 RandomAccess

有这样一个协程

async def function(**kwargs):
    session = aiohttp.ClientSession()
    response = await session.request(**kwargs)
    pass

如果使用 asyncio.run(function(arg=bala))和 new_event_loop 会出现

RuntimeError: Timeout context manager should be used inside a task

使用

loop = asyncio.get_event_loop()
loop.run_until_complete(function(arg=bala))
loop.close()

不会出现上下文管理的问题 这两者的区别在哪里,翻了半天官方文档和 csdn 的转载文实在无法理解 TAT

2723 次点击
所在节点    Python
14 条回复
Lycnir
2021-02-03 16:59:20 +08:00
python3.7 正常
cissoid
2021-02-03 17:00:23 +08:00
翻翻官方文档,

asyncio.run:
This function cannot be called when another asyncio event loop is running in the same thread.
linw1995
2021-02-03 17:23:30 +08:00
应该是因为你把同个 ClientSession 用在两个不同的 eventloop 上。asyncio.run 跑完后,event_loop 会关闭掉。在新的 event_loop 跑的时候,因为用的同一个 ClientSession,它用了上一个关闭了的 event_loop,导致这个异常抛出。

你这个是旧版的 aiohttp 吧,新版的 async_timeout 不会出现这个问题。
RandomAccess
2021-02-03 17:59:06 +08:00
DeprecationWarning: The object should be created within an async function


看样子应该是 session 在类实例化时在普通函数中创建的产生的问题,一个包含异步函数的对象在只能在 async 函数中创建,包括自己封装的类
ClericPy
2021-02-03 21:06:15 +08:00
class TimerContext(BaseTimerContext):
""" Low resolution timeout context manager """

def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
raise RuntimeError(
"Timeout context manager should be used " "inside a task"
)


老问题了, 所以我现在用到 aiohttp 尽量不用 asyncio.run, 因为它一定新建一个 loop, 然而 Session 的默认 Timeout 对象却是在协程外面初始化的, 导致两个循环不一致, 见下面源码

https://github.com/aio-libs/aiohttp/blob/742a8b6d09b2623670ddede838c913d2a8a4d89e/aiohttp/client.py#L161

据说 aiohttp 4.0 以后会好, 但是 4.0 一直发不出来

想解决的话, 简单的就是暂时别用 asyncio.run. 或者在协程函数里 import 它, 或者手动指定 Timeout 试试
RandomAccess
2021-02-04 08:22:52 +08:00
@ClericPy 谢谢大佬
如果业务场景需要 session 在两个阻塞的事件循环中传递是不是不可行的
guochao
2021-02-04 10:48:44 +08:00
简单说 python 的 event loop 是一个单进程单线程单个 loop 执行 coroutine 的过程。asyncio.run 就是一个创建 loop 并且 run until complete 的过程,new event loop 又建了一个,没有必要。

解决办法就是让整个程序的入口是一个 async 函数,在 async 函数中配置程序,新建各种实例,启动应用。在__main__里面 asyncio.run(entrypoint())就行。然后在任何其他地方都不要 new_event_loop 或者 asyncio.run

如果要执行一个 coroutine 但是不等待结束,可以 asyncio.ensure_future 或者 3.7 以上的新 API asyncio.create_task,这两个函数都是在 get_event_loop()返回的 loop 上执行对应的函数,会返回一个 Task
如果要在一个 coroutine 中执行一个 coroutine 并且等待结束,那就直接 await 。
如果要执行一个同步过程,可以用 run_in_executor,返回一个 Future
RandomAccess
2021-02-04 12:18:35 +08:00
@guochao thanks
ClericPy
2021-02-04 20:30:21 +08:00
@RandomAccess
时间循环尽可能只用一个, 就算协程用的很熟悉的人, 也很少去多个线程上跑多个 loop
所以 Python 3.9 还是 3.10 之后, 很多内置的协程方法都去掉了 loop 参数, 默认都从 running loop 里面获取

你要做的是, 首先保证整个程序只留有一个事件循环(因为多个也没有意义, await 不是阻塞, 是等待), 然后在里面传递 session 对象就是合法的了

然后你说的阻塞, 协程实际上是非阻塞的设计, 你的阻塞可能是 await 关键词, 那个是等待不算阻塞. 所以你如果想并发开始多个任务, 可以把 "协程函数" (async def 声明的)执行获得的 "协程对象" 创建为 Task, 它就会开始执行但是又不会阻塞, 创建 Task 的方式有 asyncio.ensure_future(some_coro) 或者 asyncio.create_task(coro), 后者是 3.7 以后新增的
RandomAccess
2021-02-04 21:14:56 +08:00
@ClericPy 确实是应该一个进程或线程只使用一个事件循环,只是业务场景需要进行一次长 io 且逻辑复杂的操作 A 获取操作 B 所需要的必要参数,然后任务 B 去执行多并发的也是长 io 的操作,AB 有着前后顺序,逻辑上应当是阻塞的,但是各自内部操作是无序的长 io,想尝试多个事件循环是否适应这个场景或多个不同并发的协程有没有好的顺序执行方案
RandomAccess
2021-02-04 21:16:57 +08:00
@RandomAccess 操作 A 和 B 各自都是嵌套的协程
ClericPy
2021-02-04 21:30:20 +08:00
@RandomAccess
一样是一个事件循环来解决的, 先后顺序的阻塞用 await 或者协程队列都行
非要多线程或者非协程的, 走 run_in_executor
RandomAccess
2021-02-04 21:42:49 +08:00
@ClericPy thanks
RandomAccess
2021-02-04 21:50:22 +08:00
@ClericPy 对协程的理解不是很深刻,刚才想通 await 的等待交出执行权其实也可以在代码逻辑实现前后顺序,没必要复杂化多个事件循环

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/750982

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX