1.7 - Synchronization and locking considerations
Key Concept
In concurrent programming, where multiple threads or processes execute seemingly simultaneously, ensuring the correct order of operations and data consistency is paramount. This is where synchronization and locking mechanisms come into play. Without proper synchronization, concurrent programs can suffer from various problems, including data corruption, unpredictable behavior, and crashes. This section delves into the fundamental concepts of synchronization and locking, exploring the challenges they address and the potential pitfalls to avoid.
Topics
Race Conditions: Occur when the outcome of a program depends on the unpredictable order of execution of multiple threads/processes.
A race condition occurs when the outcome of a program depends on the unpredictable order in which multiple threads or processes access and modify shared data. Imagine two threads attempting to increment a shared counter variable. If both threads read the same initial value, increment it, and then write the updated value back, the final result might be incorrect. For example, if the counter is initially 5, and both threads read 5, increment to 6, and write 6 back, the counter should be 7, but it's only 6. The final value depends on the timing of the threads' operations – which thread executes its write operation first. Race conditions are notoriously difficult to debug because they are non-deterministic; they may occur intermittently and are hard to reproduce.
Mutual Exclusion: Ensuring that only one thread/process can access a critical section of code at a time.
To prevent race conditions and ensure data integrity, the principle of **mutual exclusion** is employed. Mutual exclusion guarantees that only one thread or process can access a critical section of code (a section that accesses shared data) at any given time. This prevents conflicting operations from occurring and ensures that data remains consistent. A critical section is a portion of code that manipulates shared resources. The goal of mutual exclusion is to protect these resources from simultaneous access. Think of it like a single-lane bridge – only one car (thread/process) can be on it at a time.
Locking Mechanisms: Tools like mutexes, semaphores, and read-write locks provide different levels of access control.
Locking mechanisms are the tools used to enforce mutual exclusion. They are essentially variables that control access to a critical section. Common locking mechanisms include:
- Mutexes (Mutual Exclusion Locks): A mutex is a lock that can be in one of two states: locked or unlocked. A thread must acquire (lock) a mutex before entering a critical section and release (unlock) it when exiting. If another thread attempts to acquire a locked mutex, it will be blocked until the mutex is released.
- Semaphores: A semaphore is a more general synchronization tool than a mutex. It maintains a counter value. A semaphore can be used to control access to a limited number of resources. It can be initialized with a specific count, and threads can decrement (wait) on the semaphore to acquire a resource and increment (signal) on the semaphore to release a resource.
- Read-Write Locks (Shared/Exclusive Locks): These locks allow multiple threads to read shared data concurrently, but only one thread to write to the data at a time. This is useful when reads are much more frequent than writes.
Deadlock
A situation where two or more processes are blocked indefinitely, each waiting for the other to release a resource. Imagine two threads, Thread A and Thread B. Thread A holds lock X and is waiting for lock Y, while Thread B holds lock Y and is waiting for lock X. Neither thread can proceed, resulting in a deadlock. Deadlocks can be difficult to diagnose and resolve, often requiring careful design and resource allocation strategies.
Livelock
Livelock is a situation where threads are continuously changing their state in response to each other, but without making any progress. Unlike deadlock, where threads are blocked, in a livelock, threads are actively running but are unable to complete their tasks. Imagine two threads trying to access a shared resource. Each thread repeatedly attempts to acquire the resource, but each time it fails because the other thread is also trying to acquire it. This leads to a continuous cycle of attempts and failures, preventing either thread from making progress.
Starvation
Starvation occurs when a thread is repeatedly denied access to a resource, even though the resource is available. This can happen if the scheduling algorithm consistently favors other threads, preventing the starved thread from making progress. For example, a thread might repeatedly lose the race to acquire a mutex, even though the mutex is frequently released. Starvation can be mitigated by using fairness mechanisms in the operating system or by implementing priority scheduling.
Exercise
Consider a scenario where two threads increment a shared counter. What potential problem arises if no synchronization is used? (5 min)
Answer: Without synchronization, updates to the shared variable can be lost due to interleaving of operations. Locks (or other synchronization primitives) ensure correctness but may reduce parallel speedup.
import threading
counter = 0
def increment():
global counter
for _ in range(100_000):
counter += 1
threads = [threading.Thread(target=increment) for _ in range(2)]
for t in threads: t.start()
for t in threads: t.join()
print("Final counter:", counter)
👉 Often you’ll see a result like 150000 instead of 200000, because both threads update counter at the same time and overwrite each other.
This is the “fixed” version with a Lock:
lock = threading.Lock()
def increment_safe():
global counter
for _ in range(100_000):
with lock:
counter += 1
💡 Common Pitfalls
- Forgetting to release locks after use.
💡 Best Practices
- Use the simplest synchronization mechanism that meets the requirements.