Thread Synchronization0%

Thread Synchronization

Beginner12 min readUpdated: Jul 11, 2026
Study Materials

Thread Synchronization

When multiple threads execute concurrently within the same process, they share a unified memory space. While this shared heap enables fast inter-thread communication without serialization, it introduces severe hazards: race conditions, memory corruption, and deadlocks.

The threading module provides synchronization primitives—Lock, RLock, Semaphore, Event, and Condition—to coordinate shared access safely.


1. The Mechanics of a Race Condition

Even a seemingly trivial operation such as counter += 1 is not atomic in Python bytecode:

Output
Thread 1 Execution Flow Thread 2 Execution Flow
│ │
1. LOAD_FAST counter (reads 0) │
│ (Context switch to Thread 2) │
│ 1. LOAD_FAST counter (reads 0)
│ 2. BINARY_ADD 1 (evaluates 1)
│ 3. STORE_FAST counter (writes 1)
│ (Context switch back to Thread 1) │
2. BINARY_ADD 1 (evaluates 1) │
3. STORE_FAST counter (writes 1) │
│ │
Expected Counter Value: 2 ──► Actual Memory Value: 1 (Race Condition!)

Visual Architecture & Process Flow

How data and code flow step-by-step

Flowchart
Step 1
def unsafe_increment
Step 2
None:

2. Mutex Locks (threading.Lock)

A Mutex (Mutual Exclusion lock) ensures that only one thread can execute a critical section at any instant. Always acquire locks using the with statement to guarantee release:

Python
shared_safe_counter = 0
counter_lock = threading.Lock()
 
def safe_increment() -> None:
global shared_safe_counter
for _ in range(100_000):
with counter_lock: # Automatically acquires and releases
shared_safe_counter += 1
 
threads = [threading.Thread(target=safe_increment) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
 
print(f"Safe Expected: 500,000 | Safe Actual: {shared_safe_counter}") # Guaranteed 500,000

3. Reentrant Locks (threading.RLock)

A standard threading.Lock cannot be acquired more than once by the same thread. If a method holding a lock calls another method that requests the same lock, execution deadlocks.

An RLock (Reentrant Lock) tracks the owning thread and an internal acquisition counter. The owning thread can acquire the lock multiple times without blocking, provided it releases it the same number of times:

Python
class AccountManager:
def __init__(self) -> None:
self.balance = 100
self._lock = threading.RLock() # Reentrant lock allows nested acquisition
 
def log_audit(self, message: str) -> None:
with self._lock:
print(f"[AUDIT] {message} (Balance: {self.balance})")
 
def withdraw(self, amount: int) -> None:
with self._lock:
# Invoking log_audit acquires the same lock again
self.log_audit(f"Withdrawing {amount}")
self.balance -= amount
 
account = AccountManager()
account.withdraw(25) # Succeeds without self-deadlock

4. Resource Throttling with threading.Semaphore

A Semaphore manages an internal counter. Every acquire() decrements the counter; every release() increments it. If the counter reaches zero, subsequent acquiring threads block. This is ideal for limiting concurrent access to rate-limited APIs or database connection pools:

Visual Architecture & Process Flow

How data and code flow step-by-step

Flowchart
Step 1
def access_database_pool
client_id: int
Step 2
None:

5. Signaling with threading.Event

An Event manages an internal boolean flag (False by default). One thread can signal other threads to proceed by calling event.set(), while consumer threads pause via event.wait():

Python
server_ready_event = threading.Event()
 
def initialize_database():
print("[INIT] Loading schema, configurations, and cache...")
time.sleep(0.3)
print("[INIT] System initialization complete!")
server_ready_event.set() # Flips flag to True; wakes up all waiting threads
 
def api_listener(worker_id: int):
print(f"[WORKER {worker_id}] Waiting for database to become ready...")
server_ready_event.wait() # Blocks until event.set() is called
print(f"[WORKER {worker_id}] Database ready. Accepting client connections.")
 
init_thread = threading.Thread(target=initialize_database)
workers = [threading.Thread(target=api_listener, args=(i,)) for i in range(3)]
 
for w in workers: w.start()
init_thread.start()
 
init_thread.join()
for w in workers: w.join()

6. Architectural Summary Table

PrimitiveMechanismPrimary Use Case
LockBinary mutex (locked / unlocked)Protecting critical sections and shared mutable data
RLockReentrant mutex with recursion counterRecursive function calls or nested class methods
SemaphoreCounter-based permitsThrottling concurrency (connection pools, rate limits)
EventBoolean flag (wait / set / clear)One-to-many thread signaling and coordination
ConditionLock associated with a wait queueComplex producer-consumer pipelines

Multiple Choice Questions

1.

Why does counter += 1 lead to race conditions in multithreaded Python despite the Global Interpreter Lock (GIL)? A. The GIL is disabled in loops. B. The += operation compiles down to multiple bytecode instructions (LOAD_FAST, BINARY_ADD, STORE_FAST), and thread switching can occur between them. C. Integers in Python are stored on disk. D. Hardware threads always ignore the GIL.

Answer: B
Explanation:In CPython, += is not atomic at the bytecode level. The interpreter can switch threads after reading the variable but before writing the updated value back, resulting in lost updates.

2.

What will happen if a thread that already holds a standard threading.Lock attempts to acquire that same lock a second time? A. The second attempt returns True immediately. B. The lock is released. C. The thread blocks waiting for itself to release the lock, causing a deadlock. D. A TypeError is raised.

Answer: C
Explanation:A standard threading.Lock is non-reentrant; if the owning thread attempts to re-acquire it, it blocks waiting for the lock to become free, permanently deadlocking itself.

3.

Which synchronization primitive should be chosen when a single thread needs to acquire the same lock multiple times in recursive or nested function calls? A. threading.Lock B. threading.RLock C. threading.Event D. threading.Barrier

Answer: B
Explanation:threading.RLock (Reentrant Lock) tracks the identity of the owning thread and its acquisition depth, allowing the owner to acquire it multiple times without blocking.

4.

What is the primary function of a threading.Semaphore(value=5)? A. To guarantee that exactly 5 threads terminate at the same time. B. To allow up to 5 concurrent threads to hold the resource simultaneously before blocking additional requests. C. To create 5 separate memory heaps. D. To broadcast a stop signal to 5 worker threads.

Answer: B
Explanation:A Semaphore maintains an internal counter initialized to $N$ that allows up to $N$ simultaneous acquisitions, effectively throttling access to limited resources.

5.

Which method on a threading.Event object is used by waiting worker threads to block until a signal flag is set to True? A. event.set() B. event.wait() C. event.listen() D. event.block()

Answer: B
Explanation:event.wait() pauses the calling thread until the event's internal boolean flag is set to True via a call to event.set().

Next Lesson

Daemon vs Non-Daemon Threads

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Related Lessons

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext