异步“最小”示例#
Python语言提供了异步操作关键字,即“async”用于定义协程或“await”用于在事件循环中调度异步调用(参见PEP 492)。实现事件循环、支持这些关键字等则是各个库的责任。
最著名的库是asyncio。asyncio提供了一个API,允许将asyncio事件循环替换为自定义实现。QtAsyncio小部件提供了这种实现。它基于Qt,并在后端使用Qt的事件循环。
trio是另一个流行的库,提供了针对更复杂用例的专用低级API。具体来说,存在一个呼叫函数start_guest_run,该函数允许将Trio事件循环作为“客人”在另一个事件循环中运行——在我们的例子中,Qt的事件循环,与asyncio的方法形成对比。
基于这个功能,实现了两个使用Qt进行异步操作示例:eratosthenes和minimal
欧几里得 是一个更广泛的例子,用于可视化埃拉托斯特尼筛法算法。这个算法本身并不特别适合异步操作,因为它不是I/O密集型的,但是通过到配置的时钟同步协程可以实现良好的可视化。
Minimal 是一个最小的示例,包含一个按钮,该按钮触发放置睡眠的异步协程。它旨在突出Qt异步程序中的哪些样板代码是必要的,并为更复杂的程序提供了一个起点。
虽然 eratosthenes 将将在trio's/asyncio的事件循环中运行的异步逻辑卸载到一个独立类中,但 minimal 展示了异步函数可以集成到任何类中,包括Qt类的子类。
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtCore import (Qt, QEvent, QObject, Signal, Slot)
from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget)
import outcome
import signal
import sys
import traceback
import trio
class MainWindow(QMainWindow):
start_signal = Signal()
def __init__(self):
super().__init__()
widget = QWidget()
self.setCentralWidget(widget)
layout = QVBoxLayout(widget)
self.text = QLabel("The answer is 42.")
layout.addWidget(self.text, alignment=Qt.AlignmentFlag.AlignCenter)
async_trigger = QPushButton(text="What is the question?")
async_trigger.clicked.connect(self.async_start)
layout.addWidget(async_trigger, alignment=Qt.AlignmentFlag.AlignCenter)
@Slot()
def async_start(self):
self.start_signal.emit()
async def set_text(self):
await trio.sleep(1)
self.text.setText("What do you get if you multiply six by nine?")
class AsyncHelper(QObject):
class ReenterQtObject(QObject):
""" This is a QObject to which an event will be posted, allowing
Trio to resume when the event is handled. event.fn() is the
next entry point of the Trio event loop. """
def event(self, event):
if event.type() == QEvent.Type.User + 1:
event.fn()
return True
return False
class ReenterQtEvent(QEvent):
""" This is the QEvent that will be handled by the ReenterQtObject.
self.fn is the next entry point of the Trio event loop. """
def __init__(self, fn):
super().__init__(QEvent.Type(QEvent.Type.User + 1))
self.fn = fn
def __init__(self, worker, entry):
super().__init__()
self.reenter_qt = self.ReenterQtObject()
self.entry = entry
self.worker = worker
if hasattr(self.worker, "start_signal") and isinstance(self.worker.start_signal, Signal):
self.worker.start_signal.connect(self.launch_guest_run)
@Slot()
def launch_guest_run(self):
""" To use Trio and Qt together, one must run the Trio event
loop as a "guest" inside the Qt "host" event loop. """
if not self.entry:
raise Exception("No entry point for the Trio guest run was set.")
trio.lowlevel.start_guest_run(
self.entry,
run_sync_soon_threadsafe=self.next_guest_run_schedule,
done_callback=self.trio_done_callback,
)
def next_guest_run_schedule(self, fn):
""" This function serves to re-schedule the guest (Trio) event
loop inside the host (Qt) event loop. It is called by Trio
at the end of an event loop run in order to relinquish back
to Qt's event loop. By posting an event on the Qt event loop
that contains Trio's next entry point, it ensures that Trio's
event loop will be scheduled again by Qt. """
QApplication.postEvent(self.reenter_qt, self.ReenterQtEvent(fn))
def trio_done_callback(self, outcome_):
""" This function is called by Trio when its event loop has
finished. """
if isinstance(outcome_, outcome.Error):
error = outcome_.error
traceback.print_exception(type(error), error, error.__traceback__)
if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = MainWindow()
async_helper = AsyncHelper(main_window, main_window.set_text)
main_window.show()
signal.signal(signal.SIGINT, signal.SIG_DFL)
app.exec()
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget)
import PySide6.QtAsyncio as QtAsyncio
import asyncio
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
widget = QWidget()
self.setCentralWidget(widget)
layout = QVBoxLayout(widget)
self.text = QLabel("The answer is 42.")
layout.addWidget(self.text, alignment=Qt.AlignmentFlag.AlignCenter)
async_trigger = QPushButton(text="What is the question?")
async_trigger.clicked.connect(lambda: asyncio.ensure_future(self.set_text()))
layout.addWidget(async_trigger, alignment=Qt.AlignmentFlag.AlignCenter)
async def set_text(self):
await asyncio.sleep(1)
self.text.setText("What do you get if you multiply six by nine?")
if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
QtAsyncio.run()