PyQt5 和 asyncio:从永不完成的收益
问题描述
我正在尝试创建一个基于 PyQt5 和 asyncio 的新应用程序(使用 python 3.4,期待最终通过 async/await 升级到 3.5).我的目标是使用 asyncio,以便即使应用程序正在等待某些连接的硬件完成操作,GUI 也能保持响应.
I'm trying to create a new application based on PyQt5 and asyncio (with python 3.4, looking forward to eventually upgrade to 3.5 with async/await). My goal is to use asyncio so that the GUI stays responsive even when the application is waiting for some connected hardware to finish an operation.
在查看如何合并 Qt5 和 asyncio 的事件循环时,我发现了一个 邮件列表发帖,建议使用quamash.但是,在运行这个示例(未修改)时,
When looking how to merge the event loops of Qt5 and asyncio, I found a mailing list posting, suggesting to use quamash. However, when running this example (unmodified), the
yield from fut
似乎永远不会回来.我看到输出超时",所以计时器回调显然会触发,但 Future 无法唤醒等待方法.手动关闭窗口时,它告诉我有未完成的期货:
nevers seems to return. I see the output 'Timeout', so the timer callback obviously fires, but the Future fails to wake up the waiting method. When manually closing the window, it tells me that there are uncompleted futures:
Yielding until signal...
Timeout
Traceback (most recent call last):
File "pyqt_asyncio_list.py", line 26, in <module>
loop.run_until_complete(_go())
File "/usr/local/lib/python3.5/site-packages/quamash/__init__.py", line 291, in run_until_complete
raise RuntimeError('Event loop stopped before Future completed.')
RuntimeError: Event loop stopped before Future completed.
我在带有 python 3.5 的 Ubuntu 和带有 3.4 的 Windows 上对此进行了测试,两个平台上的行为相同.
I tested this on Ubuntu with python 3.5 and on Windows with 3.4, same behaviour on both platforms.
无论如何,由于这不是我实际尝试实现的目标,因此我还测试了一些其他代码:
Anyway, since this is not what I actually try to achieve, I tested some other code as well:
import quamash
import asyncio
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
@asyncio.coroutine
def op():
print('op()')
@asyncio.coroutine
def slow_operation():
print('clicked')
yield from op()
print('op done')
yield from asyncio.sleep(0.1)
print('timeout expired')
yield from asyncio.sleep(2)
print('second timeout expired')
def coroCallHelper(coro):
asyncio.ensure_future(coro(), loop=loop)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
def btnCallback(obj):
#~ loop.call_soon(coroCallHelper, slow_operation)
asyncio.ensure_future(slow_operation(), loop=loop)
print('btnCallback returns...')
btn = QPushButton('Button', self)
btn.resize(btn.sizeHint())
btn.move(50, 50)
btn.clicked.connect(btnCallback)
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Async')
self.show()
with quamash.QEventLoop(app=QApplication([])) as loop:
w = Example()
loop.run_forever()
#~ loop = asyncio.get_event_loop()
#~ loop.run_until_complete(slow_operation())
该程序应该显示一个带有按钮的窗口(它确实如此),该按钮调用 slow_operation() 而不会阻塞 GUI.运行此示例时,我可以随意单击按钮,因此 GUI 不会被阻塞.但是
The program is supposed to display a window with a button in it (which it does), with the button invoking slow_operation() without blocking the GUI. When running this example, I can click the button as often as I want, so the GUI is not blocked. But the
yield from asyncio.sleep(0.1)
从未通过,终端输出如下所示:
is never passed and the terminal output looks like this:
btnCallback returns...
clicked
op()
op done
btnCallback returns...
clicked
op()
op done
这次关闭窗口没有抛出异常.如果我直接用它运行事件循环,slow_operation() 函数基本上可以工作:
There is no exception thrown when I close the window this time. The slow_operation() function basically works if I directly run the event loop with it:
#~ with quamash.QEventLoop(app=QApplication([])) as loop:
#~ w = Example()
#~ loop.run_forever()
loop = asyncio.get_event_loop()
loop.run_until_complete(slow_operation())
现在,有两个问题:
一般来说,这是实现冗长操作与 GUI 分离的明智方法吗?我的意图是按钮回调将协程调用发布到事件循环(有或没有额外的嵌套级别,参见 coroCallHelper()),然后在其中安排和执行.我不需要单独的线程,因为实际上只有 I/O 需要时间,没有实际处理.
Is this a sensible way to achieve decoupling of lengthy operations from the GUI, generally? My intention is that the button callback posts the coroutine call to the event loop (with or without an additional level of nesting, cf. coroCallHelper()), where it is then scheduled and executed. I don't need separate threads, as it is really only I/O that takes time, no actual processing.
如何解决此问题?
谢谢,菲利普
解决方案
好的,这是 SO 的一个优点:写下一个问题会让你重新思考一切.不知怎的,我想通了:
Ok, that's one plus of SO: Writing down a question makes you think again about everything. Somehow I just figured it out:
再次查看 quamash repo 中的示例,我发现要使用的事件循环是得到的有点不同:
Looking again at the example from the quamash repo, I found that the event loop to use is obtained somewhat differently:
app = QApplication(sys.argv)
loop = QEventLoop(app)
asyncio.set_event_loop(loop) # NEW must set the event loop
# ...
with loop:
loop.run_until_complete(master())
关键似乎是 asyncio.set_event_loop()
.还需要注意的是,这里提到的 QEventLoop
来自 quamash 包,而不是来自 Qt5.所以我的例子现在看起来像这样:
The key seems to be the asyncio.set_event_loop()
. It is also important to note that the QEventLoop
mentioned there is the one from the quamash package, NOT from Qt5. So my example now looks like this:
import sys
import quamash
import asyncio
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
@asyncio.coroutine
def op():
print('op()')
@asyncio.coroutine
def slow_operation():
print('clicked')
yield from op()
print('op done')
yield from asyncio.sleep(0.1)
print('timeout expired')
yield from asyncio.sleep(2)
print('second timeout expired')
loop.stop()
def coroCallHelper(coro):
asyncio.ensure_future(coro(), loop=loop)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
def btnCallback(obj):
#~ loop.call_soon(coroCallHelper, slow_operation)
asyncio.ensure_future(slow_operation(), loop=loop)
print('btnCallback returns...')
btn = QPushButton('Button', self)
btn.resize(btn.sizeHint())
btn.move(50, 50)
btn.clicked.connect(btnCallback)
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Async')
self.show()
app = QApplication(sys.argv)
loop = quamash.QEventLoop(app)
asyncio.set_event_loop(loop) # NEW must set the event loop
with loop:
w = Example()
w.show()
loop.run_forever()
print('Coroutine has ended')
它现在正常工作":
btnCallback returns...
clicked
op()
op done
timeout expired
second timeout expired
Coroutine has ended
也许这对其他人有一些帮助.至少我对此很满意;)当然,仍然欢迎对一般模式发表评论!
Maybe this is of some help for others. I'm happy with it at least ;) Comments on the general pattern are still welcome, of course!
附录:请注意,如果 quamash 被 asyncqt 替换,这适用于最新的 Python 版本,直到 Python 3.7.x.然而,在 Python 3.8 中使用相同的代码会导致 @coroutine
装饰器生成 RuntimeWarning
并最终以 RuntimeError: no running event loop
失败在 asyncio.sleep()
中.也许其他人知道要改变什么才能让它再次工作.可能只是 asyncqt 还不兼容 Python 3.8.
Addendum: Please note that this works with recent Python versions up to Python 3.7.x if quamash is replaced by asyncqt. However, using the same code with Python 3.8 causes the @coroutine
decorators to generate RuntimeWarning
s and eventually fails with a RuntimeError: no running event loop
in asyncio.sleep()
. Maybe someone else knows what to change to get this working again. It might just be that asyncqt is not yet compatible with Python 3.8.
问候,菲利普
相关文章