問題描述
我正在嘗試創建一個基于 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.
問候,菲利普
這篇關于PyQt5 和 asyncio:從永不完成的收益的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網!