【发布时间】:2019-12-13 01:16:45
【问题描述】:
我有一个名为cond 的asyncio.Condition。我想等它,但只能等那么久才放弃。由于asyncio.Condition.wait 不会超时,因此无法直接执行此操作。 The docs 声明 asyncio.wait_for 应该用于包装并提供超时:
asyncio.wait_for() 函数可用于在超时后取消任务。
因此,我们得出以下解决方案:
async def coro():
print("Taking lock...")
async with cond:
print("Lock acquired.")
print("Waiting!")
await asyncio.wait_for(cond.wait(), timeout=999)
print("Was notified!")
print("Lock released.")
现在假设coro 本身在运行五秒后被取消。这会在wait_for 中抛出CancelledError,这会在重新引发错误之前取消cond.wait。然后错误传播到coro,由于async with 块,隐式尝试释放cond 中的锁。但是,当前没有持有锁; cond.wait 已被取消,但没有机会处理该取消并重新获取锁。因此,我们得到一个丑陋的异常,如下所示:
Taking lock...
Lock acquired.
Waiting!
ERROR:asyncio:Task exception was never retrieved
future: <Task finished coro=<coro() done, defined at [REDACTED]> exception=RuntimeError('Lock is not acquired.',)>
Traceback (most recent call last):
[REDACTED], in coro
await asyncio.wait_for(cond.wait(), timeout=999)
[REDACTED], in wait_for
yield from waiter
concurrent.futures._base.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
[REDACTED], in coro
print("Was notified!")
[REDACTED], in coro
res = func(*args, **kw)
[REDACTED], in __aexit__
self.release()
[REDACTED], in release
raise RuntimeError('Lock is not acquired.')
RuntimeError: Lock is not acquired.
换句话说,在处理CancelledError 时,coro 在尝试释放未持有的锁时引发了RuntimeError。堆栈跟踪显示print("Was notified!") 行的原因是因为这是有问题的async with 块的最后一行。
这感觉不是我能解决的问题;我开始怀疑这是图书馆本身的一个错误。但是,我想不出任何方法来避免这个问题或创建一个解决方法,所以任何想法都会受到赞赏。
在写这个问题并进一步调查时,我在 Python 错误跟踪器上偶然发现了类似的问题,最后检查了 asyncio 源代码,并确定这实际上是 asyncio 本身的错误。
我已将它提交给问题跟踪器here,供有同样问题的人使用,并用我创建的解决方法回答了我自己的问题。
编辑:应 ParkerD 的要求,这里是产生上述问题的完整可运行示例:
编辑 2: 更新示例以使用 Python 3.7+ 中的新 asyncio.run 和 asyncio.create_task 功能
import asyncio
async def coro():
cond = asyncio.Condition()
print("Taking lock...")
async with cond:
print("Lock acquired.")
print("Waiting!")
await asyncio.wait_for(cond.wait(), timeout=999)
print("Was notified!")
print("Lock released.")
async def cancel_after_5(c):
task = asyncio.create_task(c)
await asyncio.sleep(5)
task.cancel()
await asyncio.wait([task])
asyncio.run(cancel_after_5(coro()))
【问题讨论】:
-
你能贴出调用
coro的代码吗?当我运行并取消它时,一切似乎都正常 -
@ParkerD 我添加了一个可运行的示例。
-
所以,如果你使用
asyncio.run(cancel_after_5(coro()))而不是asyncio.get_event_loop().run_until_complete(cancel_after_5(coro()))对我来说效果很好,这很有趣。 -
@ParkerD 刚刚检查过了。看起来问题是在事件循环之外分配
cond = asyncio.Condition()。如果你使用asyncio.run,它会创建自己的事件循环,这不是cond看到的那个,所以你会得到一个got Future attached to a different loop异常而不是预期的CancelledError或RuntimeError: Lock is not acquired。出于某种原因,除非您明确捕获并打印它,否则这是无声的。我将编辑帖子以使用asyncio.run给出一个可运行的示例。
标签: python python-asyncio