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
![]() |
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:
- Create a class named
BankAccountto hold the user account balance, and add two methods,get_balanceto return the current balance, andwithdrawto let the user withraw the specified amount of money from the account. In the demonstration we useQThread.msleep()to simulate network lag.
- Create the
Workerclass. We will move this class to a background thread and call itsprocess()method to process the user request.
- 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 singleBankAccountobject 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.
![]() |
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. |
- Create the
Workerclass. This worker version has two signals,requestUpdate(int)which requests that theBankAccountupdate the balance, andtransactionProcessed()which signals that the transaction is processed. We connectWorker.requestUpdate()toBankAccount.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.
- Create the
BankAccountclass. It is mostly unchanged except that we add a signal namedbalanceSent()to it. The signal will be used to send the final balance to the GUI thread.
- 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 thebalanceSent()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.
![]() |
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:
- 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()andrequestUpdate()signals.
- Create the
BankAccountclass. In itswithdraw()method, guard the code that updates the account balance with thelock()andunlock()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.
![]() |
You use a mutex to prevent race conditions in your multithreaded banking application. You decide to use |
To use a QMutexLocker in your application:
In the
BankAccountclass add a mutex instance variable.In the
withdraw()method, create aQMutexLockerlocal variable just before the code that updates the balance. Whenwithdraw()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.
![]() |
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:
Create the class that represents the shared resource,
AtmPoolin the example. Initialize a QSemaphore with the number of available resources (e.g., 5 connections).In the
AtmPoolclass, add ause_atm()slot that acquires the semaphore before using the resource (simulating a withdrawal with a random sleep), and releases it in afinallyblock to ensure it’s always freed.
- Create the
Workerclass, which callsuse_atm()in itsprocess()method to simulate a customer using an ATM. In the main window class, create theAtmPoolobject 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.
![]() |
In your multithreaded banking application, you decide to use |
To use QSemaphoreReleaser in your application:
In the
AtmPoolclass, modify theuse_atmmethod: Callacquire()first, then create aQSemaphoreReleaser(self.semaphore)immediately after. The releaser will automatically callrelease()when it goes out of scope at the end of the method.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.
