Avoiding random crashes when multithreading Qt
Overcoming the surprisingly stringent thread-unsafety of Qt
Writing parallel code is always a challenge, but there are extra precautions you must take when multithreading with the Qt framework. Failing to heed these precautions will lead to race conditions which cause random, hard-to-track and hard-to-reproduce crashes.
What “thread-safe” means to Qt
Most objects in Qt are not thread-safe. In other contexts, an object that is not thread-safe means that only one thread can read to or write from that object at a time. Practically speaking, Qt has a more stringent definition: only a single special thread, known as the “GUI thread,” may touch it.
In Qt, the GUI thread runs the main event loop. Because of some hidden global state kept by Qt, even seemingly-isolated Qt objects have pointers to resources which are not kept thread-safe. This means that most Qt objects may not be touched by any other thread, even if they are completely isolated to that thread.
This is practical, but controversial, advice. While Qt implies that this is only true for certain functions (such as GUI repaint()
calls), there is no documentation or guarantees about which Qt functions will make such calls internally.
The problem: a minimal example
Let’s walk through an example of how an innocuous callback creates a race condition. I’ve written this using PyQt for readability and reproducibility, but the principles apply to C++ Qt as well.
lock = threading.Lock()
class MyObject():
def __init__(self):
self.bar = QtWidgets.QProgressBar() def callback(self, value):
with lock:
self.bar.setValue(value)
The callback looks thread-safe, but if you take a closer look at the implementation of setValue
, you’ll find a call to repaint
which in turn grabs a global QOpenGLContext
. Only Qt’s GUI thread is allowed to touch this context, so you’ll have a race condition.
To demonstrate, let’s run this from a background thread:
def longRunningTask(self, callback):
for i in xrange(100):
time.sleep(.01)
callback(i)obj = MyObject()
threadPool = ThreadPool(16)
for i in xrange(50):
self.threadPool.apply_async(longRunningTask, (obj.callback,))
We’ve launched fifty threads, all of which are directly calling the same callback function. You’ll find that OSX and Qt both nondeterministically spit out errors before segfaulting, even though the callback itself is safely locked. On my machine, I receive some subset of the following messages:
It does not make sense to draw an image when [NSGraphicsContext currentContext] is nil. This is a programming error. Break on void _NSWarnForDrawingImageWithNoCurrentContext(void) to debug. This will be logged only once. This may break in the future.
QPainter::begin: A paint device can only be painted by one painter at a time.
QPainter::setCompositionMode: Painter not active
QBackingStore::endPaint() called with active painter on backingstore paint device
Segmentation fault: 11
The solution: making it thread-safe
To avoid these race conditions, leverage Qt’s Signals
and Slots
to communicate with Qt objects. This guarantees thread-safety because a callback created via Slot
will be run on the main thread by default.
class Task(QtCore.QObject):
updated = QtCore.pyqtSignal(int) def longRunningTask(self):
for i in xrange(100):
time.sleep(.01)
self.updated.emit(i)obj = MyObject()
threadPool = ThreadPool(16)
for i in xrange(50):
task = Task()
task.updated.connect(obj.callback)
self.threadPool.apply_async(task.longRunningTask, ())
Rather than directly calling obj.callback()
in a background thread, emitting a Signal
will instead notify the GUI thread to run the callback. Doing so guarantees that only the GUI thread will touch the QProgressBar
.
Demo
A complete demo application with both implementations is available on this Gist:
Running it shows the thread-safety of the Signals
and Slots
, and the race condition caused by the inline callback. When the left button is toggled on, everything works as expected. Toggling off the use of Signals
and Slots
leads to a race condition and segfault.
Additional Reading
From the Qt documentation:
Threading Basics
Reentrancy and Thread-Safety
Signals and Slots