PyQt5和asyncio:从不结束的收益。

时间:2020-12-10 23:00:03

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.

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

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

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

yield from fut

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.

I tested this on Ubuntu with python 3.5 and on Windows with 3.4, same behaviour on both platforms.

我在Ubuntu上用python 3.5测试了这个,在Windows上测试了3.4,在两个平台上都是相同的行为。

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

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

该程序应该显示一个带有按钮的窗口(它确实是这样做的),按钮调用slow_operation()而不阻塞GUI。在运行这个示例时,我可以按我想要的频率单击按钮,这样GUI就不会被阻塞。但是,

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

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

Now, two questions:

现在,两个问题:

  1. 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.

    通常来说,这是实现从GUI中分离冗长操作的一种明智的方法吗?我的意图是,按钮回调将coroutine调用发送到事件循环(带有或没有额外的嵌套级别,cf coroCallHelper()),然后将其调度并执行。我不需要单独的线程,因为它实际上只是需要时间的I/O,没有实际的处理。

  2. How can I fix this behaviour?

    我该如何改正这种行为?

Thanks, Philipp

谢谢,菲利普

1 个解决方案

#1


7  

Ok, that's one plus of SO: Writing down a question makes you think again about everything. Somehow I just figured it out:

好的,这是其中的一个优点:写下一个问题会让你重新思考所有的事情。不知怎么的,我发现了:

Looking again at the example from the quamash repo, I found that the event loop to use is obtained somewhat differently:

再看一下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())

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:

关键似乎是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')

And it 'just works' now:

它现在只是工作:

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!

也许这对其他人有帮助。我至少对它感到满意;当然,对通用模式的评论仍然是受欢迎的!

Regards, Philipp

问候,菲利普

#1


7  

Ok, that's one plus of SO: Writing down a question makes you think again about everything. Somehow I just figured it out:

好的,这是其中的一个优点:写下一个问题会让你重新思考所有的事情。不知怎么的,我发现了:

Looking again at the example from the quamash repo, I found that the event loop to use is obtained somewhat differently:

再看一下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())

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:

关键似乎是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')

And it 'just works' now:

它现在只是工作:

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!

也许这对其他人有帮助。我至少对它感到满意;当然,对通用模式的评论仍然是受欢迎的!

Regards, Philipp

问候,菲利普