我正在构建一个工人类,该类使用asyncio连接到外部事件流。它是单个流,但是有多个使用者可以启用它。目标是仅在一个或多个消费者需要时才保持连接。
我的要求如下:
听起来很简单。但是,启动顺序引起了我的问题,因为它本身是异步的。因此,假设此接口:
class Stream:
async def start(self, *, timeout=DEFAULT_TIMEOUT):
pass
async def stop(self):
pass
我有以下几种情况:
方案1 -启动时发生异常
方案2 -部分异步取消
方案3 -完全异步取消
我很难涵盖所有情况,而又没有任何竞争条件,也没有一团乱麻的Future或Event对象的意粉。
这是尝试写start()
的尝试。完成启动序列时,它依靠_worker()
设置名为asyncio.Event
的{{1}}:
self._worker_ready
该似乎可以工作,但它似乎太复杂了,很难测试:工人有许多async def start(self, timeout=None):
assert not self.closing
if not self._task:
self._task = asyncio.ensure_future(self._worker())
# Wait until worker is ready, has failed, or timeout triggers
try:
self._waiting_start += 1
wait_ready = asyncio.ensure_future(self._worker_ready.wait())
done, pending = await asyncio.wait(
[self._task, wait_ready],
return_when=asyncio.FIRST_COMPLETED, timeout=timeout
)
except asyncio.CancelledError:
wait_ready.cancel()
if self._waiting_start == 1:
self.closing = True
self._task.cancel()
with suppress(asyncio.CancelledError):
await self._task # let worker shutdown
raise
finally:
self._waiting_start -= 1
# worker failed to start - either throwing or timeout triggering
if not self._worker_ready.is_set():
self.closing = True
self._task.cancel()
wait_ready.cancel()
try:
await self._task # let worker shutdown
except asyncio.CancelledError:
raise FeedTimeoutError('stream failed to start within %ss' % timeout)
else:
assert False, 'worker must propagate the exception'
分,如果我想尽一切可能的话会导致爆炸性爆炸取消点和执行命令。
我需要更好的方法。因此,我想知道:
答案 0 :(得分:2)
您的要求听起来很合理。我会尝试通过将start
替换为将来的字符(在这种情况下为任务)来简化Event
,使用它既可以等待启动完成,也可以传播过程中发生的异常(如果有) 。像这样:
class Stream:
async def start(self, *, timeout=DEFAULT_TIMEOUT):
loop = asyncio.get_event_loop()
if self._worker_startup_task is None:
self._worker_startup_task = \
loop.create_task(self._worker_startup())
self._add_user()
try:
await asyncio.shield(asyncio.wait_for(
self._worker_startup_task, timeout))
except:
self._rm_user()
raise
async def _worker_startup(self):
loop = asyncio.get_event_loop()
await asyncio.sleep(1) # ...
self._worker_task = loop.create_task(self._worker())
在此代码中,工作程序启动与工作程序协程分离,并且也移至单独的任务。可以等待这个单独的任务,并且不需要专用的Event
,但是更重要的是,它允许方案1和2由相同的代码处理。即使有人取消了第一个消费者,工作人员启动任务也不会被取消-取消只是意味着等待它的消费者减少了。
因此,在取消消费者的情况下,await self._worker_startup_task
对于其他消费者来说也很好,而在工作程序启动时发生实际异常的情况下,所有其他服务员将看到相同的异常,因为任务已完成。 / p>
方案3应该自动运行,因为无论出于何种原因,我们总是取消用户无法再看到的启动。如果消费者因为启动本身失败而走了,那么self._worker_startup_task
将已经完成(有例外),并且取消操作将是无效的。如果是因为所有使用者都在等待启动时被取消,那么self._worker_startup_task.cancel()
将按照方案3的要求取消启动顺序。
其余代码看起来像这样(未经测试):
def __init__(self):
self._users = 0
self._worker_startup = None
def _add_user(self):
self._users += 1
def _rm_user(self):
self._users -= 1
if self._users:
return
self._worker_startup_task.cancel()
self._worker_startup_task = None
if self._worker_task is not None:
self._worker_task.cancel()
self._worker_task = None
async def stop(self):
self._rm_user()
async def _worker(self):
# actual worker...
while True:
await asyncio.sleep(1)
答案 1 :(得分:1)
通过先前的测试并整合了@ user4815162342的建议,我想出了一个可重用的解决方案:
st = SharedTask(test())
task1 = asyncio.ensure_future(st.wait())
task2 = asyncio.ensure_future(st.wait(timeout=15))
task3 = asyncio.ensure_future(st.wait())
这做对了:task2在15秒后取消自身。除非所有任务都被取消,否则取消任务对test()
无效。在这种情况下,要取消的最后一个任务将手动取消test()
并等待取消处理完成。
如果通过了协程,则仅在第一个任务开始等待时安排它。
最后,等待共享任务完成后,只是立即产生其结果(似乎很明显,但初始版本却没有)。
import asyncio
from contextlib import suppress
class SharedTask:
__slots__ = ('_clients', '_task')
def __init__(self, task):
if not (asyncio.isfuture(task) or asyncio.iscoroutine(task)):
raise TypeError('task must be either a Future or a coroutine object')
self._clients = 0
self._task = task
@property
def started(self):
return asyncio.isfuture(self._task)
async def wait(self, *, timeout=None):
self._task = asyncio.ensure_future(self._task)
self._clients += 1
try:
return await asyncio.wait_for(asyncio.shield(self._task), timeout=timeout)
except:
self._clients -= 1
if self._clients == 0 and not self._task.done():
self._task.cancel()
with suppress(asyncio.CancelledError):
await self._task
raise
def cancel(self):
if asyncio.iscoroutine(self._task):
self._task.close()
elif asyncio.isfuture(self._task):
self._task.cancel()
重新引发任务异常取消(在注释中提到)是有意的。它允许这种模式:
async def my_task():
try:
await do_stuff()
except asyncio.CancelledError as exc:
await flush_some_stuff() # might raise an exception
raise exc
客户端可以取消共享任务并处理可能导致的异常,无论my_task
是否包裹在SharedTask
中,其工作原理都相同。