异步“最小”示例¶
Python语言提供了用于异步操作的关键字,即“async”用于定义协程或“await”用于在事件循环中调度异步调用(参见PEP 492)。由包来实现事件循环、支持这些关键字以及其他功能。
最著名的包是asyncio。asyncio提供了一个API,允许用自定义实现替换asyncio事件循环。这样的实现可以通过QtAsyncio模块获得。它基于Qt,并在后端使用Qt的事件循环。
trio 是另一个流行的包,它为更复杂的用例提供了一个专用的 低级别 API。具体来说,存在一个函数 start_guest_run,它允许将 Trio 事件循环作为“客人”运行在另一个事件循环中——在我们的案例中是 Qt 的,与 asyncio 的方法形成对比。
基于此功能,已经实现了两个使用Qt的异步示例:eratosthenes 和 minimal:

eratosthenes 是一个更广泛的示例,它可视化了埃拉托斯特尼筛法算法。这个算法本身并不特别适合异步操作,因为它不是I/O密集型的,但将协程同步到可配置的刻度允许良好的可视化。
minimal 是一个最小示例,展示了一个按钮,该按钮触发一个带有睡眠的异步协程。它旨在突出显示在使用 Qt 进行异步编程时哪些样板代码是必不可少的,并为更复杂的程序提供了一个起点。
虽然eratosthenes将异步逻辑卸载到将在trio/asyncio事件循环中运行的单独类中,minimal展示了异步函数可以集成到任何类中,包括Qt类的子类。
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
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 __future__ import annotations
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(handle_sigint=True)