PyQt5 和 asyncio:yield from never finishes
PyQt5 and asyncio: yield from never finishes
我正在尝试创建一个基于 PyQt5 和 asyncio 的新应用程序(使用 python 3.4,期待最终升级到 async/await 3.5)。我的目标是使用 asyncio,以便 GUI 保持响应,即使应用程序正在等待某些连接的硬件完成操作。
在寻找如何合并Qt5和asyncio的事件循环时,我找到了一个mailing list posting, suggesting to use quamash。但是,当 运行 宁此示例(未修改)时,
yield from fut
似乎从来没有 return。我看到输出 'Timeout',所以定时器回调显然触发了,但 Future 未能唤醒等待方法。当手动关闭window时,它告诉我还有未完成的期货:
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.
我在 Ubuntu 上用 python 3.5 和 Windows 上用 3.4 测试了这个,两个平台上的行为相同。
无论如何,由于这不是我实际尝试实现的目标,我也测试了一些其他代码:
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())
该程序应该显示一个 window,里面有一个按钮(确实如此),按钮调用 slow_operation() 而不会阻塞 GUI。 运行在这个例子中,我可以随时点击按钮,所以 GUI 不会被阻塞。但是
yield from asyncio.sleep(0.1)
从未通过,终端输出如下所示:
btnCallback returns...
clicked
op()
op done
btnCallback returns...
clicked
op()
op done
这次关闭window没有异常。如果我直接 运行 事件循环, slow_operation() 函数基本上可以工作:
#~ 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 需要时间,没有实际处理。
我该如何解决这个问题?
谢谢,
菲利普
好的,这是 SO 的一个优点:写下一个问题会让您重新思考一切。不知怎的,我刚刚弄明白了:
再次查看 quamash repo 中的示例,我发现要使用的事件循环的获取方式有所不同:
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。所以我的例子现在看起来像这样:
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')
现在'just works':
btnCallback returns...
clicked
op()
op done
timeout expired
second timeout expired
Coroutine has ended
也许这对其他人有帮助。至少我很满意 ;)
当然,仍然欢迎对一般模式发表评论!
附录:请注意,如果 quamash 被 asyncqt 替换,这适用于最新的 Python 版本,最高可达 Python 3.7.x .但是,在 Python 3.8 中使用相同的代码会导致 @coroutine
装饰器生成 RuntimeWarning
并最终失败并在 asyncio.sleep()
中显示 RuntimeError: no running event loop
。也许其他人知道要改变什么才能让它再次工作。可能只是 asyncqt 还不兼容 Python 3.8.
此致,
菲利普
我正在尝试创建一个基于 PyQt5 和 asyncio 的新应用程序(使用 python 3.4,期待最终升级到 async/await 3.5)。我的目标是使用 asyncio,以便 GUI 保持响应,即使应用程序正在等待某些连接的硬件完成操作。
在寻找如何合并Qt5和asyncio的事件循环时,我找到了一个mailing list posting, suggesting to use quamash。但是,当 运行 宁此示例(未修改)时,
yield from fut
似乎从来没有 return。我看到输出 'Timeout',所以定时器回调显然触发了,但 Future 未能唤醒等待方法。当手动关闭window时,它告诉我还有未完成的期货:
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.
我在 Ubuntu 上用 python 3.5 和 Windows 上用 3.4 测试了这个,两个平台上的行为相同。
无论如何,由于这不是我实际尝试实现的目标,我也测试了一些其他代码:
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())
该程序应该显示一个 window,里面有一个按钮(确实如此),按钮调用 slow_operation() 而不会阻塞 GUI。 运行在这个例子中,我可以随时点击按钮,所以 GUI 不会被阻塞。但是
yield from asyncio.sleep(0.1)
从未通过,终端输出如下所示:
btnCallback returns...
clicked
op()
op done
btnCallback returns...
clicked
op()
op done
这次关闭window没有异常。如果我直接 运行 事件循环, slow_operation() 函数基本上可以工作:
#~ 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 需要时间,没有实际处理。
我该如何解决这个问题?
谢谢, 菲利普
好的,这是 SO 的一个优点:写下一个问题会让您重新思考一切。不知怎的,我刚刚弄明白了:
再次查看 quamash repo 中的示例,我发现要使用的事件循环的获取方式有所不同:
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。所以我的例子现在看起来像这样:
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')
现在'just works':
btnCallback returns...
clicked
op()
op done
timeout expired
second timeout expired
Coroutine has ended
也许这对其他人有帮助。至少我很满意 ;) 当然,仍然欢迎对一般模式发表评论!
附录:请注意,如果 quamash 被 asyncqt 替换,这适用于最新的 Python 版本,最高可达 Python 3.7.x .但是,在 Python 3.8 中使用相同的代码会导致 @coroutine
装饰器生成 RuntimeWarning
并最终失败并在 asyncio.sleep()
中显示 RuntimeError: no running event loop
。也许其他人知道要改变什么才能让它再次工作。可能只是 asyncqt 还不兼容 Python 3.8.
此致, 菲利普