Introduction
It is inevitable that some of our tasks in the program will take long time to run, leaving the user staring at a frozen screen, whether it is reading/writing a large file, searching database or syncing assets. In a production environment, these seconds can really add up for the entire team; can you imagine how many working hours are wasted when every user need to wait for, let’s say, a library tool to query the entire asset database and finally displaying them every single time.
We can surely improve the query, but that’s usually not where the bottleneck is. The speed of querying is usually sufficient for human brain to process. What we want is not to be slow down by un-interactive GUI. So, why not have assets to fill little by little in a continuous stream, or just query their name first, and use a separate thread to load in rest of the information like date created, author and thumbnail.
Now, let’s get straight to the point. I’m going to show three basic examples of different methods to deal with frozen GUI, each has its own use cases.
Example Issue
1 | import time |
Here I created a simple example to show how a long-running task can block controls in our Qt Gui.
The run_long_task()
is simply a time.sleep()
that will halt for 2 seconds
util returning control back to the main event loop.
After evoking the run_long_task()
method, we can
no longer interact with the line edit from our UI, until the task has finished.
![]() |
---|
(Clicking and typing has no effect while task is running) |
processEvent()
Using QApplication.processEvent()
can achieve a semi-interactive GUI
during a long-running task. Here’s to show what I meant.
Let’s add a progress bar to track our task status.
1 | class Window(QtWidgets.QMainWindow): |
And now, by evoking the
static method processEvent()
during the long-running task, we enforce
Qt to handle normal events like updating GUI, and respond to our user input,
before handing the control back to the task.
1 | def run_long_task(self): |
Now, as we can see, our main GUI can update the progress bar and messages, and even allow us to sort of interact with the line edit, but the interaction is by no means smooth.
![]() |
---|
(Somewhat interactive, yet choppy) |
Before we create a real multi-threading solution, first, let’s establish a signal slot workflow that Qt provides: instead of updating the progress bar and status bar by calling its method directly, we emit a signal to handle that.
1 | def __init__(self): |
QThread
Instead of directly subclassing QThread()
, it is recommended by many to
create a QObject()
and attach it to a QThread()
. The QObject()
we created,
known as the worker, will be running our long-running task, and emits the
update signals.
It is important to keep a note that, the QThread()
that houses our worker needs to live in the main
event loop (i.e. kept in the main application as self.__thread
).
If not, it will be collected by gc and the thread will exit pre-maturely.
thread.worker = worker
is also essential if the worker doesn’t live in the main
event loop (i.e. self.__worker
).
Now as we can see, we can freely interact with the line edit while the long-running task is handled in the background.
![]() |
---|
(Very smooth typing) |
QThreadPool and QRunnable
A natural progression would be handling multiple long-running tasks,
luckily, we have QThreadPool
for that.
In the following example, we have three long-running task in the background, while we still have free control of the main GUI.
![]() |
---|
(Multiple threads and smooth typing) |
-
QThreadPool
managesQRunnable
, which doesn’t have built-in signals, thus we need to attach signals externally by creating aQObject
to store them. -
QThreadPool
also deletes theQRunnable
instances automatically when it finishes by default. -
if max thread is exceeded, the process is queued until a thread is available.
Reference
PythonGUIs - Multithreading PyQt5 applications with QThreadPool
Maya’s Programming & Electronics Blog - How To Really, Truly Use QThreads; The Full Explanation