Python > Advanced Python Concepts > Concurrency and Parallelism > Synchronization Primitives (Locks, Semaphores, Events)
Locking for Thread Safety
This code demonstrates the use of a Lock
to protect a shared resource from race conditions in a multithreaded environment. Without proper synchronization, multiple threads accessing and modifying shared data can lead to unpredictable and incorrect results.
Code Example: Using a Lock
This code creates a shared variable counter
and a threading.Lock
instance. The increment_counter
and decrement_counter
functions both acquire the lock before modifying the counter and release it afterward. The try...finally
block ensures that the lock is always released, even if an exception occurs within the critical section. The main part of the script creates and starts multiple threads that increment and decrement the counter. The thread.join()
ensures that main thread waits for the other threads to complete before printing the final counter value.
import threading
import time
# Shared resource
counter = 0
# Lock to protect the shared resource
lock = threading.Lock()
def increment_counter(num_increments):
global counter
for _ in range(num_increments):
lock.acquire()
try:
counter += 1
finally:
lock.release()
def decrement_counter(num_decrements):
global counter
for _ in range(num_decrements):
lock.acquire()
try:
counter -= 1
finally:
lock.release()
if __name__ == "__main__":
num_threads = 2
increments_per_thread = 100000
decrements_per_thread = 50000
threads = []
for i in range(num_threads):
if i % 2 == 0:
thread = threading.Thread(target=increment_counter, args=(increments_per_thread,))
else:
thread = threading.Thread(target=decrement_counter, args=(decrements_per_thread,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print(f"Final counter value: {counter}")
Concepts Behind the Snippet
Race Condition: Occurs when multiple threads access and modify shared data concurrently, leading to unpredictable and potentially incorrect results. Without proper synchronization mechanisms, the final value of the shared data can depend on the specific timing of thread execution. Critical Section: A section of code that accesses and modifies shared resources. It's crucial to protect critical sections with synchronization primitives to ensure atomicity and prevent race conditions. Lock: A synchronization primitive that allows only one thread to access a shared resource at a time. A thread must acquire the lock before entering the critical section and release it afterward. Other threads attempting to acquire the lock will be blocked until it is released.
Real-Life Use Case
Consider a banking application where multiple threads are trying to update the balance of a customer's account simultaneously. Without a lock, a race condition could occur where one thread reads the balance, another thread withdraws funds, and then the first thread writes back an outdated balance, leading to incorrect account balances.
Best Practices
Minimize Lock Contention: Keep critical sections as short as possible to reduce the amount of time threads spend waiting for the lock. Longer critical sections increase the likelihood of contention and can degrade performance. Avoid Deadlocks: Be careful when acquiring multiple locks to avoid deadlocks. A deadlock occurs when two or more threads are blocked indefinitely, waiting for each other to release locks. One way to avoid deadlocks is to acquire locks in a consistent order. Use Context Managers: Use the with
statement to acquire and release locks automatically. This ensures that the lock is always released, even if an exception occurs within the critical section. For example: with lock: ...
Interview Tip
Be prepared to explain the concept of race conditions and how locks prevent them. Understand the difference between mutexes and semaphores, and be able to describe scenarios where each is appropriate. Also, practice using locks in code snippets.
When to Use Locks
Use locks whenever multiple threads need to access and modify shared resources. Locks are particularly useful for protecting critical sections of code that must be executed atomically.
Memory Footprint
Locks have a relatively small memory footprint, typically only requiring a few bytes of memory to store the lock's state (e.g., whether it is acquired or released). The primary cost associated with locks is the potential for thread blocking and context switching, which can impact performance.
Alternatives
RLock (Reentrant Lock): Allows the same thread to acquire the lock multiple times without blocking. Useful when a function that already holds the lock calls another function that also needs the lock. Semaphore: Allows a limited number of threads to access a shared resource concurrently. Useful for controlling access to resources with a limited capacity. Condition: Allows threads to wait for a specific condition to become true. Useful for coordinating threads that depend on each other. Queue: Thread-safe data structure that can be used to pass data between threads. Often used in producer-consumer scenarios.
Pros
Simple and Easy to Use: Locks are relatively easy to understand and use, making them a good choice for simple synchronization tasks. Effective Protection: Locks provide effective protection against race conditions and data corruption.
Cons
Potential for Deadlocks: Locks can lead to deadlocks if not used carefully. Performance Overhead: Acquiring and releasing locks can introduce performance overhead, especially in highly concurrent applications. Limited Concurrency: Locks can limit concurrency by allowing only one thread to access the shared resource at a time.
FAQ
-
What happens if a thread tries to acquire a lock that is already held by another thread?
The thread will block until the lock is released by the thread that currently holds it. -
How can I avoid deadlocks when using multiple locks?
Acquire locks in a consistent order across all threads. Consider using timeouts when acquiring locks to prevent indefinite blocking. -
Is it always necessary to use locks when working with threads?
No, it is only necessary to use locks when multiple threads are accessing and modifying shared resources. If threads are working with independent data, locks are not needed.