我正在尝试创建一个基于Python的CLI,它通过websockets与Web服务进行通信。我遇到的一个问题是CLI间歇性地无法处理对Web服务发出的请求。查看来自Web服务的日志,我可以看到问题是由于这些请求经常在套接字关闭的同时(甚至之后)发生的事实引起的:
2016-09-13 13:28:10,930 [22 ] INFO DeviceBridge - Device bridge has opened
2016-09-13 13:28:11,936 [21 ] DEBUG DeviceBridge - Device bridge has received message
2016-09-13 13:28:11,937 [21 ] DEBUG DeviceBridge - Device bridge has received valid message
2016-09-13 13:28:11,937 [21 ] WARN DeviceBridge - Unable to process request: {"value": false, "path": "testcube.pwms[0].enabled", "op": "replace"}
2016-09-13 13:28:11,936 [5 ] DEBUG DeviceBridge - Device bridge has closed
在我的CLI中,我定义了一个类CommunicationService
,它负责处理与Web服务的所有直接通信。在内部,它使用websockets
包来处理通信,它本身建立在asyncio
之上。
CommunicationService
包含以下发送请求的方法:
def send_request(self, request: str) -> None:
logger.debug('Sending request: {}'.format(request))
asyncio.ensure_future(self._ws.send(request))
...其中ws
是先前在另一种方法中打开的websocket:
self._ws = await websockets.connect(websocket_address)
我想要的是能够等待asyncio.ensure_future
返回的未来,并在必要时暂停一会儿,以便在websocket关闭之前让Web服务有时间处理请求。
但是,由于send_request
是一种同步方法,因此它不能只是await
这些未来。使它异步是没有意义的,因为没有什么可以等待它返回的协程对象。我也无法使用loop.run_until_complete
,因为循环在调用时已经在运行。
我发现有人描述的问题与我mail.python.org处的问题非常相似。在该线程中发布的解决方案是在循环已经运行的情况下使函数返回coroutine对象:
def aio_map(coro, iterable, loop=None):
if loop is None:
loop = asyncio.get_event_loop()
coroutines = map(coro, iterable)
coros = asyncio.gather(*coroutines, return_exceptions=True, loop=loop)
if loop.is_running():
return coros
else:
return loop.run_until_complete(coros)
这对我来说是不可能的,因为我正在使用PyRx(反应框架的Python实现),send_request
只被称为Rx observable的订阅者,这意味着返回值得到丢弃,我的代码无法使用:
class AnonymousObserver(ObserverBase):
...
def _on_next_core(self, value):
self._next(value)
另一方面,我不确定这是asyncio
通常会遇到的某种问题,还是我没有得到它,但我和#39;发现使用它非常令人沮丧。在C#中(例如),我需要做的就是以下内容:
void SendRequest(string request)
{
this.ws.Send(request).Wait();
// Task.Delay(500).Wait(); // Uncomment If necessary
}
同时,asyncio
"等待"无益的只是返回另一个我被迫丢弃的协程。
更新
我找到了解决此问题的方法似乎有效。我有一个异步回调,它在执行命令之后和CLI终止之前执行,所以我只是从它改变了它......
async def after_command():
await comms.stop()
......对此:
async def after_command():
await asyncio.sleep(0.25) # Allow time for communication
await comms.stop()
我仍然乐意接受这个问题的任何答案,以备将来参考。在其他情况下,我可能无法依赖这样的解决方法,我仍然认为在send_request
内执行延迟会更好,以便CommunicationService
的客户不必关心自己有时间问题。
关于文森特的问题:
你的循环是在另一个线程中运行,还是由某些回调调用send_request?
所有内容都在同一个线程中运行 - 它被回调调用。所发生的是我定义所有命令以使用异步回调,并且当执行时,其中一些命令将尝试向Web服务发送请求。由于他们是异步的,因此他们不会这样做,直到他们通过调用CLI顶层的loop.run_until_complete
来执行 - 这意味着循环在他们和&# #39;在执行中途并发出此请求(通过间接调用send_request
)。
更新2
这是基于Vincent提出的添加"完成"的解决方案。回调。
新的布尔字段_busy
已添加到CommunicationService
,以表示是否正在进行通信活动。
CommunicationService.send_request
以在发送请求之前将_busy
设置为true,然后在完成后向_ws.send
提供回复以重置_busy
:
def send_request(self, request: str) -> None:
logger.debug('Sending request: {}'.format(request))
def callback(_):
self._busy = False
self._busy = True
asyncio.ensure_future(self._ws.send(request)).add_done_callback(callback)
CommunicationService.stop
现已实现,等待此标志在进行之前设置为false:
async def stop(self) -> None:
"""
Terminate communications with TestCube Web Service.
"""
if self._listen_task is None or self._ws is None:
return
# Wait for comms activity to stop.
while self._busy:
await asyncio.sleep(0.1)
# Allow short delay after final request is processed.
await asyncio.sleep(0.1)
self._listen_task.cancel()
await asyncio.wait([self._listen_task, self._ws.close()])
self._listen_task = None
self._ws = None
logger.info('Terminated connection to TestCube Web Service')
这似乎也有效,至少这种方式所有通信时序逻辑都封装在CommunicationService
类中,应该如此。
更新3
基于Vincent提案的更好的解决方案。
而不是self._busy
我们有self._send_request_tasks = []
。
新send_request
实施:
def send_request(self, request: str) -> None:
logger.debug('Sending request: {}'.format(request))
task = asyncio.ensure_future(self._ws.send(request))
self._send_request_tasks.append(task)
新stop
实施:
async def stop(self) -> None:
if self._listen_task is None or self._ws is None:
return
# Wait for comms activity to stop.
if self._send_request_tasks:
await asyncio.wait(self._send_request_tasks)
...
答案 0 :(得分:2)
您可以使用set
任务:
self._send_request_tasks = set()
使用ensure_future
计划任务并使用add_done_callback
进行清理:
def send_request(self, request: str) -> None:
task = asyncio.ensure_future(self._ws.send(request))
self._send_request_tasks.add(task)
task.add_done_callback(self._send_request_tasks.remove)
等待set
任务完成:
async def stop(self):
if self._send_request_tasks:
await asyncio.wait(self._send_request_tasks)
答案 1 :(得分:1)
鉴于您不在异步函数中,您可以使用yield from
关键字自己有效地实现await
。以下代码将阻止,直到将来返回:
def send_request(self, request: str) -> None:
logger.debug('Sending request: {}'.format(request))
future = asyncio.ensure_future(self._ws.send(request))
yield from future.__await__()