Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions qtapputils/managers/taskmanagers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]] = {}
Expand Down Expand Up @@ -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)

Expand All @@ -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:
Expand Down Expand Up @@ -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 = []
Expand Down
Loading