Skip to main content

Race Conditions & Thread Synchronization with Locks

When multiple threads read and write to the exact same global variable or shared data structure at the exact same millisecond, a catastrophic bug called a Race Condition occurs.


1. The Race Condition: The Shared Bank Account Bug

Imagine two family members using two ATM debit cards connected to one joint bank balance of ₹1,000 at the exact same microsecond:

import threading
import time

# Shared global bank balance
bank_balance = 1000

def withdraw_money(amount, person_name):
global bank_balance
if bank_balance >= amount:
print(f"✅ {person_name} approved! Withdrawing ₹{amount}...")
time.sleep(0.1) # Simulate cash dispenser latency
bank_balance -= amount
print(f"💰 {person_name} finished! Remaining balance: ₹{bank_balance}")
else:
print(f"❌ {person_name} declined: Insufficient funds!")

# Both threads attempt to withdraw ₹800 simultaneously
t1 = threading.Thread(target=withdraw_money, args=(800, "Rahul"))
t2 = threading.Thread(target=withdraw_money, args=(800, "Priya"))

t1.start()
t2.start()
t1.join()
t2.join()

print(f"🚨 Final Bank Balance: ₹{bank_balance}")
# Output can result in -₹600! Overdraft disaster!

2. Fixing the Bug with threading.Lock() (Mutual Exclusion)

A Lock (Mutex) ensures that only one thread is allowed to access the critical section at a time.

All other threads are placed on hold in a queue until the active thread releases the lock:

import threading
import time

bank_balance = 1000
# Create the security lock
balance_lock = threading.Lock()

def safe_withdraw(amount, person_name):
global bank_balance

# Acquire the lock using Python context manager
with balance_lock:
print(f"🔒 Lock acquired by {person_name}")
if bank_balance >= amount:
time.sleep(0.1)
bank_balance -= amount
print(f"✅ {person_name} withdrew ₹{amount}. Remaining: ₹{bank_balance}")
else:
print(f"❌ {person_name} declined: Insufficient funds!")
# Lock is automatically released here

t1 = threading.Thread(target=safe_withdraw, args=(800, "Rahul"))
t2 = threading.Thread(target=safe_withdraw, args=(800, "Priya"))

t1.start()
t2.start()
t1.join()
t2.join()
print(f"🎉 Final Safe Bank Balance: ₹{bank_balance}") # Correctly prints ₹200!

3. Manual acquire() vs. with lock:

PatternSyntaxRisk
Manuallock.acquire() ... lock.release()If an error occurs before release, program suffers Deadlock (freezes forever)
Context Managerwith lock:Automatically releases lock even if an unexpected exception occurs

Quick Summary

  • Race Conditions: Bug where multiple concurrent threads read and mutate shared memory out of order, corrupting data.
  • threading.Lock() (Mutex): Ensures mutual exclusion so only 1 thread executes a critical section at any moment.
  • Context Manager (with lock:): Safely acquires and auto-releases locks, preventing catastrophic program Deadlocks.

What's Next?

Let's compare multithreading with full multi-core parallel processing in Multiprocessing vs. Multithreading!