| name | pyqt-threading |
| description | PyQt/PySide6 threading and concurrency - QThread, QThreadPool, QTimer, thread safety, concurrent patterns |
| metadata | {"author":"mte90","version":"1.0.0","tags":["python","qt","pyqt","pyside","threading","concurrency","async","qthread"]} |
PyQt Threading - Concurrency and Thread Safety
Comprehensive guide to threading in PyQt applications.
Thread Safety Rules
CRITICAL: Qt/PyQt is NOT thread-safe for UI operations. You MUST follow these rules:
- Never access widgets from worker threads - Only the main thread can modify UI
- Use signals for cross-thread communication - Emit signals from worker, connect to slots in main thread
- Use Qt.QueuedConnection for thread-safe signal delivery - AutoConnection handles this automatically
- Never block the main thread - Long operations will freeze the UI
class BadWorker(QThread):
def run(self):
self.label.setText("Done")
class GoodWorker(QThread):
finished = Signal(str)
def run(self):
result = self.process_data()
self.finished.emit(result)
QThread with Worker Object (Recommended Pattern)
The most flexible pattern separates the worker logic from thread lifecycle:
from PySide6.QtCore import QThread, Signal, QObject, Slot
class Worker(QObject):
"""Worker object that does the actual work."""
finished = Signal(object)
progress = Signal(int)
error = Signal(str)
def __init__(self, data):
super().__init__()
self.data = data
self._is_cancelled = False
@Slot()
def process(self):
"""Main processing method called from thread."""
try:
for i, item in enumerate(self.data):
if self._is_cancelled:
return
result = self.process_item(item)
self.progress.emit(int((i + 1) / len(self.data) * 100))
self.finished.emit({"status": "success", "count": len(self.data)})
except Exception as e:
.error.emit((e))
():
._is_cancelled =
():
time
time.sleep()
item *
():
():
().__init__()
.thread =
.worker =
():
.thread = QThread()
.worker = Worker(data)
.worker.moveToThread(.thread)
.worker.finished.connect(.on_finished)
.worker.progress.connect(.on_progress)
.worker.error.connect(.on_error)
.thread.started.connect(.worker.process)
.thread.finished.connect(.thread.deleteLater)
.thread.start()
():
.worker:
.worker.cancel()
.thread:
.thread.quit()
.thread.wait()
():
()
.cleanup()
():
()
():
()
.cleanup()
():
.thread =
.worker =
QThread Subclass (Simpler Pattern)
For simpler cases, subclass QThread directly:
from PySide6.QtCore import QThread, Signal
class DataProcessor(QThread):
"""Thread that processes data and emits progress."""
progress = Signal(int)
result_ready = Signal(list)
error_occurred = Signal(str)
finished = Signal()
def __init__(self, input_data, parent=None):
super().__init__(parent)
self.input_data = input_data
self._cancelled = False
def run(self):
"""Thread entry point - called by start()."""
try:
results = []
total = len(self.input_data)
for i, item in enumerate(self.input_data):
if self._cancelled:
self.error_occurred.emit("Cancelled")
return
processed = self.process_item(item)
results.append(processed)
progress_percent = int((i + 1) / total * 100)
self.progress.emit(progress_percent)
self.result_ready.emit(results)
Exception e:
.error_occurred.emit((e))
:
.finished.emit()
():
time
time.sleep()
(item).upper()
():
._cancelled =
():
():
().__init__()
.processor =
.progress = QProgressBar()
.start_btn = QPushButton()
.cancel_btn = QPushButton()
.start_btn.clicked.connect(.start_processing)
.cancel_btn.clicked.connect(.cancel_processing)
():
data = [, , , , ]
.processor = DataProcessor(data)
.processor.progress.connect(.progress.setValue)
.processor.result_ready.connect(.on_results)
.processor.error_occurred.connect(.on_error)
.processor.finished.connect(.on_finished)
.processor.start()
.start_btn.setEnabled()
():
.processor:
.processor.cancel()
():
()
():
QMessageBox.warning(, , error)
():
.start_btn.setEnabled()
.progress.setValue()
.processor =
QThreadPool with QRunnable
For parallel execution of independent tasks:
from PySide6.QtCore import QThreadPool, QRunnable, Signal, QObject, QThread
import time
class TaskSignals(QObject):
"""Signals for QRunnable (QRunnable cannot have signals directly)."""
finished = Signal(object)
error = Signal(str)
progress = Signal(int)
class ParallelTask(QRunnable):
"""Runnable task for thread pool."""
def __init__(self, task_id, data):
super().__init__()
self.task_id = task_id
self.data = data
self.signals = TaskSignals()
self._cancelled = False
def run(self):
"""Executed by thread pool."""
try:
time.sleep(0.5)
if self._cancelled:
return
result = {
"id": self.task_id,
"processed": str(self.data).upper(),
"thread": int(QThread.currentThreadId())
}
self.signals.finished.emit(result)
except Exception e:
.signals.error.emit((e))
():
._cancelled =
():
all_finished = Signal()
():
().__init__()
.pool = QThreadPool()
.pool.setMaxThreadCount(max_threads)
.active_tasks = {}
.completed_count =
.total_tasks =
():
.completed_count =
.total_tasks = (tasks)
.active_tasks.clear()
task_id, data (tasks):
task = ParallelTask(task_id, data)
task.signals.finished.connect(
result, tid=task_id: .on_task_finished(result)
)
task.signals.error.connect(.on_task_error)
.active_tasks[task_id] = task
.pool.start(task)
():
.completed_count +=
task_id = result[]
.active_tasks[task_id]
.completed_count >= .total_tasks:
.all_finished.emit(.completed_count)
():
()
():
task .active_tasks.values():
task.cancel()
.active_tasks.clear()
QTimer for Periodic Updates
from PySide6.QtCore import QTimer, Slot
class PollingWidget(QWidget):
def __init__(self):
super().__init__()
self.timer = QTimer(self)
self.timer.timeout.connect(self.on_timeout)
self.status_label = QLabel("Last update: Never")
self.poll_btn = QPushButton("Start Polling")
self.poll_btn.setCheckable(True)
layout = QVBoxLayout(self)
layout.addWidget(self.status_label)
layout.addWidget(self.poll_btn)
self.poll_btn.toggled.connect(self.toggle_polling)
@Slot()
def toggle_polling(self, checked):
if checked:
self.timer.start(1000)
self.poll_btn.setText("Stop Polling")
else:
self.timer.stop()
self.poll_btn.setText("Start Polling")
@Slot()
def on_timeout():
datetime datetime
.status_label.setText()
QThreadPool with QRunnable - Fire-and-Forget Pattern
For fire-and-forget tasks where you don't need to wait for results:
from PySide6.QtCore import QThreadPool, QRunnable, Signal, QObject
import time
class BackgroundTask(QRunnable):
"""Runnable for fire-and-forget operations."""
def __init__(self, task_id, data):
super().__init__()
self.task_id = task_id
self.data = data
self._cancelled = False
def run(self):
"""Executed by thread pool - auto-managed lifecycle."""
try:
for i in range(10):
if self._cancelled:
return
import time
time.sleep(0.1)
print(f"Task {self.task_id} completed")
except Exception as e:
print(f"Task {self.task_id} failed: {e}")
task = BackgroundTask(42, "some data")
QThreadPool.globalInstance().start(task)
Key Behaviors:
- Auto-delete: QRunnable is deleted automatically after
run() completes
- No explicit lifecycle management needed: Pool handles creation and destruction
- Global thread pool:
QThreadPool.globalInstance() returns the default singleton
- Default limit: Typically 8 threads (can be configured via
setMaxThreadCount())
- Thread recycling: Completed tasks' threads are reused for new tasks
moveToThread() Pattern - Worker Object Semantics
Correct Pattern: Worker + Thread Controller
The worker object pattern is the recommended approach for explicit thread control:
from PySide6.QtCore import QThread, Signal, QObject, Slot
class Worker(QObject):
"""Worker owns the work, controller owns the thread."""
progress = Signal(int)
finished = Signal(object)
def __init__(self, data):
super().__init__()
self.data = data
self._is_running = False
@Slot()
def process(self):
"""Main work method - called from thread."""
self._is_running = True
try:
for i in range(100):
self.progress.emit(i)
self.finished.emit(None)
finally:
self._is_running = False
class ThreadController(QObject):
"""Controller owns thread and manages worker."""
def __init__(self):
super().__init__()
self.thread = None
self.worker =
():
.thread = QThread()
.worker = Worker(data)
.worker.moveToThread(.thread)
.thread.started.connect(.worker.process)
.thread.finished.connect(.thread.quit)
.thread.finished.connect(.thread.deleteLater)
.thread.start()
Ownership Semantics
| Object | Owner | Lifetime | Deletion |
|---|
| Worker | ThreadController | Until thread.quit() + wait() | worker.deleteLater() |
| Thread | ThreadController | Until deleted | thread.deleteLater() |
| Signals | Their parent | Until parent deleted | Automatic |
Common Mistakes to Avoid
controller = ThreadController()
controller.thread = QThread()
controller.worker = Worker()
controller.thread.moveToThread(controller.worker)
controller.thread.start()
def start_work():
thread = QThread()
worker = Worker()
worker.moveToThread(thread)
thread.start()
controller = ThreadController()
controller.start_work(data)
controller.thread.quit()
controller.thread.wait()
Thread Lifecycle Management
Thread Signals and State
from PySide6.QtCore import QThread, Signal, QObject
class MyThread(QThread):
started = Signal()
finished = Signal()
isRunningChanged = Signal(bool)
def __init__(self):
super().__init__()
self._running = False
def run(self):
self._running = True
self.isRunningChanged.emit(True)
self.started.emit()
try:
pass
finally:
self.finished.emit()
Proper Cleanup Pattern
class ThreadManager(QObject):
def __init__(self):
super().__init__()
self.threads = {}
def create_thread(self, name, worker):
thread = QThread()
thread.setObjectName(name)
worker.moveToThread(thread)
thread.started.connect(worker.start_work)
thread.finished.connect(self._on_thread_finished, Qt.QueuedConnection)
thread.start()
self.threads[name] = thread
return thread
def _on_thread_finished(self, thread, name):
"""Clean up resources when thread exits."""
self.on_thread_cleanup.emit(thread, name)
if name in self.thread_workers:
worker = self.thread_workers.pop(name)
worker.deleteLater()
print(f"Thread {name} cleaned up")
def on_thread_cleanup(self, thread, name):
"""Override to handle custom cleanup."""
del self.threads[name]
manager = ThreadManager()
manager.thread_workers = {"worker1": worker}
thread = manager.create_thread(, worker)
Graceful Shutdown Patterns
class GracefulWorker(QObject):
finished = Signal()
def __init__(self):
super().__init__()
self._shutdown_requested = False
self._current_task = None
def shutdown(self):
"""Request graceful shutdown."""
self._shutdown_requested = True
if self._current_task:
self._current_task.cancel()
def run(self):
while not self._shutdown_requested:
if self._check_ready_for_work():
self._do_work()
else:
import time
time.sleep(0.01)
self.finished.emit()
def shutdown_worker(worker, thread):
"""Clean shutdown of worker and thread."""
worker.shutdown()
for _ in range():
worker.isFinished():
time
time.sleep()
thread.quit()
thread.wait()
worker.deleteLater()
thread.deleteLater()
Thread Safety - Advanced Patterns
Thread-Safe State Management
from PySide6.QtCore import QObject, QObject, QMutex, QMutexLocker, QAtomicPointer
from PySide6.QtCore import QThread, Signal
import threading
class ThreadSafeCounter:
"""Atomic counter for thread-safe increment."""
def __init__(self, initial=0):
self._value = QAtomicPointer(initial)
def increment(self):
"""Atomic increment operation."""
return QAtomicPointer.fetchAndAddRelaxed(self._value, 1)
def get(self):
"""Atomic read."""
return QAtomicPointer.load(self._value)
def set(self, value):
"""Atomic write."""
QAtomicPointer.store(self._value, value)
class SharedResource:
"""Advanced thread-safe resource."""
def __init__(self):
self._data = {}
self._mutex = threading.Lock()
self._ref_count = 0
self._lock = QMutex()
def ():
acquired = QMutex.tryLock(._lock, )
acquired:
RuntimeError()
._ref_count +=
():
QMutex.unlock(._lock)
._ref_count -=
():
locker = QMutexLocker(._lock)
._data[key] = value
Avoiding UI State Races
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QObject, Signal, Slot
import threading
class UIStateManager:
"""Prevent UI state races using mutex guards."""
def __init__(self):
self._state_lock = threading.Lock()
self._update_in_progress = False
def safe_update_ui(self, value):
"""Ensure only one UI update at a time."""
if threading.current_thread() is threading.main_thread():
self._apply_ui_update(value)
else:
self.update_ui_slot(value)
@Slot()
def update_ui_slot(self, value):
"""Thread-safe slot for UI updates."""
with self._state_lock:
if self._update_in_progress:
self._cancel_pending_update()
self._update_in_progress = True
try:
QApplication.instance().postEvent(
QApplication.instance(),
._update_event(value)
)
:
._update_in_progress =
():
.status_label.setText((value))
():
.status_label.setText()
Resource Exhaustion Prevention
class ThreadPoolManager:
"""Configurable thread pool with resource limits."""
def __init__(self, max_threads=4, max_concurrent=2):
self.pool = QThreadPool()
self.pool.setMaxThreadCount(max_threads)
self.max_concurrent = max_concurrent
self.active_count = 0
self._lock = QMutex()
def submit_safe(self, task):
"""Submit task only if under concurrency limit."""
if self._should_proceed():
self.pool.start(task)
else:
print("Too many concurrent tasks")
def _should_proceed(self):
"""Check if we can proceed with new task."""
with QMutexLocker(self._lock):
self.active_count += 1
result = self.active_count < self.max_concurrent
if not result:
self.active_count -= 1
return result
def _cleanup_finished(self, task):
QMutexLocker(._lock):
.active_count -=
:
():
.max_duration = max_duration
.max_memory = max_memory
._start_time =
._memory_usage =
():
._start_time = time.time()
._memory_usage = ._get_memory_usage()
():
elapsed = time.time() - ._start_time
current_memory = ._get_memory_usage()
elapsed > .max_duration:
TimeoutError()
memory_delta = current_memory - ._memory_usage
memory_delta > .max_memory:
MemoryError()
():
resource
resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
Common Issues
| Issue | Cause | Solution |
|---|
| UI freezes | Blocking operation in main thread | Move to worker thread |
| Crashes on widget access | Accessing UI from worker thread | Use signals instead |
| Memory leaks | Thread not cleaned up | Use deleteLater() and proper lifecycle |
| Deadlocks | Multiple mutexes acquired in different order | Always acquire in same order, use timeout |
| Race conditions | Shared data without locks | Use QMutex or atomic operations |
Best Practices
- Always use signals for cross-thread communication - Direct widget access from threads causes crashes
- Keep worker objects thread-affinity aware - Never assume QObject is in main thread
- Clean up threads properly - Use deleteLater() and quit() + wait()
- Handle cancellation - Check flags periodically in long operations
- Use QThreadPool for parallel independent tasks - Default pool manages resource limits
- Use moveToThread() for explicit thread control - Worker object pattern is recommended
- Never use time.sleep() in main thread - Use QTimer or workers instead
- Keep locks short-lived - Only hold mutexes for critical section duration
- Use QReadWriteLock for read-heavy data - Multiple readers possible, single writer
- Monitor thread pool limits - Set maxThreadCount to prevent resource exhaustion
Common Pitfalls
| Issue | Cause | Solution |
|---|
| UI crashes on widget access | Widget accessed from worker thread | Always use signals to update UI from main thread |
| Deadlock | Multiple mutexes acquired in different order | Always acquire in consistent order, use timeouts |
| Race conditions | Shared data without locks | Use QMutex, QAtomicPointer, or atomic operations |
| Memory leaks | Threads not cleaned up | Use deleteLater() on threads and workers |
| Thread not stopping | No quit() + wait() sequence | Always call thread.quit() then thread.wait() |
| Signals firing from wrong thread | AutoConnection uses queued delivery | AutoConnection is correct - don't change |
| Re-entrancy issues | Signal handler calls slot recursively | Use flags to track state changes |
| Resource exhaustion | Unlimited thread pool threads | Set maxThreadCount on QThreadPool |
| Busy-wait loops | Thread polling without sleep | Use QTimer instead of polling |
| Lock not released | Exception before unlock | Use QMutexLocker for RAII-style cleanup |
Top 5 Mistakes to Avoid
-
Never access UI from worker threads - The most common crash cause
def run(self):
self.label.setText("Done")
def run(self):
self.finished.emit("Done")
-
Forgetting thread lifecycle management - Threads become zombies
thread = QThread()
thread.start()
thread.start()
thread.quit()
thread.wait()
-
Acquiring locks in wrong order - Deadlock
def do_work(self):
with lock_a:
with lock_b:
pass
def do_other_work(self):
with lock_b:
with lock_a:
pass
-
Using locks too long - Performance issues
with lock:
result = heavy_computation()
lock:
data = .shared_data
result = heavy_computation(data)
References
Official Documentation
Qt for Python (PySide6/PyQt6)
Community Resources
Advanced Topics
Testing