- What is thread synchronization ā preventing race conditions
- Locks (Mutex) ā the most basic synchronization tool
- Reentrant Locks ā locks that can be reacquired
- Semaphores ā controlling access to limited resources
- Condition Variables ā waiting for specific conditions
- Events and Barriers ā thread coordination
What is Thread Synchronization?
Thread synchronization is the process of coordinating thread execution to prevent conflicts when multiple threads access shared resources. Without synchronization, threads can interfere with each other, causing data corruption and unpredictable behavior.
š¦ Think of it like a traffic intersection.
Without traffic lights, cars crash into each other. With traffic lights, cars take turns and pass safely. Thread synchronization is like traffic lights for your code ā it ensures threads take turns accessing shared resources.
š Locks
Exclusive access to resources
š Reentrant Locks
Lock that can be reacquired
š Semaphores
Controlled concurrent access
š¢ Events & Barriers
Thread coordination
š” Key concept: Synchronization prevents race conditions and ensures data consistency in multi-threaded programs.
Race Conditions
What is a Race Condition?
A race condition occurs when multiple threads access shared data concurrently and at least one thread modifies it, leading to unexpected results due to timing interleaving.
# ============================================================
# RACE CONDITION EXAMPLE
# ============================================================
import threading
import time
# Shared counter
counter = 0
def increment_counter():
"""Increment counter (NOT thread-safe)"""
global counter
temp = counter # Read
time.sleep(0.000001) # Simulate context switch
counter = temp + 1 # Write
print("=" * 40)
print("RACE CONDITION DEMONSTRATION")
print("=" * 40)
print(f"Initial counter: {counter}")
# Create threads
threads = []
for _ in range(100):
t = threading.Thread(target=increment_counter)
threads.append(t)
# Start threads
for t in threads:
t.start()
# Wait for threads
for t in threads:
t.join()
print(f"Final counter: {counter}")
print(f"Expected: 100")
print("""
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā OBSERVATIONS: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠Counter should be 100 but may be less ā
ā ⢠Threads interfered with each other ā
ā ⢠This is a classic race condition ā
ā ⢠Need synchronization to fix it ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
""")
Race condition key points:
- Multiple threads ā access shared data
- Interleaving ā unpredictable timing
- Corruption ā data inconsistency
- Prevention ā use synchronization
Quick Check: What is a race condition? (Answer: When threads interfere with each other, causing data corruption)
Locks (Mutex)
Exclusive Access with Locks
A lock (mutex) ensures that only one thread can access a shared resource at a time. Threads acquire the lock before accessing the resource and release it afterward.
# ============================================================
# USING LOCKS FOR SYNCHRONIZATION
# ============================================================
import threading
import time
# Shared counter
counter = 0
lock = threading.Lock()
def increment_counter():
"""Increment counter WITH lock (thread-safe)"""
global counter
lock.acquire() # Acquire lock
try:
temp = counter
time.sleep(0.000001)
counter = temp + 1
finally:
lock.release() # Always release lock
print("=" * 40)
print("THREAD SAFE WITH LOCK")
print("=" * 40)
print(f"Initial counter: {counter}")
# Create threads
threads = []
for _ in range(100):
t = threading.Thread(target=increment_counter)
threads.append(t)
# Start threads
for t in threads:
t.start()
# Wait for threads
for t in threads:
t.join()
print(f"Final counter: {counter}")
print(f"Expected: 100 ā")
# ============================================================
# LOCK WITH CONTEXT MANAGER
# ============================================================
print("\n" + "=" * 40)
print("LOCK WITH CONTEXT MANAGER")
print("=" * 40)
counter2 = 0
lock2 = threading.Lock()
def increment_with_context():
global counter2
with lock2: # Context manager automatically releases
temp = counter2
time.sleep(0.000001)
counter2 = temp + 1
threads = []
for _ in range(100):
t = threading.Thread(target=increment_with_context)
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Final counter (with context): {counter2}")
print("""
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā LOCK (MUTEX) ADVANTAGES: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠Simple to use ā
ā ⢠Guarantees exclusive access ā
ā ⢠Prevents race conditions ā
ā ⢠Works with context manager (`with lock`) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā LOCK DISADVANTAGES: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠Can cause deadlocks if not careful ā
ā ⢠Performance overhead ā
ā ⢠Not reentrant (can't be reacquired) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
""")
Lock key points:
- Exclusive ā only one thread at a time
- Simple ā easy to implement
- Context manager ā use `with lock`
- Deadlock risk ā must release properly
Quick Check: What does a lock (mutex) do? (Answer: Ensures exclusive access to a shared resource)
Reentrant Locks
Locks That Can Be Reacquired
A reentrant lock (RLock) allows a thread to acquire the same lock multiple times without deadlocking. This is useful when a function that holds a lock calls another function that also needs the lock.
# ============================================================
# REENTRANT LOCKS
# ============================================================
import threading
import time
# Regular lock vs reentrant lock
regular_lock = threading.Lock()
reentrant_lock = threading.RLock()
print("=" * 40)
print("REENTRANT LOCK EXAMPLE")
print("=" * 40)
def function_a():
"""Function that acquires a lock"""
print(" function_a: acquiring lock...")
reentrant_lock.acquire()
print(" function_a: lock acquired")
function_b() # Calls function_b
print(" function_a: releasing lock...")
reentrant_lock.release()
def function_b():
"""Function that also needs the same lock"""
print(" function_b: acquiring lock...")
reentrant_lock.acquire()
print(" function_b: lock acquired")
print(" function_b: doing work...")
time.sleep(0.1)
print(" function_b: releasing lock...")
reentrant_lock.release()
# This works with RLock
print("\nUsing RLock (reentrant):")
t = threading.Thread(target=function_a)
t.start()
t.join()
print("\nā RLock works because it tracks the owning thread")
# ============================================================
# RLock vs Lock Comparison
# ============================================================
print("\n" + "=" * 40)
print("RLock vs Lock")
print("=" * 40)
# RLock can be reacquired
lock_r = threading.RLock()
with lock_r:
print(" RLock: first acquire")
with lock_r: # This works!
print(" RLock: second acquire (reentrant)")
# Lock cannot be reacquired
lock_l = threading.Lock()
with lock_l:
print(" Lock: first acquire")
try:
with lock_l: # This would deadlock
print(" This won't execute")
except Exception as e:
print(f" Lock: {type(e).__name__} - cannot reacquire")
print("""
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā RLock ADVANTAGES: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠Can be reacquired by the same thread ā
ā ⢠Prevents deadlocks in nested calls ā
ā ⢠Tracks acquisition count ā
ā ⢠Same interface as Lock ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā RLock DISADVANTAGES: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠Slightly more overhead ā
ā ⢠Can hide design problems ā
ā ⢠Only use when needed ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
""")
Reentrant lock key points:
- Reentrant ā same thread can acquire multiple times
- Tracks owner ā knows which thread holds it
- Counts acquires ā must release same number
- Nested calls ā prevents deadlocks
Quick Check: When should you use RLock instead of Lock? (Answer: When a function that holds a lock calls another function that also needs the lock)
Semaphores
Controlling Access to Limited Resources
A semaphore limits the number of threads that can access a resource simultaneously. It's like a permit system ā threads acquire permits before using the resource and release them when done.
# ============================================================
# SEMAPHORE EXAMPLE
# ============================================================
import threading
import time
import random
# Semaphore with capacity 3 (max 3 concurrent threads)
semaphore = threading.Semaphore(3)
def use_resource(thread_id):
"""Use a limited resource with semaphore control"""
print(f"Thread {thread_id}: Waiting for permit...")
semaphore.acquire()
try:
print(f"Thread {thread_id}: Got permit! Using resource...")
time.sleep(random.uniform(0.5, 1.5))
print(f"Thread {thread_id}: Done using resource.")
finally:
semaphore.release()
print(f"Thread {thread_id}: Released permit.")
print("=" * 40)
print("SEMAPHORE DEMONSTRATION")
print("=" * 40)
print("Semaphore capacity: 3 (max 3 concurrent threads)")
print()
# Create 8 threads
threads = []
for i in range(8):
t = threading.Thread(target=use_resource, args=(i,))
threads.append(t)
# Start all threads
for t in threads:
t.start()
# Wait for all threads
for t in threads:
t.join()
print("\nAll threads completed!")
# ============================================================
# BOUNDED SEMAPHORE
# ============================================================
print("\n" + "=" * 40)
print("BOUNDED SEMAPHORE")
print("=" * 40)
# BoundedSemaphore prevents releasing more than acquired
bounded = threading.BoundedSemaphore(2)
print("BoundedSemaphore capacity: 2")
print("Acquiring lock...")
bounded.acquire()
print("Lock acquired (1/2)")
bounded.acquire()
print("Lock acquired (2/2)")
print("\nReleasing lock...")
bounded.release()
print("Lock released (1/2)")
bounded.release()
print("Lock released (2/2)")
try:
print("\nAttempting extra release...")
bounded.release() # This raises ValueError
except ValueError as e:
print(f"ValueError: {e}")
print("""
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā SEMAPHORE ADVANTAGES: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠Limits concurrent resource usage ā
ā ⢠Configurable capacity ā
ā ⢠Bounded version prevents errors ā
ā ⢠Great for connection pools ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā SEMAPHORE DISADVANTAGES: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠More complex than locks ā
ā ⢠Must manage permits carefully ā
ā ⢠BoundedSemaphore needed for safety ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
""")
Semaphore key points:
- Limits concurrency ā controls how many threads
- Permit system ā acquire/release permits
- BoundedSemaphore ā prevents over-release
- Resource pools ā database connections, etc.
Quick Check: What is a semaphore used for? (Answer: Limiting the number of threads accessing a resource)
Condition Variables
Waiting for Specific Conditions
A condition variable allows threads to wait until a specific condition is met. It combines a lock with wait/notify functionality, making it perfect for producer-consumer scenarios.
# ============================================================
# CONDITION VARIABLE - PRODUCER CONSUMER
# ============================================================
import threading
import time
import random
class BoundedBuffer:
"""Producer-Consumer with Condition Variable"""
def __init__(self, capacity=5):
self.buffer = []
self.capacity = capacity
self.condition = threading.Condition()
def produce(self, item):
"""Add item to buffer (producer)"""
with self.condition:
while len(self.buffer) >= self.capacity:
print(f" Buffer full. Producer waiting...")
self.condition.wait()
self.buffer.append(item)
print(f" Produced: {item} (buffer: {len(self.buffer)}/{self.capacity})")
self.condition.notify()
def consume(self):
"""Remove item from buffer (consumer)"""
with self.condition:
while len(self.buffer) == 0:
print(f" Buffer empty. Consumer waiting...")
self.condition.wait()
item = self.buffer.pop(0)
print(f" Consumed: {item} (buffer: {len(self.buffer)}/{self.capacity})")
self.condition.notify()
return item
# Create buffer
buffer = BoundedBuffer(capacity=3)
def producer_thread():
"""Producer thread"""
for i in range(6):
time.sleep(random.uniform(0.3, 0.8))
buffer.produce(f"Item-{i}")
def consumer_thread():
"""Consumer thread"""
for i in range(6):
time.sleep(random.uniform(0.5, 1.2))
buffer.consume()
print("=" * 40)
print("PRODUCER-CONSUMER WITH CONDITION")
print("=" * 40)
# Create threads
producer = threading.Thread(target=producer_thread)
consumer = threading.Thread(target=consumer_thread)
# Start threads
producer.start()
consumer.start()
# Wait for threads
producer.join()
consumer.join()
print("\nAll items produced and consumed!")
print("""
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā CONDITION VARIABLE ADVANTAGES: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠Wait for specific conditions ā
ā ⢠Efficient (no busy waiting) ā
ā ⢠Combines lock with notification ā
ā ⢠Perfect for producer-consumer ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā CONDITION VARIABLE DISADVANTAGES: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠More complex than simple locks ā
ā ⢠Must use `wait()` in a loop ā
ā ⢠Spurious wakeups possible ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
""")
Condition variable key points:
- Wait/Notify ā threads wait for conditions
- No busy waiting ā efficient waiting
- Producer-consumer ā classic use case
- Loop check ā handle spurious wakeups
Quick Check: What is a condition variable used for? (Answer: Allowing threads to wait for specific conditions to be met)
Events
Thread Signaling with Events
An event is a simple synchronization object that allows threads to signal each other. Threads can wait for an event to be set, making it perfect for one-time signaling.
# ============================================================
# EVENT DEMONSTRATION
# ============================================================
import threading
import time
import random
# Create event
event = threading.Event()
def waiter(name):
"""Thread that waits for the event"""
print(f"{name}: Waiting for event...")
event.wait() # Block until event is set
print(f"{name}: Event received! Proceeding...")
# Do work
time.sleep(random.uniform(0.3, 0.6))
print(f"{name}: Work completed.")
def setter():
"""Thread that sets the event after a delay"""
print("Setter: Sleeping for 2 seconds...")
time.sleep(2)
print("Setter: Setting event now!")
event.set() # Signal all waiting threads
print("Setter: Event set. All waiters can proceed.")
print("=" * 40)
print("EVENT DEMONSTRATION")
print("=" * 40)
# Create waiting threads
waiters = []
for i in range(3):
t = threading.Thread(target=waiter, args=(f"Waiter-{i}",))
waiters.append(t)
t.start()
# Create setter thread
setter_thread = threading.Thread(target=setter)
setter_thread.start()
# Wait for all threads
for t in waiters:
t.join()
setter_thread.join()
print("All threads completed!")
# ============================================================
# EVENT WITH TIMEOUT
# ============================================================
print("\n" + "=" * 40)
print("EVENT WITH TIMEOUT")
print("=" * 40)
event2 = threading.Event()
def waiter_with_timeout(name):
"""Wait with timeout"""
print(f"{name}: Waiting (timeout 3s)...")
if event2.wait(timeout=3):
print(f"{name}: Event received!")
else:
print(f"{name}: Timeout! Continuing anyway...")
# Create waiter that will timeout
t = threading.Thread(target=waiter_with_timeout, args=("Timeout Waiter",))
t.start()
t.join()
print("""
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā EVENT ADVANTAGES: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠Simple signaling mechanism ā
ā ⢠One-to-many notification ā
ā ⢠Can set timeout ā
ā ⢠Low overhead ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā EVENT DISADVANTAGES: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠One-time use (must reset) ā
ā ⢠No way to clear for reuse ā
ā ⢠Only works with `wait()` ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
""")
Event key points:
- Signaling ā threads signal each other
- One-time ā event is cleared after use
- Timeout ā can wait with timeout
- Simple ā easy to understand
Quick Check: What is an event used for? (Answer: Signaling between threads)
Barriers
Synchronizing Thread Groups
A barrier synchronizes a fixed number of threads. Threads wait at the barrier until all threads have reached it, then they all proceed together.
# ============================================================
# BARRIER DEMONSTRATION
# ============================================================
import threading
import time
import random
# Barrier for 3 threads
barrier = threading.Barrier(3, action=lambda: print(" ā” All threads reached barrier! ā”"))
def worker(worker_id):
"""Worker thread that uses a barrier"""
print(f"Worker {worker_id}: Phase 1 - preparing...")
time.sleep(random.uniform(0.5, 1.5))
print(f"Worker {worker_id}: Phase 1 complete, waiting at barrier...")
# Wait at barrier
barrier.wait()
print(f"Worker {worker_id}: Phase 2 - executing...")
time.sleep(random.uniform(0.3, 0.8))
print(f"Worker {worker_id}: Phase 2 complete!")
print("=" * 40)
print("BARRIER DEMONSTRATION")
print("=" * 40)
print("Barrier for 3 threads")
print()
# Create 3 workers
workers = []
for i in range(3):
t = threading.Thread(target=worker, args=(i,))
workers.append(t)
t.start()
# Wait for all workers
for t in workers:
t.join()
print("\nAll workers completed!")
# ============================================================
# BARRIER WITH TIMEOUT
# ============================================================
print("\n" + "=" * 40)
print("BARRIER WITH TIMEOUT")
print("=" * 40)
barrier_timeout = threading.Barrier(3, timeout=2)
def slow_worker(worker_id):
"""Worker that might timeout"""
delay = random.uniform(0.5, 2.5)
print(f"Worker {worker_id}: Starting (delay: {delay:.2f}s)")
time.sleep(delay)
print(f"Worker {worker_id}: Reached barrier")
try:
barrier_timeout.wait()
print(f"Worker {worker_id}: Passed barrier!")
except threading.BrokenBarrierError:
print(f"Worker {worker_id}: Barrier broken (timeout)!")
# Create workers (some may timeout)
workers2 = []
for i in range(3):
t = threading.Thread(target=slow_worker, args=(i,))
workers2.append(t)
t.start()
for t in workers2:
t.join()
print("""
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā BARRIER ADVANTAGES: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠Synchronizes multiple threads ā
ā ⢠All threads proceed together ā
ā ⢠Can have action on completion ā
ā ⢠Supports timeout ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā BARRIER DISADVANTAGES: ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ⢠Fixed number of threads ā
ā ⢠Broken barrier on timeout ā
ā ⢠Not reusable after break ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
""")
Barrier key points:
- Group synchronization ā all threads together
- Fixed count ā exactly N threads
- Action function ā called when all reach
- Timeout ā broken barrier on timeout
Quick Check: What does a barrier do? (Answer: It synchronizes a group of threads at a specific point)
Synchronization Comparison
Choosing the Right Synchronization Tool
| Tool | Use Case | Complexity | Threads |
|---|---|---|---|
| Lock | Exclusive resource access | Low | 1 at a time |
| RLock | Nested lock requirements | Low | 1 at a time (reentrant) |
| Semaphore | Limited resource pools | Medium | N at a time |
| Condition | Complex waiting conditions | High | Variable |
| Event | One-time signaling | Low | All waiting |
| Barrier | Group synchronization | Medium | Fixed N |
Selection guide:
- Lock ā simple exclusive access
- RLock ā when functions call each other with locks
- Semaphore ā connection pools, resource limits
- Condition ā producer-consumer patterns
- Event ā one-time notifications
- Barrier ā parallel processing stages
Quick Check: What would you use for a connection pool? (Answer: A Semaphore)
Best Practices
Synchronization Best Practices
# ============================================================
# SYNCHRONIZATION BEST PRACTICES
# ============================================================
print("1. USE CONTEXT MANAGERS")
print(" - `with lock:` for automatic release")
print(" - Prevents forgotten releases")
print(" - Even on exceptions")
print("\n2. MINIMIZE LOCKED SECTIONS")
print(" - Keep critical sections small")
print(" - Only lock what's needed")
print(" - Improves performance")
print("\n3. AVOID DEADLOCKS")
print(" - Acquire locks in consistent order")
print(" - Use timeouts where possible")
print(" - Avoid nested locks if possible")
print("\n4. USE APPROPRIATE TOOLS")
print(" - Lock for simple exclusive access")
print(" - Semaphore for resource pools")
print(" - Condition for complex waiting")
print(" - Event for signaling")
print("\n5. HANDLE EXCEPTIONS")
print(" - Ensure locks are released")
print(" - Use try/finally blocks")
print(" - Context managers handle this")
print("\n6. AVOID BUSY WAITING")
print(" - Use condition.wait() not loops")
print(" - Events and semaphores are efficient")
print(" - Let threads sleep when waiting")
print("\n7. TEST FOR RACE CONDITIONS")
print(" - Use stress testing")
print(" - Add randomization")
print(" - Use ThreadSanitizer if available")
print("\n8. DOCUMENT SYNCHRONIZATION")
print(" - Explain what each lock protects")
print(" - Document ordering requirements")
print(" - Helps maintainability")
Best practices summary:
- Context managers ā `with lock:` for safety
- Minimize locks ā keep critical sections small
- Avoid deadlocks ā consistent order, timeouts
- Choose right tool ā match tool to use case
Quick Check: What is the recommended way to use locks? (Answer: With context managers like `with lock:`)
Try It Yourself
Experiment with thread synchronization in the editor below.
THREAD SYNCHRONIZATION - PRACTICE
========================================
1. LOCK
----------------------------------------
Counter with lock: 5000 (expected: 5000)
2. SEMAPHORE
----------------------------------------
T0: waiting...
T0: using resource
T1: waiting...
T1: using resource
T0: done
T2: waiting...
T2: using resource
T1: done
T3: waiting...
T3: using resource
T2: done
T3: done
3. EVENT
----------------------------------------
Waiter: waiting for event...
Main: setting event...
Waiter: event received!
Synchronization ensures thread safety!
You've Got It!
You now understand thread synchronization in Python. You know how to use locks, semaphores, condition variables, events, and barriers to write thread-safe code.
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What is the difference between a Lock and a Semaphore?
What is the difference between Event and Condition?
What is a deadlock and how can I prevent it?
What is a common interview question about thread synchronization?
When should I use a Barrier?
Where to Go From Here
Now that you understand thread synchronization, check out these related topics:
Multi Tasking
Review the basics of multi-tasking in Python.
Review āThreads Assignments
Practice your threading skills with exercises.
Practice Now āConcurrent Programming & GIL
Learn about the GIL and its impact on concurrency.
Learn More ā