Python > Advanced Python Concepts > Concurrency and Parallelism > Threads and the `threading` Module
Thread Synchronization: Using Locks to Prevent Race Conditions
This example demonstrates how to use locks to synchronize access to shared resources and prevent race conditions in a multithreaded environment. It extends the previous example by using a Lock
to protect the counter variable, ensuring that only one thread can access it at a time.
Code Snippet
The code now includes a threading.Lock()
object within the Counter
class. The increment
method uses a with self.lock:
statement to acquire the lock before accessing the counter and release it afterwards. This ensures that only one thread can increment the counter at any given time, preventing race conditions. The with
statement ensures that the lock is always released, even if an exception occurs within the critical section. This makes the code more robust and easier to read.
import threading
import time
class Counter:
def __init__(self):
self.value = 0
self.lock = threading.Lock()
def increment(self):
with self.lock:
time.sleep(0.01) # Simulate some work
self.value += 1
counter = Counter()
def worker():
for _ in range(1000):
counter.increment()
threads = []
for i in range(5):
t = threading.Thread(target=worker)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Final counter value: {counter.value}")
Concepts Behind the Snippet
This snippet introduces the concept of thread synchronization using locks. Key concepts include:
threading.Lock
Class: Represents a lock object.acquire()
Method: Acquires the lock, blocking if necessary until the lock is available.release()
Method: Releases the lock, allowing another thread to acquire it.with
Statement: Provides a convenient way to acquire and release a lock, ensuring that the lock is always released, even if an exception occurs.
Real-Life Use Case
Consider a banking application where multiple threads are updating a customer's account balance. Without proper synchronization, race conditions could lead to incorrect balance calculations. Using locks ensures that only one thread can update the balance at a time, preventing data corruption.
Best Practices
with
statements for lock management: This ensures that the lock is always released, even if an exception occurs.
Interview Tip
Be prepared to explain different types of synchronization primitives (locks, semaphores, conditions, events) and their use cases. Understand the concept of deadlocks and how to prevent them.
When to Use Locks
Locks are appropriate when you need to protect shared mutable state from concurrent access by multiple threads. They are particularly useful for preventing race conditions and ensuring data integrity.
Memory Footprint
Locks have a relatively small memory footprint. The primary overhead is the memory required to store the lock object itself and any associated metadata.
Alternatives
Alternatives to locks include:
Pros
Cons
FAQ
-
What is a deadlock?
A deadlock occurs when two or more threads are blocked indefinitely, waiting for each other to release a resource that they need. This typically happens when threads acquire multiple locks in different orders. -
How can I prevent deadlocks?
Deadlocks can be prevented by:- Acquiring locks in a consistent order.
- Avoiding holding multiple locks at the same time.
- Using timeouts when acquiring locks.
- Using deadlock detection and recovery mechanisms.