Practical Examples and Solutions
To better illustrate the concepts of race conditions and the techniques to prevent them, let's explore some practical examples and solutions.
Example 1: Counter Increment
Suppose we have a shared counter that multiple threads need to increment. Without proper synchronization, a race condition can occur, leading to an incorrect final count.
import threading
## Shared counter
counter = 0
def increment_counter():
global counter
for _ in range(1000000):
counter += 1
## Create and start the threads
threads = [threading.Thread(target=increment_counter) for _ in range(4)]
for thread in threads:
thread.start()
## Wait for all threads to finish
for thread in threads:
thread.join()
print(f"Final counter value: {counter}")
To prevent the race condition, we can use a mutex to ensure that only one thread can access the shared counter at a time:
import threading
## Shared counter
counter = 0
lock = threading.Lock()
def increment_counter():
global counter
for _ in range(1000000):
with lock:
counter += 1
## Create and start the threads
threads = [threading.Thread(target=increment_counter) for _ in range(4)]
for thread in threads:
thread.start()
## Wait for all threads to finish
for thread in threads:
thread.join()
print(f"Final counter value: {counter}")
Example 2: Shared Bank Account
Consider a scenario where multiple threads are accessing a shared bank account. Without proper synchronization, a race condition can occur, leading to incorrect account balances.
import threading
## Shared bank account
balance = 1000
def withdraw(amount):
global balance
if balance >= amount:
balance -= amount
print(f"Withdrew {amount}, new balance: {balance}")
else:
print("Insufficient funds")
def deposit(amount):
global balance
balance += amount
print(f"Deposited {amount}, new balance: {balance}")
## Create and start the threads
withdraw_thread = threading.Thread(target=withdraw, args=(500,))
deposit_thread = threading.Thread(target=deposit, args=(200,))
withdraw_thread.start()
deposit_thread.start()
## Wait for all threads to finish
withdraw_thread.join()
deposit_thread.join()
To prevent the race condition, we can use a mutex to ensure that only one thread can access the shared bank account at a time:
import threading
## Shared bank account
balance = 1000
lock = threading.Lock()
def withdraw(amount):
global balance
with lock:
if balance >= amount:
balance -= amount
print(f"Withdrew {amount}, new balance: {balance}")
else:
print("Insufficient funds")
def deposit(amount):
global balance
with lock:
balance += amount
print(f"Deposited {amount}, new balance: {balance}")
## Create and start the threads
withdraw_thread = threading.Thread(target=withdraw, args=(500,))
deposit_thread = threading.Thread(target=deposit, args=(200,))
withdraw_thread.start()
deposit_thread.start()
## Wait for all threads to finish
withdraw_thread.join()
deposit_thread.join()
These examples demonstrate how race conditions can occur in Python multithreading and how to use synchronization techniques, such as mutexes, to prevent them. By understanding and applying these concepts, you can write more reliable and robust concurrent applications in Python.