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

当寻找如何合并Qt5和asyncio的事件循环时,我发现了一个mailing list posting,建议使用quamash。但是,在运行此示例(未修改)时,

yield from fut

从来没有回来。我看到输出“Timeout”,因此显然触发了计时器回调,但是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.

我在python 3.5的Ubuntu和3.4的Windows上对此进行了测试,两个平台上的行为相同。

无论如何,由于这并不是我真正想要实现的目标,因此我也测试了一些其他代码:
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())

现在,有两个问题:
  • 通常,这是将冗长的操作与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')
    

    现在它“正常工作”:
    btnCallback returns...
    clicked
    op()
    op done
    timeout expired
    second timeout expired
    Coroutine has ended
    

    也许这对其他人有帮助。我至少对此感到满意;)
    当然,仍然欢迎对一般模式发表评论!

    附录:请注意,如果将quamash替换为asyncqt,则此版本适用于Python 3.7.x之前的最新Python版本。但是,在Python 3.8中使用相同的代码会导致@coroutine装饰器生成RuntimeWarning,并最终导致RuntimeError: no running event loop中的asyncio.sleep()失败。也许其他人知道要进行哪些更改才能使此功能再次起作用。可能是asyncqt尚未与Python 3.8兼容。

    问候,
    菲利普

    10-07 20:26
    查看更多