PyQt5和asyncio:从永不完成的收益率

Phi*_*rch 9 python pyqt5 python-asyncio

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

在查看如何合并Qt5和asyncio的事件循环时,我发现了一个邮件列表发布,建议使用quamash.但是,在运行此示例(未修改)时,

yield from fut
Run Code Online (Sandbox Code Playgroud)

nevers似乎又回来了.我看到输出'Timeout',所以定时器回调显然会触发,但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.
Run Code Online (Sandbox Code Playgroud)

我在使用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())
Run Code Online (Sandbox Code Playgroud)

程序应该显示一个窗口,里面有一个按钮(它可以),按钮调用slow_operation()而不会阻止GUI.运行此示例时,我可以根据需要随时单击该按钮,因此不会阻止GUI.但是

yield from asyncio.sleep(0.1)
Run Code Online (Sandbox Code Playgroud)

永远不会传递,终端输出如下所示:

btnCallback returns...
clicked
op()
op done
btnCallback returns...
clicked
op()
op done
Run Code Online (Sandbox Code Playgroud)

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

现在,有两个问题:

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

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

谢谢,菲利普

Phi*_*rch 14

好吧,这是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())
Run Code Online (Sandbox Code Playgroud)

关键似乎是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')
Run Code Online (Sandbox Code Playgroud)

它现在"正常":

btnCallback returns...
clicked
op()
op done
timeout expired
second timeout expired
Coroutine has ended
Run Code Online (Sandbox Code Playgroud)

也许这对其他人有所帮助.我至少对它感到高兴;)当然,欢迎对一般模式的评论!

此致,菲利普

  • 顺便说一句,问/答对我有很大帮助!同样在Python 3.5+中,您可以(并且应该-https://docs.python.org/3/library/asyncio-task.html#coroutines)使用`async def`而不是`@ asyncio.coroutine`。 (3认同)
  • 是.默认情况下,`asyncio.get_event_loop()`返回默认的`SelectorEventLoop`.`asyncio`内部有许多函数,它们使用`get_event_loop()`来获取循环并将回调附加到它.如果你没有`set_event_loop`你的自定义事件循环,所有Futures和协同程序都被安排到默认的事件循环,永远不会运行. (2认同)