PyQt5 和 asyncio:从永不结束中产生

2024-01-06

我正在尝试创建一个基于 PyQt5 和 asyncio 的新应用程序(使用 python 3.4,期待最终使用 async/await 升级到 3.5)。我的目标是使用 asyncio,以便即使应用程序正在等待某些连接的硬件完成操作,GUI 也能保持响应。

在寻找如何合并 Qt5 和 asyncio 的事件循环时,我发现了一个邮件列表张贴 https://riverbankcomputing.com/pipermail/pyqt/2014-August/034638.html,建议使用quamash https://github.com/harvimt/quamash。然而,当运行这个例子(未修改)时,

yield from fut

尼韦尔似乎又回来了。我看到输出“超时”,因此计时器回调显然已触发,但 Future 无法唤醒等待方法。当手动关闭窗口时,它告诉我还有未完成的 future:

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())

该程序应该显示一个窗口,其中有一个按钮(确实如此),该按钮调用 Slow_operation() 而不阻塞 GUI。运行此示例时,我可以根据需要多次单击按钮,因此 GUI 不会被阻止。但是

yield from asyncio.sleep(0.1)

从未通过,终端输出如下所示:

btnCallback returns...
clicked
op()
op done
btnCallback returns...
clicked
op()
op done

这次关闭窗口时没有抛出异常。如果我直接用它运行事件循环,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())

现在,有两个问题:

  1. 一般来说,这是实现冗长操作与 GUI 解耦的明智方法吗?我的意图是按钮回调将协程调用发布到事件循环(有或没有额外的嵌套级别,参见 coroCallHelper()),然后在其中调度和执行。我不需要单独的线程,因为实际上只有 I/O 需要时间,没有实际的处理。

  2. 我该如何解决这个问题?

谢谢, 菲利普


好吧,这是 SO 的一个优点:写下一个问题会让你重新思考一切。不知怎的,我刚刚想通了:

再次查看来自的示例夸玛什回购 https://github.com/harvimt/quamash,我发现要使用的事件循环的获取方式有些不同:

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')

现在它“正常工作”了:

btnCallback returns...
clicked
op()
op done
timeout expired
second timeout expired
Coroutine has ended

也许这对其他人有一些帮助。至少我对此很满意;) 当然,仍然欢迎对总体模式提出评论!

Addendum:请注意,如果将 quamash 替换为 asyncqt,则这适用于最新的 Python 版本(最高可达 Python 3.7.x)。但是,在 Python 3.8 中使用相同的代码会导致@coroutine生成的装饰器RuntimeWarnings 并最终失败RuntimeError: no running event loop in asyncio.sleep()。也许其他人知道要更改什么才能使其再次正常工作。可能只是 asyncqt 尚未与 Python 3.8 兼容。

问候, 菲利普

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

PyQt5 和 asyncio:从永不结束中产生 的相关文章

随机推荐