aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdrian Herrmann <adrian.herrmann@qt.io>2023-02-17 17:47:25 +0100
committerQt Cherry-pick Bot <cherrypick_bot@qt-project.org>2023-03-02 17:38:30 +0000
commit1d537e8b068319ba90a1bbf2e328c8a9d687e04b (patch)
tree3986b9306ca92b159aa98e856cf9643852b5234c
parente48afcb0d6df296cbfd3d320640bc56d5aa85078 (diff)
examples: Improvements to the asyncio examples
- The asyncio AsyncHelper enters a quasi idle loop of switching between the asyncio event loop and the Qt event loop, where the asyncio event loop never receives new work at some point (besides yielding to Qt) and wastes memory and CPU cycles. Remedy this by signaling to AsyncHelper when asyncio's work is done. - Don't pass signals as parameters. - Fix comments in the asyncio examples that mentioned Trio. - Renamed the requirements files as only the Trio examples need them. - Remove unused imports. Task-number: PYSIDE-2169 Change-Id: Ia7197ea3446dd6ae514089b0dd260214c458db6a Reviewed-by: Christian Tismer <tismer@stackless.com> (cherry picked from commit e49516c724a11ac084372b5f0d7d62a35af363cc) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
-rw-r--r--examples/async/eratosthenes/eratosthenes_asyncio.py59
-rw-r--r--examples/async/eratosthenes/eratosthenes_trio.py14
-rw-r--r--examples/async/eratosthenes/requirements_trio.txt (renamed from examples/async/eratosthenes/requirements.txt)0
-rw-r--r--examples/async/minimal/minimal_asyncio.py59
-rw-r--r--examples/async/minimal/minimal_trio.py28
-rw-r--r--examples/async/minimal/requirements_trio.txt (renamed from examples/async/minimal/requirements.txt)0
6 files changed, 87 insertions, 73 deletions
diff --git a/examples/async/eratosthenes/eratosthenes_asyncio.py b/examples/async/eratosthenes/eratosthenes_asyncio.py
index 87d56e6fa..f63058cdf 100644
--- a/examples/async/eratosthenes/eratosthenes_asyncio.py
+++ b/examples/async/eratosthenes/eratosthenes_asyncio.py
@@ -6,10 +6,8 @@ from PySide6.QtGui import (QColor, QFont, QPalette)
from PySide6.QtWidgets import (QApplication, QGridLayout, QLabel, QMainWindow, QVBoxLayout, QWidget)
import asyncio
-import outcome
import signal
import sys
-import traceback
from random import randint
@@ -61,7 +59,9 @@ class MainWindow(QMainWindow):
widget.setPalette(palette)
-class Eratosthenes():
+class Eratosthenes(QObject):
+
+ done_signal = Signal()
""" This Sieve of Eratosthenes runs on a configurable tick (default
0.1 seconds). At each tick, a new subroutine will be created
@@ -69,6 +69,7 @@ class Eratosthenes():
these subroutines also operates on the same tick. """
def __init__(self, num, window, tick=0.1):
+ super().__init__()
self.num = num
self.sieve = [True] * self.num
self.base = 0
@@ -119,15 +120,17 @@ class Eratosthenes():
"🥳 Congratulations! You found all the prime numbers and solved mathematics. 🥳"
)
+ # This signals to the guest run when there are no more asyncio tasks
+ # left so its event loop can finish.
+ self.done_signal.emit()
-class AsyncHelper(QObject):
- trigger_signal = Signal()
+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. """
+ asyncio to resume when the event is handled. event.fn() is
+ the next entry point of the asyncio event loop. """
def event(self, event):
if event.type() == QEvent.User + 1:
event.fn()
@@ -136,22 +139,26 @@ class AsyncHelper(QObject):
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. """
+ self.fn is the next entry point of the asyncio event loop. """
def __init__(self, fn):
super().__init__(QEvent.Type(QEvent.User + 1))
self.fn = fn
- def __init__(self, entry=None):
+ def __init__(self, worker, entry):
super().__init__()
self.reenter_qt = self.ReenterQtObject()
self.entry = entry
self.loop = asyncio.new_event_loop()
+ self.done = False
- def set_entry(self, entry):
- 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.on_worker_started)
+ if hasattr(self.worker, "done_signal") and isinstance(self.worker.done_signal, Signal):
+ self.worker.done_signal.connect(self.on_worker_done)
@Slot()
- def launch_guest_run(self):
+ def on_worker_started(self):
""" To use asyncio and Qt together, one must run the asyncio
event loop as a "guest" inside the Qt "host" event loop. """
if not self.entry:
@@ -159,13 +166,23 @@ class AsyncHelper(QObject):
asyncio.set_event_loop(self.loop)
self.loop.create_task(self.entry())
self.loop.call_soon(self.next_guest_run_schedule)
+ self.done = False # Set this explicitly as we might want to restart the guest run.
self.loop.run_forever()
+ @Slot()
+ def on_worker_done(self):
+ """ When all our current asyncio tasks are finished, we must end
+ the "guest run" lest we enter a quasi idle loop of switching
+ back and forth between the asyncio and Qt loops. We can
+ launch a new guest run by calling launch_guest_run() again. """
+ self.done = True
+
def continue_loop(self):
""" This function is called by an event posted to the Qt event
- loop to restart the asyncio event loop. """
- self.loop.call_soon(self.next_guest_run_schedule)
- self.loop.run_forever()
+ loop to continue the asyncio event loop. """
+ if not self.done:
+ self.loop.call_soon(self.next_guest_run_schedule)
+ self.loop.run_forever()
def next_guest_run_schedule(self):
""" This function serves to pause and re-schedule the guest
@@ -189,13 +206,13 @@ if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = MainWindow(rows, cols)
eratosthenes = Eratosthenes(num, main_window)
- async_helper = AsyncHelper(entry=eratosthenes.start)
+ async_helper = AsyncHelper(eratosthenes, eratosthenes.start)
- # This establishes the entry point for the Trio guest run. It varies
- # depending on how and when its event loop is to be triggered, e.g.,
- # from the beginning (as here) or rather at a specific moment like
- # a button press.
- QTimer.singleShot(0, async_helper.launch_guest_run)
+ # This establishes the entry point for the asyncio guest run. It
+ # varies depending on how and when its event loop is to be
+ # triggered, e.g., from the beginning (as here) or rather at a
+ # specific moment like a button press.
+ QTimer.singleShot(0, async_helper.on_worker_started)
main_window.show()
diff --git a/examples/async/eratosthenes/eratosthenes_trio.py b/examples/async/eratosthenes/eratosthenes_trio.py
index 5fb2a35be..862a4fddd 100644
--- a/examples/async/eratosthenes/eratosthenes_trio.py
+++ b/examples/async/eratosthenes/eratosthenes_trio.py
@@ -61,7 +61,7 @@ class MainWindow(QMainWindow):
widget.setPalette(palette)
-class Eratosthenes():
+class Eratosthenes(QObject):
""" This Sieve of Eratosthenes runs on a configurable tick (default
0.1 seconds). At each tick, a new subroutine will be created
@@ -69,6 +69,7 @@ class Eratosthenes():
these subroutines also operates on the same tick. """
def __init__(self, num, window, tick=0.1):
+ super().__init__()
self.num = num
self.sieve = [True] * self.num
self.base = 0
@@ -119,8 +120,6 @@ class Eratosthenes():
class AsyncHelper(QObject):
- trigger_signal = Signal()
-
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
@@ -138,13 +137,14 @@ class AsyncHelper(QObject):
super().__init__(QEvent.Type(QEvent.User + 1))
self.fn = fn
- def __init__(self, entry=None):
+ def __init__(self, worker, entry):
super().__init__()
self.reenter_qt = self.ReenterQtObject()
self.entry = entry
- def set_entry(self, entry):
- 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):
@@ -183,7 +183,7 @@ if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = MainWindow(rows, cols)
eratosthenes = Eratosthenes(num, main_window)
- async_helper = AsyncHelper(entry=eratosthenes.start)
+ async_helper = AsyncHelper(eratosthenes, eratosthenes.start)
# This establishes the entry point for the Trio guest run. It varies
# depending on how and when its event loop is to be triggered, e.g.,
diff --git a/examples/async/eratosthenes/requirements.txt b/examples/async/eratosthenes/requirements_trio.txt
index e2cc10204..e2cc10204 100644
--- a/examples/async/eratosthenes/requirements.txt
+++ b/examples/async/eratosthenes/requirements_trio.txt
diff --git a/examples/async/minimal/minimal_asyncio.py b/examples/async/minimal/minimal_asyncio.py
index 337a2558d..497e388a1 100644
--- a/examples/async/minimal/minimal_asyncio.py
+++ b/examples/async/minimal/minimal_asyncio.py
@@ -5,18 +5,17 @@ from PySide6.QtCore import (Qt, QEvent, QObject, Signal, Slot)
from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget)
import asyncio
-import outcome
import signal
import sys
-import traceback
class MainWindow(QMainWindow):
- def __init__(self, async_signal):
- super().__init__()
+ start_signal = Signal()
+ done_signal = Signal()
- self.async_signal = async_signal
+ def __init__(self):
+ super().__init__()
widget = QWidget()
self.setCentralWidget(widget)
@@ -32,21 +31,20 @@ class MainWindow(QMainWindow):
@Slot()
def async_start(self):
- self.async_signal.emit()
+ self.start_signal.emit()
async def set_text(self):
await asyncio.sleep(1)
self.text.setText("What do you get if you multiply six by nine?")
+ self.done_signal.emit()
class AsyncHelper(QObject):
- trigger_signal = Signal()
-
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. """
+ asyncio to resume when the event is handled. event.fn() is
+ the next entry point of the asyncio event loop. """
def event(self, event):
if event.type() == QEvent.User + 1:
event.fn()
@@ -55,22 +53,26 @@ class AsyncHelper(QObject):
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. """
+ self.fn is the next entry point of the asyncio event loop. """
def __init__(self, fn):
super().__init__(QEvent.Type(QEvent.User + 1))
self.fn = fn
- def __init__(self, entry=None):
+ def __init__(self, worker, entry):
super().__init__()
self.reenter_qt = self.ReenterQtObject()
self.entry = entry
self.loop = asyncio.new_event_loop()
+ self.done = False
- def set_entry(self, entry):
- 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.on_worker_started)
+ if hasattr(self.worker, "done_signal") and isinstance(self.worker.done_signal, Signal):
+ self.worker.done_signal.connect(self.on_worker_done)
@Slot()
- def launch_guest_run(self):
+ def on_worker_started(self):
""" To use asyncio and Qt together, one must run the asyncio
event loop as a "guest" inside the Qt "host" event loop. """
if not self.entry:
@@ -78,13 +80,23 @@ class AsyncHelper(QObject):
asyncio.set_event_loop(self.loop)
self.loop.create_task(self.entry())
self.loop.call_soon(self.next_guest_run_schedule)
+ self.done = False # Set this explicitly as we might want to restart the guest run.
self.loop.run_forever()
+ @Slot()
+ def on_worker_done(self):
+ """ When all our current asyncio tasks are finished, we must end
+ the "guest run" lest we enter a quasi idle loop of switching
+ back and forth between the asyncio and Qt loops. We can
+ launch a new guest run by calling launch_guest_run() again. """
+ self.done = True
+
def continue_loop(self):
""" This function is called by an event posted to the Qt event
- loop to restart the asyncio event loop. """
- self.loop.call_soon(self.next_guest_run_schedule)
- self.loop.run_forever()
+ loop to continue the asyncio event loop. """
+ if not self.done:
+ self.loop.call_soon(self.next_guest_run_schedule)
+ self.loop.run_forever()
def next_guest_run_schedule(self):
""" This function serves to pause and re-schedule the guest
@@ -102,15 +114,8 @@ class AsyncHelper(QObject):
if __name__ == "__main__":
app = QApplication(sys.argv)
- async_helper = AsyncHelper()
- main_window = MainWindow(async_helper.trigger_signal)
- async_helper.set_entry(main_window.set_text)
-
- # This establishes the entry point for the Trio guest run. It varies
- # depending on how and when its event loop is to be triggered, e.g.,
- # at a specific moment like a button press (as here) or rather from
- # the beginning.
- async_helper.trigger_signal.connect(async_helper.launch_guest_run)
+ main_window = MainWindow()
+ async_helper = AsyncHelper(main_window, main_window.set_text)
main_window.show()
diff --git a/examples/async/minimal/minimal_trio.py b/examples/async/minimal/minimal_trio.py
index 1769a2dfc..526bdd331 100644
--- a/examples/async/minimal/minimal_trio.py
+++ b/examples/async/minimal/minimal_trio.py
@@ -13,10 +13,10 @@ import trio
class MainWindow(QMainWindow):
- def __init__(self, async_signal):
- super().__init__()
+ start_signal = Signal()
- self.async_signal = async_signal
+ def __init__(self):
+ super().__init__()
widget = QWidget()
self.setCentralWidget(widget)
@@ -32,7 +32,7 @@ class MainWindow(QMainWindow):
@Slot()
def async_start(self):
- self.async_signal.emit()
+ self.start_signal.emit()
async def set_text(self):
await trio.sleep(1)
@@ -41,8 +41,6 @@ class MainWindow(QMainWindow):
class AsyncHelper(QObject):
- trigger_signal = Signal()
-
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
@@ -60,13 +58,14 @@ class AsyncHelper(QObject):
super().__init__(QEvent.Type(QEvent.User + 1))
self.fn = fn
- def __init__(self, entry=None):
+ def __init__(self, worker, entry):
super().__init__()
self.reenter_qt = self.ReenterQtObject()
self.entry = entry
- def set_entry(self, entry):
- 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):
@@ -99,15 +98,8 @@ class AsyncHelper(QObject):
if __name__ == "__main__":
app = QApplication(sys.argv)
- async_helper = AsyncHelper()
- main_window = MainWindow(async_helper.trigger_signal)
- async_helper.set_entry(main_window.set_text)
-
- # This establishes the entry point for the Trio guest run. It varies
- # depending on how and when its event loop is to be triggered, e.g.,
- # at a specific moment like a button press (as here) or rather from
- # the beginning.
- async_helper.trigger_signal.connect(async_helper.launch_guest_run)
+ main_window = MainWindow()
+ async_helper = AsyncHelper(main_window, main_window.set_text)
main_window.show()
diff --git a/examples/async/minimal/requirements.txt b/examples/async/minimal/requirements_trio.txt
index ae0d704f0..ae0d704f0 100644
--- a/examples/async/minimal/requirements.txt
+++ b/examples/async/minimal/requirements_trio.txt