From 0528df5e18902cd367316cf8922de28a4d1f04b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 4 Sep 2025 11:39:12 -0400 Subject: [PATCH] Improve thread lifecycle management in TaskManagerBase --- qtapputils/managers/taskmanagers.py | 39 +++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/qtapputils/managers/taskmanagers.py b/qtapputils/managers/taskmanagers.py index 661821e..8db549e 100644 --- a/qtapputils/managers/taskmanagers.py +++ b/qtapputils/managers/taskmanagers.py @@ -81,6 +81,7 @@ def __init__(self, verbose: bool = False): self.verbose = verbose self._worker = None + self._thread_is_quitting = False self._task_callbacks: dict[uuid.UUID, Callable] = {} self._task_data: dict[uuid.UUID, tuple[str, tuple, dict]] = {} @@ -146,6 +147,7 @@ def set_worker(self, worker: WorkerBase): self._worker.moveToThread(self._thread) self._thread.started.connect(self._worker.run_tasks) + self._thread.finished.connect(self._handle_thread_finished) self._worker.sig_task_completed.connect(self._handle_task_completed) @@ -170,16 +172,24 @@ def _handle_task_completed( # Remove references to the completed task from internal structures. self._cleanup_task(task_uuid4) - # If there are still running tasks, do not proceed further. - if len(self._running_tasks) > 0: - return - - # We quit the thread here to ensure all resources are cleaned up - # and to prevent issues with lingering events or stale object - # references. This makes the worker lifecycle simpler and more robust, - # especially in PyQt/PySide, and avoids subtle bugs that can arise - # from reusing threads across multiple batches. - self._thread.quit() + # When all running task are completed, we quit the thread to ensure + # all resources are cleaned up and to prevent issues with lingering + # events or stale object references. This makes the worker lifecycle + # more robust, especially in PyQt/PySide, and avoids subtle bugs that + # can arise from reusing threads across multiple batches. + if len(self._running_tasks) == 0: + self._thread_is_quitting = True + self._thread.quit() + # NOTE: After 'quit()' is called, the thread's event loop exits + # after processing pending events, and the 'QThread.finished' + # signal is emitted. This triggers '_handle_thread_finished()', + # which manages pending tasks or signals that all work is done. + + def _handle_thread_finished(self): + """ + Handle when the thread event loop is shut down. + """ + self._thread_is_quitting = False # If there are pending tasks, begin processing them. if len(self._pending_tasks) > 0: @@ -226,15 +236,12 @@ def _run_pending_tasks(self): if len(self._pending_tasks) == 0: return + if self._thread_is_quitting: + return + if self.verbose: print(f'Executing {len(self._pending_tasks)} pending tasks...') - # Ensure the thread is not running before starting new tasks. - # This prevents starting a thread that is already active, which can - # cause errors. - if self._thread.isRunning(): - qtwait(lambda: not self._thread.isRunning()) - # Move all pending tasks to the running tasks queue. self._running_tasks = self._pending_tasks.copy() self._pending_tasks = []