Avoiding random crashes when multithreading Qt

Overcoming the surprisingly stringent thread-unsafety of Qt

Armin Samii
3 min readDec 6, 2018

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.

Running the code above: using signals/slots is thread-safe

Additional Reading

From the Qt documentation:
Threading Basics
Reentrancy and Thread-Safety
Signals and Slots

--

--

Armin Samii

Building products using computer graphics and data visualizations. Ranked-Choice Voting enthusiast. Pittsburgh, PA.