Running Heavy Geoprocessing in Background Without Freezing UI
Running heavy geoprocessing in the background without freezing the UI requires offloading CPU-intensive operations to a worker thread using QGIS's built-in…
Running heavy geoprocessing in the background without freezing the UI requires offloading CPU-intensive operations to a worker thread using QGIS’s built-in QgsTask framework. By subclassing QgsTask and implementing the run() method, you execute geoprocessing logic outside the main GUI thread while maintaining thread-safe communication through signals and QgsTaskManager. This prevents the QGIS interface from locking up during raster calculations, large vector overlays, network analysis, or batch exports.
Why the Main Thread Freezes & How QgsTask Solves It
QGIS relies on Qt’s single-threaded event loop to handle UI rendering, map canvas updates, and user input. When a Python script executes a long-running operation synchronously, it monopolizes this loop, blocking event processing and triggering the “Application Not Responding” state. The Asynchronous Task Execution with QgsTask pattern delegates work to a managed thread pool. QGIS automatically handles thread lifecycle, cancellation signals, and progress reporting. When background work completes, the finished() callback executes safely on the main thread, allowing you to update layers, trigger map refreshes, or show dialogs without violating Qt’s thread-affinity rules. For broader architectural guidance on structuring plugins and custom tools, consult the Plugin Development & UI Integration documentation.
flowchart LR
M["Main thread: pre-fetch data, addTask()"] --> W["Worker: run() heavy work on pre-fetched data"]
W -->|"setProgress / isCanceled"| W
W --> F["Main thread: finished(result)"]
F --> UI["Update layers, refresh canvas, dialogs"]
Production-Ready Implementation
The following implementation processes a list of geometries pre-fetched on the main thread. All heavy computation happens in run(), and only the finished() callback touches the QGIS project or UI. A custom signal carries results from the worker to any listening UI component.
from qgis.core import QgsTask, QgsMessageLog, Qgis, QgsApplication, QgsGeometry
from qgis.PyQt.QtCore import pyqtSignal
class HeavyGeoprocessingTask(QgsTask):
# Custom signal to safely pass results back to the main thread
processing_complete = pyqtSignal(list, bool)
def __init__(self, geometries: list, output_path: str, buffer_dist: float = 10.0,
description="Heavy Geoprocessing"):
super().__init__(description, QgsTask.CanCancel)
# Clone geometries so this task owns its data independently
self.geometries = [g.clone() for g in geometries]
self.output_path = output_path
self.buffer_dist = buffer_dist
self.result_geometries = []
self.result_message = ""
def run(self) -> bool:
"""
Executes in a background thread.
MUST remain strictly thread-safe: no QgsProject, iface, or UI calls.
"""
try:
total = len(self.geometries)
if total == 0:
self.result_message = "Input list contains no geometries."
return False
for i, geom in enumerate(self.geometries):
if self.isCanceled():
self.result_message = "Task cancelled by user."
return False
# Replace with actual heavy logic (e.g., geometry processing, GDAL calls)
buffered = geom.buffer(self.buffer_dist, 8)
self.result_geometries.append(buffered)
# Thread-safe progress reporting (0–100)
self.setProgress((i + 1) / total * 100)
self.result_message = f"Successfully processed {len(self.result_geometries)} geometries."
return True
except Exception as e:
self.result_message = f"Error: {str(e)}"
return False
def finished(self, result: bool):
"""Executes on the main thread. Safe for UI updates and layer management."""
if result:
QgsMessageLog.logMessage(self.result_message, "Geoprocessing", Qgis.Success)
else:
QgsMessageLog.logMessage(self.result_message, "Geoprocessing", Qgis.Warning)
# Emit custom signal for UI components listening to this task
self.processing_complete.emit(self.result_geometries, result)
Thread-Safety Rules & Best Practices
Violating Qt’s thread model is the primary cause of crashes and silent data corruption in QGIS plugins. Adhere to these boundaries:
Inside run() (Worker Thread):
- ✅ Use
self.setProgress(),self.isCanceled(), andQgsMessageLog.logMessage() - ✅ Operate on pre-fetched data (geometries, attribute dicts, plain Python objects)
- ✅ Run pure geometry operations on
QgsGeometryobjects cloned before task submission - ❌ Never call
QgsProject.instance(),iface,QgsMapCanvas, or modify anyQObjectnot created in this thread - ❌ Never call
QgsVectorLayer.getFeatures()— data providers are not thread-safe
Inside finished() (Main Thread):
- ✅ Update UI elements, add layers to the project, refresh the canvas, or emit signals
- ✅ Parse results and trigger downstream workflows
- ❌ Do not perform heavy computation here; it defeats the purpose of background execution
Task Execution & UI Integration
Pre-fetch all layer data on the main thread, then instantiate the task and hand it to QGIS’s global task manager.
from qgis.core import QgsApplication, QgsProject
# --- Main thread: fetch data before submitting ---
layer = iface.activeLayer()
if not layer:
iface.messageBar().pushWarning("No Layer", "Select a vector layer first.")
else:
# Fetch and clone geometries on the main thread
geometries = [f.geometry() for f in layer.getFeatures()]
task = HeavyGeoprocessingTask(
geometries, "/tmp/processed_output.gpkg", buffer_dist=50.0, description="Batch Export"
)
# Optional: Connect to custom signal for UI feedback
task.processing_complete.connect(lambda geoms, success:
iface.messageBar().pushSuccess("Done", f"{len(geoms)} features saved")
if success else iface.messageBar().pushWarning("Failed", "Processing failed"))
QgsApplication.taskManager().addTask(task)
The task manager respects system resources and prevents thread starvation. For detailed API behavior, refer to the official QgsTask Class Reference.
Advanced Patterns: Cancellation, Dependencies & GDAL
- Graceful Cancellation: Always check
self.isCanceled()inside loops. GDAL and OGR operations run at the C level and cannot be interrupted mid-call. Structure your pipeline to check cancellation between discrete processing steps. - Task Dependencies: Use
QgsTask.addSubTask()to chain tasks. The manager supports dependency graphs, ensuring Task B only runs after Task A succeeds. - Quick Scripts: For lightweight operations, skip subclassing and use
QgsTask.fromFunction(). It wraps a callable in a task automatically, though it lacks fine-grained progress control. - Memory Management: Large datasets should be processed in chunks to avoid exhausting RAM. QGIS’s thread pool does not isolate memory allocation; heavy allocations still impact the host process.
Thread safety in Qt follows strict affinity rules: objects created in one thread cannot be directly accessed from another. Review Qt Threading Basics to understand signal-slot queueing and cross-thread communication patterns. When combined with QgsTask, these patterns enable responsive, enterprise-grade geoprocessing tools.