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 from PySide6.QtCore import QObject, QThread, Slot
 2 
 3 # 1. Create the BankAccount class
 4 
 5 class BankAccount(QObject):
 6     
 7     def __init__(self, balance, parent=None):
 8         super().__init__(parent)
 9         self.balance = balance
10     
11     @Slot(result=int)
12     def get_balance(self):
13         return self.balance
14     
15     def withdraw(self, amount):
16         print('---withdraw start---',
17             QThread.currentThread().objectName())
18         balance = self.balance
19         QThread.msleep(1)
20         #print(end='')
21         #loop = QEventLoop()
22         #QTimer.singleShot(1000, loop.quit)
23         if balance >= amount:
24             balance -= amount
25             self.balance = balance
26         print('---withdraw end---',
27             QThread.currentThread().objectName(), self.balance)
  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 from PySide6.QtCore import QObject, Signal
 2 
 3 # 2. Create the worker class
 4 
 5 class Worker(QObject):
 6     
 7     finished = Signal()
 8 
 9     def __init__(self, bank_account, amount, parent=None):
10         super().__init__(parent)
11         self.bank_account = bank_account
12         self.amount = amount
13 
14     def process(self):
15         self.bank_account.withdraw(self.amount)
16         self.finished.emit()
  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 import sys
 2 from PySide6.QtCore import QThread, QEventLoop, QTimer
 3 from PySide6.QtWidgets import (QApplication, 
 4     QWidget, QPushButton, QVBoxLayout, QLabel)
 5 from bankaccount import BankAccount
 6 from worker import Worker
 7 
 8 
 9 class Window(QWidget):
10     
11     def __init__(self):
12 
13         super().__init__()
14         
15         # 3. Simulate a joint bank account
16         
17         self.thread_count = 5
18         self.amount = 100
19 
20         self.label = QLabel('Click to start')
21         self.button = QPushButton('Withdraw money')
22         self.button.clicked.connect(self.start_threads)
23 
24         layout = QVBoxLayout()
25         layout.addWidget(self.label)
26         layout.addWidget(self.button)
27         self.setLayout(layout)
28 
29         self.workers = []
30 
31     def start_threads(self):
32 
33         self.button.setEnabled(False)
34 
35         self.bank_account = BankAccount(
36             self.thread_count * self.amount)
37         self.bank_account_thread = QThread()
38         self.bank_account_thread.setObjectName(
39             'Bank account thread')
40         self.bank_account.moveToThread(
41             self.bank_account_thread)
42         self.bank_account_thread.start()
43         
44         self.completed = 0
45 
46         self.workers.clear()
47 
48         for i in range(self.thread_count):
49             background_thread = QThread(self)
50             background_thread.setObjectName(f'Thread {i}')
51 
52             worker_obj = Worker(self.bank_account, self.amount)
53             self.workers.append(worker_obj)
54             worker_obj.moveToThread(background_thread)
55             
56             worker_obj.finished.connect(self.on_worker_done)
57     
58             background_thread.started.connect(worker_obj.process)
59             worker_obj.finished.connect(background_thread.quit)
60             worker_obj.finished.connect(worker_obj.deleteLater)
61             background_thread.finished.connect(
62                 background_thread.deleteLater)
63             
64             background_thread.start()
65             
66     def on_worker_done(self):
67         
68         self.completed += 1
69 
70         if self.completed == self.thread_count:
71             print('Expected: 0, Got:', self.bank_account.balance)
72             self.label.setText('Final balance: ' +
73                 str(self.bank_account.balance))
74             self.bank_account_thread.quit()
75             self.bank_account_thread.wait()
76             self.button.setEnabled(True)
77             print('=====================')
78 
79 
80 if __name__ == '__main__':
81     
82     app = QApplication(sys.argv)
83     main_window = Window()
84     main_window.show()
85     sys.exit(app.exec())
  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 from PySide6.QtCore import QObject, Signal
 2 
 3 class Worker(QObject):
 4     
 5     transactionProcessed = Signal()
 6     requestUpdate = Signal(int)
 7 
 8     def __init__(self, bank_account, amount, parent=None):
 9         super().__init__(parent)
10         self.bank_account = bank_account
11         self.amount = amount
12         self.requestUpdate.connect(self.bank_account.withdraw)
13 
14     def process(self):
15         self.requestUpdate.emit(self.amount)
16         self.transactionProcessed.emit()
  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 from PySide6.QtCore import QObject, QThread, Signal, Slot
 2 
 3 class BankAccount(QObject):
 4     
 5     balanceSent = Signal(int)
 6     
 7     def __init__(self, balance, parent=None):
 8         super().__init__(parent)
 9         self.balance = balance
10     
11     @Slot(int)
12     def withdraw(self, amount):
13         print('---withdraw start---',
14             QThread.currentThread().objectName())
15         balance = self.balance
16         if balance >= amount:
17             balance -= amount
18             QThread.msleep(1)
19             self.balance = balance
20         print('---withdraw end---',
21             QThread.currentThread().objectName())
22         
23     @Slot()
24     def send_balance(self):
25         self.balanceSent.emit(self.balance)
  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 import sys
 2 from PySide6.QtCore import Signal, QThread
 3 from PySide6.QtWidgets import (QApplication, 
 4     QWidget, QPushButton, QVBoxLayout, QLabel)
 5 from bankaccount import BankAccount
 6 from worker import Worker
 7 
 8 
 9 class Window(QWidget):
10     
11     requestBalance = Signal()
12     
13     def __init__(self):
14 
15         super().__init__()
16         
17         self.thread_count = 5
18         self.amount = 100
19         
20         self.label = QLabel('Click to start')
21         self.button = QPushButton('Withdraw money')
22         self.button.clicked.connect(self.start_threads)
23 
24         layout = QVBoxLayout()
25         layout.addWidget(self.label)
26         layout.addWidget(self.button)
27         self.setLayout(layout)
28 
29         self.workers = []
30 
31     def start_threads(self):
32 
33         self.button.setEnabled(False)
34 
35         self.bank_account = BankAccount(self.thread_count * self.amount)
36         self.bank_account_thread = QThread()
37         self.bank_account_thread.setObjectName('Bank account thread')
38         self.bank_account.moveToThread(self.bank_account_thread)
39         self.requestBalance.connect(self.bank_account.send_balance)
40         self.bank_account.balanceSent.connect(self.on_balance_sent)
41         self.bank_account_thread.start()
42 
43         self.completed = 0
44 
45         self.workers.clear()
46 
47         for i in range(self.thread_count):
48             background_thread = QThread(self)
49             background_thread.setObjectName(f'Thread {i}')
50 
51             worker_obj = Worker(self.bank_account, self.amount)
52             self.workers.append(worker_obj)
53             worker_obj.moveToThread(background_thread)
54             
55             worker_obj.transactionProcessed.connect(self.on_worker_done)
56     
57             background_thread.started.connect(worker_obj.process)
58             worker_obj.transactionProcessed.connect(background_thread.quit)
59             worker_obj.transactionProcessed.connect(worker_obj.deleteLater)
60             background_thread.finished.connect(background_thread.deleteLater)
61             
62             background_thread.start()
63             
64     def on_worker_done(self):
65         self.completed += 1
66         if self.completed == self.thread_count:
67             self.requestBalance.emit()
68 
69             
70     def on_balance_sent(self, balance):
71 
72         self.bank_account_thread.quit()
73         self.bank_account_thread.wait()
74         print('Expected: 0, Got:', balance)
75         self.label.setText(f'Final balance: {balance}')
76         self.button.setEnabled(True)
77 
78 
79 if __name__ == '__main__':
80     
81     app = QApplication(sys.argv)
82     main_window = Window()
83     main_window.show()
84     sys.exit(app.exec())
  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 program[^1].

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 called[^2].

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 from PySide6.QtCore import QObject, Signal
 2 
 3 class Worker(QObject):
 4     
 5     finished = Signal()
 6 
 7     def __init__(self, bank_account, amount, parent=None):
 8         super().__init__(parent)
 9         self.bank_account = bank_account
10         self.amount = amount
11 
12     def process(self):
13         self.bank_account.withdraw(self.amount)
14         self.finished.emit()
  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 from PySide6.QtCore import QObject ,QThread, QMutex, Slot
 2 
 3 class BankAccount(QObject):
 4     
 5     def __init__(self, balance, parent=None):
 6         super().__init__(parent)
 7         self.balance = balance
 8         self.mutex = QMutex()
 9     
10     @Slot(result=int)
11     def get_balance(self):
12         return self.balance
13     
14     def withdraw(self, amount):
15         self.mutex.lock()
16         print('---withdraw start---',
17             QThread.currentThread().objectName())
18         balance = self.balance
19         QThread.msleep(1)
20         #print(end='')
21         #loop = QEventLoop()
22         #QTimer.singleShot(1000, loop.quit)
23         if balance >= amount:
24             balance -= amount
25             self.balance = balance
26         print('---withdraw end---',
27             QThread.currentThread().objectName(), self.balance)
28         self.mutex.unlock()
  1. Create the BankAccount class. In its withdraw() method, guard the code that updates the account balance with the lock() and unlock() calls.
 1 import sys
 2 from PySide6.QtCore import QThread
 3 from PySide6.QtWidgets import (QApplication, 
 4     QWidget, QPushButton, QVBoxLayout, QLabel)
 5 from bankaccount import BankAccount
 6 from worker import Worker
 7 
 8 class Window(QWidget):
 9     
10     def __init__(self):
11 
12         super().__init__()
13         
14         self.thread_count = 5
15         self.amount = 100
16 
17         self.label = QLabel('Click to start')
18         self.button = QPushButton('Withdraw money')
19         self.button.clicked.connect(self.start_threads)
20 
21         layout = QVBoxLayout()
22         layout.addWidget(self.label)
23         layout.addWidget(self.button)
24         self.setLayout(layout)
25 
26         self.workers = []
27 
28     def start_threads(self):
29 
30         self.button.setEnabled(False)
31 
32         self.bank_account = BankAccount(
33             self.thread_count * self.amount)
34         self.bank_account_thread = QThread()
35         self.bank_account_thread.setObjectName(
36             'Bank account thread')
37         self.bank_account.moveToThread(
38             self.bank_account_thread)
39         self.bank_account_thread.start()
40         
41         self.completed = 0
42 
43         self.workers.clear()
44 
45         for i in range(self.thread_count):
46             background_thread = QThread(self)
47             background_thread.setObjectName(f'Thread {i}')
48 
49             worker_obj = Worker(self.bank_account, self.amount)
50             self.workers.append(worker_obj)
51             worker_obj.moveToThread(background_thread)
52             
53             worker_obj.finished.connect(self.on_worker_done)
54     
55             background_thread.started.connect(worker_obj.process)
56             worker_obj.finished.connect(background_thread.quit)
57             worker_obj.finished.connect(worker_obj.deleteLater)
58             background_thread.finished.connect(
59                 background_thread.deleteLater)
60             
61             background_thread.start()
62             
63     def on_worker_done(self):
64         
65         self.completed += 1
66 
67         if self.completed == self.thread_count:
68             print('Expected: 0, Got:', self.bank_account.balance)
69             self.label.setText('Final counter: ' +
70                 str(self.bank_account.balance))
71             self.bank_account_thread.quit()
72             self.bank_account_thread.wait()
73             self.button.setEnabled(True)
74             print('=====================')
75 
76 
77 if __name__ == '__main__':
78     
79     app = QApplication(sys.argv)
80     main_window = Window()
81     main_window.show()
82     sys.exit(app.exec())

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

QMutexLocker[^3] 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 from PySide6.QtCore import (QObject, QThread, QMutex,
 2     QMutexLocker, Slot)
 3 
 4 class BankAccount(QObject):
 5     
 6     def __init__(self, balance, parent=None):
 7         super().__init__(parent)
 8         self.balance = balance
 9         self.mutex = QMutex()
10     
11     @Slot(result=int)
12     def get_balance(self):
13         return self.balance
14     
15     def withdraw(self, amount):
16         locker = QMutexLocker(self.mutex)
17         print('---withdraw start---',
18             QThread.currentThread().objectName())
19         balance = self.balance
20         QThread.msleep(1)
21         #print(end='')
22         #loop = QEventLoop()
23         #QTimer.singleShot(1000, loop.quit)
24         if balance >= amount:
25             balance -= amount
26             self.balance = balance
27         print('---withdraw end---',
28             QThread.currentThread().objectName(), self.balance)
  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 done[^4]. 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 from random import randint
 2 from PySide6.QtCore import QObject, QSemaphore, QThread, Slot
 3 
 4 class AtmPool(QObject):
 5     
 6     def __init__(self, parent=None):
 7         super().__init__(parent)
 8         self.atm_count = 5        
 9         self.semaphore = QSemaphore(self.atm_count)
10     
11     @Slot()
12     def use_atm(self):
13         self.semaphore.acquire()
14         try:
15             print(QThread.currentThread().objectName() +
16                 ' is using an ATM ' +
17                 '(available: ' +
18                 str(self.semaphore.available()) + ')')
19             QThread.msleep(randint(10, 200))
20         finally:
21             print(QThread.currentThread().objectName() +
22                 ' done. (available before release: ' +
23                 str(self.semaphore.available()))
24             self.semaphore.release()
  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 from PySide6.QtCore import QObject, Signal
 2 
 3 class Worker(QObject):
 4     
 5     finished = Signal()
 6 
 7     def __init__(self, atm_pool, parent=None):
 8         super().__init__(parent)
 9         self.atm_pool = atm_pool
10 
11     def process(self):
12         self.atm_pool.use_atm()
13         self.finished.emit()
  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.
 1 import sys
 2 from PySide6.QtCore import QThread
 3 from PySide6.QtWidgets import (QApplication, 
 4     QWidget, QPushButton, QVBoxLayout, QLabel)
 5 from atmpool import AtmPool
 6 from worker import Worker
 7 
 8 class Window(QWidget):
 9     
10     def __init__(self):
11 
12         super().__init__()
13         
14         self.thread_count = 15
15 
16         self.label = QLabel('Click to start', self)
17         self.button = QPushButton('Withdraw Money', self)
18         self.button.clicked.connect(self.start_threads)
19 
20         layout = QVBoxLayout(self)
21         layout.addWidget(self.label)
22         layout.addWidget(self.button)
23 
24         self.workers = []
25 
26     def start_threads(self):
27 
28         self.button.setEnabled(False)
29 
30         self.atm_pool = AtmPool()
31         self.atm_pool_thread = QThread()
32         self.atm_pool_thread.setObjectName('Atm Pool thread')
33         self.atm_pool.moveToThread(self.atm_pool_thread)
34         self.atm_pool_thread.start()
35         
36         self.completed = 0
37 
38         self.workers.clear()
39 
40         for i in range(self.thread_count):
41             background_thread = QThread(self)
42             background_thread.setObjectName(f'Thread {i}')
43 
44             worker_obj = Worker(self.atm_pool)
45             self.workers.append(worker_obj)
46             worker_obj.moveToThread(background_thread)
47             
48             worker_obj.finished.connect(self.on_worker_done)
49     
50             background_thread.started.connect(worker_obj.process)
51             worker_obj.finished.connect(background_thread.quit)
52             worker_obj.finished.connect(worker_obj.deleteLater)
53             background_thread.finished.connect(
54                 background_thread.deleteLater)
55             
56             background_thread.start()
57             
58     def on_worker_done(self):
59         
60         self.completed += 1
61 
62         if self.completed == self.thread_count:
63             self.atm_pool_thread.quit()
64             self.atm_pool_thread.wait()
65             self.button.setEnabled(True)
66 
67 
68 if __name__ == '__main__':
69     
70     app = QApplication(sys.argv)
71     main_window = Window()
72     main_window.show()
73     sys.exit(app.exec())

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

QSemaphoreReleaser[^5] 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 from random import randint
 2 from PySide6.QtCore import (QObject, QSemaphore,
 3     QSemaphoreReleaser, QThread, Slot)
 4 
 5 class AtmPool(QObject):
 6     
 7     def __init__(self, parent=None):
 8 
 9         super().__init__(parent)
10         self.atm_count = 5
11         self.semaphore = QSemaphore(self.atm_count)
12 
13     @Slot()
14     def use_atm(self):
15         self.semaphore.acquire()
16         releaser = QSemaphoreReleaser(self.semaphore)
17         print(QThread.currentThread().objectName() +
18             ' is using an ATM' +
19             ' (available: ' +
20             str(self.semaphore.available()) + ')')
21         QThread.msleep(randint(10, 200))
22         print(QThread.currentThread().objectName() +
23             ' done ' +
24             '(available before release: ' +
25             str(self.semaphore.available()) + ')')
26         #del releaser
  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