为什么即使遇到`RuntimeError`,异步事件循环有时也可以完成任务?

时间:2018-11-07 16:45:05

标签: python python-asyncio

我一直在使用Python的asyncio。我想我现在有一个合理的理解。但是以下行为使我感到困惑。

test.py

from threading import Thread
import asyncio

async def wait(t):
    await asyncio.sleep(t)
    print(f'waited {t} sec')

def run(loop):
    loop.run_until_complete(wait(2))

loop = asyncio.get_event_loop()
t = Thread(target=run, args=(loop,))
t.start()
loop.run_until_complete(wait(1))
t.join()

此代码是错误的。 我知道。事件循环在运行时无法运行,并且通常也不安全。

我的问题:为什么wait(1) 有时仍能完成其工作?

这是两次连续运行的输出:

>>> py test.py
... Traceback (most recent call last):
...   File "test.py", line 14, in <module>
...     loop.run_until_complete(wait(1))
...   File "C:\Python\Python37\lib\asyncio\base_events.py", line 555, in run_until_complete
...     self.run_forever()
...   File "C:\Python\Python37\lib\asyncio\base_events.py", line 510, in run_forever
... 
...     raise RuntimeError('This event loop is already running')
... RuntimeError: This event loop is already running
... waited 2 sec

>>> py test.py
... Traceback (most recent call last):
...   File "test.py", line 14, in <module>
...     loop.run_until_complete(wait(1))
...   File "C:\Python\Python37\lib\asyncio\base_events.py", line 555, in run_until_c
... omplete
...     self.run_forever()
...   File "C:\Python\Python37\lib\asyncio\base_events.py", line 510, in run_forever
... 
...     raise RuntimeError('This event loop is already running')
... RuntimeError: This event loop is already running
... waited 1 sec
... waited 2 sec

第一次运行的行为是我所期望的-主线程失败,但是事件循环仍然运行wait(2)以在线程t中完成。

第二步令人费解,wait(1)已经抛出RuntimeError怎么做?我猜想这与线程同步和事件循环的非线程安全性有关。但是我不知道这是如何工作的。

2 个答案:

答案 0 :(得分:1)

每个线程都会抛出异常。。在与事件循环不同的线程中引发运行时错误。无论如何,事件循环都会继续执行。

wait(1)有时可以完成任务,因为您会很幸运。 asyncio循环内部数据结构无法防止使用线程导致的竞争情况(这就是为什么应该使用specific thread-support methods的原因)。但是,竞争条件的性质取决于事件的确切顺序,并且每次运行程序时,顺序都可以更改,具体取决于操作系统当时在做什么。

run_until_complete()方法 first 调用asyncio.ensure_task(),将协程添加到附加了“完成”回调的任务队列中,该回调将再次停止事件循环,然后调用{ {1}}。当协程返回时,回调将停止循环。 loop.run_forever()调用会在此处抛出loop.run_forever()

从线程执行此操作时,任务将添加到附加到循环的双端队列对象中,如果在适当的时候发生(例如,当运行中的循环不忙于清空队列时),则运行中的循环进入即使RuntimeError调用引发了异常,主线程也会找到并执行它。

所有这些都依赖于实现细节。不同版本的Python在这里可能会表现出不同的行为,并且如果您安装替代循环(例如uvloop),几乎肯定还会有不同的行为。

如果要从其他线程安排协程,请使用asyncio.run_coroutine_threadsafe();会的:

loop.run_forever()

以上操作实际上并未完成from threading import Thread import asyncio async def wait(t): print(f'going to wait {t} seconds') await asyncio.sleep(t) print(f'waited {t} sec') def run(loop): asyncio.run_coroutine_threadsafe(wait(2), loop) loop = asyncio.get_event_loop() t = Thread(target=run, args=(loop,)) t.start() loop.run_until_complete(wait(1)) t.join() 协程,因为wait(2)协程正在与wait(1)一起运行,因此其回调在2秒等待结束之前再次停止了循环。但是协程实际上是启动的:

loop.run_until_complete()

但是如果您使主线程协程花费的时间更长(例如going to wait 1 seconds going to wait 2 seconds waited 1 sec ),那么从线程调度的协程也将完成。在关闭循环之前,您需要做更多的工作以确保不再有计划在循环中运行的未决任务。

答案 1 :(得分:1)

哦...没关系。我阅读了asyncio的代码,并弄清楚了。实际上很简单。

run_until_complete调用ensure_future(future, loop=self) 之前会检查self.is_running()(在run_forever中完成)。由于循环已经在运行,因此可以在引发RuntimeError之前拾取任务。当然,并非总是由于比赛条件而发生。