30. Thread Synchronization

There are cases where executing a piece of code in a separate thread is desirable (or unavoidable) but it inevitably makes the program flow more complex and presents the programmer with a set of challenges, one of which is thread synchronization. In this chapter, we demonstrate why thread synchronization may be necessary and present the Qt tools for synchronizing threads.

30.1 Race Condition Demo

An icon of a clipboard-list1

You are working on a banking application that lets users withdraw money from their bank account. This takes place over a network so you decide to make withdrawals in a background thread to avoid blocking the GUI thread.

In your application you:

  1. Create a class named BankAccount to hold the user account balance, and add two methods, get_balance to return the current balance, and withdraw to let the user withraw the specified amount of money from the account. In the demonstration we use QThread.msleep() to simulate network lag.
  1. Create the Worker class. We will move this class to a background thread and call its process() method to process the user request.
  1. In the main application class we simulate a situation where five users simultaneously withdraw money from a joint bank account. Each user withdraws 100 monetary units from the account. We initialize the bank account with (thread_count * amount) monetary units so that the expected final balance is zero. On the “Withdraw money” button click, we start five background threads and five worker objects, and we move each worker to its own thread. The money is withdrawn as soon as each background thread starts. The single BankAccount object also lives in its own separate thread.

A sample output could be:

 1 ---withdraw start--- Thread 0
 2 ---withdraw start--- Thread 1
 3 ---withdraw start--- Thread 4
 4 ---withdraw start--- Thread 2
 5 ---withdraw start--- Thread 3
 6 ---withdraw end--- Thread 2 400
 7 ---withdraw end--- Thread 3 400
 8 ---withdraw end--- Thread 4 400
 9 ---withdraw end--- Thread 1 400
10 ---withdraw end--- Thread 0 400
11 Expected: 0, Got: 400
12 =====================

We expect the final balance to be zero but we end up with 400 monetary units in the account left.

From the output, you can see that all five worker objects manipulate BankAccount.balance at the same time. At the very beginning of BankAccount.withdraw() we assign self.balance to a local variable

1 balance = self.balance

All five threads reach that line of code before any of them had a chance to subtract the amount from the balance

1 ---withdraw start--- Thread 0
2 ---withdraw start--- Thread 1
3 ---withdraw start--- Thread 4
4 ---withdraw start--- Thread 2
5 ---withdraw start--- Thread 3

So for each worker, balance equals to 500. When each worker subtracts the amount from the balance:

1 balance -= amount
2 self.balance = balance

the final balance value ends up being 400. The above code snippet is not an atomic operation and the threads are not synchronized.

In such a situation, the developer needs a way to synchronize thread execution, i.e., to only let a single thread withdraw money at any given time. Qt provides several means to achieve this.

30.2 Queued Signal-Slot Connection

One benefit of Qt’s signals and slots mechanism is that queued connections are thread-safe for cross-thread communication. The slot is invoked when control returns to the event loop of the receiver object’s thread. Internally, Qt posts a QMetaCallEvent (event of type QEvent.MetaCall) to the receiver’s event queue, ensuring that slot invocations for that object are serialized.

An icon of a clipboard-list1

You are developing a multithreaded banking application that enables users to withdraw money from their bank accounts. To avoid race conditions when accessing the account balance, you decide to take advantage of Qt’s signals and slots mechanism for thread-safe communication.

  1. Create the Worker class. This worker version has two signals, requestUpdate(int) which requests that the BankAccount update the balance, and transactionProcessed() which signals that the transaction is processed. We connect Worker.requestUpdate() to BankAccount.withdraw()
1 self.requestUpdate.connect(self.bank_account.withdraw)

Both the worker and the bank account live in their own threads, so the connection type defaults to Qt.QueuedConnection. This means that:

  • Worker.requestUpdate() will be placed in the bank account thread event loop.
  • BankAccount.withdraw() will be executed in the bank account thread.
  • BankAccount.withdraw() will block the bank account thread until it returns.

This ensures that BankAccount.withdraw() is accessed by one thread at a time and the race condition is avoided.

  1. Create the BankAccount class. It is mostly unchanged except that we add a signal named balanceSent() to it. The signal will be used to send the final balance to the GUI thread.
  1. Add the requestBalance() signal to the main window. When all worker threads are finished, the main windows sends this signal to the bank account thread, which in turn responds with the balanceSent() signal. The output is:
 1 ---withdraw start--- Bank account thread
 2 ---withdraw end--- Bank account thread
 3 ---withdraw start--- Bank account thread
 4 ---withdraw end--- Bank account thread
 5 ---withdraw start--- Bank account thread
 6 ---withdraw end--- Bank account thread
 7 ---withdraw start--- Bank account thread
 8 ---withdraw end--- Bank account thread
 9 ---withdraw start--- Bank account thread
10 ---withdraw end--- Bank account thread
11 Expected: 0, Got: 0

Access to BankAccount.balance is synchronized. Note that neither the worker objects nor the main window object access BankAccount.balance directly - all communication takes place using signals and slots. Also note that all withdraw() calls are executed in the bank account thread.

30.3 QMutex

A mutex is a synchronization tool that prevents multiple threads from accessing the same shared resource - object, data structure or section of code - simultaneously. When one thread acquires a mutex, any other thread trying to access that same resource will be blocked and forced to wait until the first thread releases it. This prevents race conditions where concurrent access could corrupt data or cause unpredictable behavior in your program1.

In Qt, the mutex lock applies to the scope between QMutex.lock() and QMutex.unlock(). Any code inside, including function/method calls, runs under the protection of the lock. No other thread can enter the protected section until unlock() is called2.

An icon of a clipboard-list1

You are working on a multithreaded banking application that allows users to withdraw money from their accounts. Use a mutex to prevent race conditions on the bank account balance.

To use QMutex in your application:

  1. Create the worker class. The class is similar to the worker from the race condition demo, except we don’t need to send the transactionProcessed() and requestUpdate() signals.
  1. Create the BankAccount class. In its withdraw() method, guard the code that updates the account balance with the lock() and unlock() calls.

When you start the application and withdraw money, the output is:

 1 ---withdraw start--- Thread 0
 2 ---withdraw end--- Thread 0 400
 3 ---withdraw start--- Thread 1
 4 ---withdraw end--- Thread 1 300
 5 ---withdraw start--- Thread 3
 6 ---withdraw end--- Thread 3 200
 7 ---withdraw start--- Thread 2
 8 ---withdraw end--- Thread 2 100
 9 ---withdraw start--- Thread 4
10 ---withdraw end--- Thread 4 0
11 Expected: 0, Got: 0
12 =====================

withdraw() calls are serialized and each is executed in the caller thread.

30.4 QMutexLocker

QMutexLocker3 is a convenience class that simplifies using mutexes. QMutexLocker is created within a method where a QMutex needs to be locked - the mutex is locked when mutex locker is created and unlocked when the mutex locker is destroyed.

An icon of a clipboard-list1

You use a mutex to prevent race conditions in your multithreaded banking application. You decide to use QMutexLocker to avoid locking and unlocking the mutex manually.

To use a QMutexLocker in your application:

  1. In the BankAccount class add a mutex instance variable.

  2. In the withdraw() method, create a QMutexLocker local variable just before the code that updates the balance. When withdraw() returns the variable goes out of scope, unlocking the mutex.

QMutexLocker is an example of the RAII idiom in C++, ensuring that a mutex is automatically unlocked when the locker goes out of scope - just as Python’s context managers guarantee automatic resource release upon exiting a with block of code.

30.5 QSemaphore

A semaphore is a synchronization primitive used in concurrent programming to control access to a shared resource with a limited number of units. It keeps the count of available units, allowing threads to acquire (decrement) the count when using the resource and release (increment) it when done4. In Qt, the QSemaphore class provides a general counting semaphore.

An icon of a clipboard-list1

You are developing a multithreaded banking application where multiple customers attempt to withdraw money using ATMs at the same location. Due to provider restrictions, the location has a limited number of internet connections available. To ensure that customers can only perform withdrawals when a connection is available, you decide to use a QSemaphore to manage the shared pool of ATM network connections.

To use QSemaphore in your application:

  1. Create the class that represents the shared resource, AtmPool in the example. Initialize a QSemaphore with the number of available resources (e.g., 5 connections).

  2. In the AtmPool class, add a use_atm() slot that acquires the semaphore before using the resource (simulating a withdrawal with a random sleep), and releases it in a finally block to ensure it’s always freed.

  1. Create the Worker class, which calls use_atm() in its process() method to simulate a customer using an ATM. In the main window class, create the AtmPool object and move it to its own thread. Then, create 15 worker threads (more than available resources) to demonstrate queuing. Each worker signals when finished.

When you run the application and click “Withdraw Money, the output is:

 1 Thread 0 is using an ATM (available: 4)
 2 Thread 2 is using an ATM (available: 3)
 3 Thread 3 is using an ATM (available: 2)
 4 Thread 6 is using an ATM (available: 0)
 5 Thread 1 is using an ATM (available: 1)
 6 Thread 2 done. (available before release: 0
 7 Thread 7 is using an ATM (available: 0)
 8 Thread 6 done. (available before release: 0
 9 Thread 9 is using an ATM (available: 0)
10 Thread 7 done. (available before release: 0
11 Thread 4 is using an ATM (available: 0)
12 Thread 3 done. (available before release: 0
13 Thread 10 is using an ATM (available: 0)
14 Thread 0 done. (available before release: 0
15 Thread 13 is using an ATM (available: 0)
16 Thread 1 done. (available before release: 0
17 Thread 5 is using an ATM (available: 0)
18 Thread 9 done. (available before release: 0
19 Thread 11 is using an ATM (available: 0)
20 Thread 13 done. (available before release: 0
21 Thread 8 is using an ATM (available: 0)
22 Thread 8 done. (available before release: 0
23 Thread 4 done. (available before release: 0
24 Thread 10 done. (available before release: 0
25 Thread 14 is using an ATM (available: 0)
26 Thread 12 is using an ATM (available: 0)
27 Thread 5 done. (available before release: 1
28 Thread 11 done. (available before release: 1
29 Thread 14 done. (available before release: 3
30 Thread 12 done. (available before release: 4

The semaphore limits concurrent access to the shared connections while allowing serialization of extra requests.

30.6 QSemaphoreReleaser

QSemaphoreReleaser5 is a convenience class that simplifies semaphore usage with RAII-style automatic release. It acquires the semaphore separately but ensures release when the releaser object is destroyed (e.g., goes out of scope), similar to QMutexLocker for mutexes. This helps prevent leaks if exceptions occur during resource usage.

An icon of a clipboard-list1

In your multithreaded banking application, you decide to use QSemaphoreReleaser to automatically handle releasing the semaphore without manual calls in a finally block.

To use QSemaphoreReleaser in your application:

  1. In the AtmPool class, modify the use_atm method: Call acquire() first, then create a QSemaphoreReleaser(self.semaphore) immediately after. The releaser will automatically call release() when it goes out of scope at the end of the method.

  2. The rest of the code remains the same as in the basic semaphore example.

This approach makes the code cleaner and exception-safe, ensuring the semaphore is always released even if an error occurs during the withdrawal.

30.7 QWaitCondition


  1. If you don’t implement __init__() in your class at all, the parent class gets initalized automatically.↩︎

  2. https://doc.qt.io/qtforpython-6/↩︎

  3. https://doc.qt.io/↩︎

  4. https://doc.qt.io/qt-6/qobject.html↩︎

  5. https://doc.qt.io/qt-6/qcolordialog.html↩︎