Introduction
Multithreading is a powerful feature in Python that allows multiple threads to execute concurrently within the same process.
If two or more threads attempt to modify the same variable, file, or database record simultaneously, the program may produce incorrect or unpredictable results. This problem is known as a race condition.
To prevent such issues, Python provides Thread Synchronization mechanisms. Synchronization ensures that shared resources are accessed safely and that only one thread performs critical operations at a time.
What is Thread Synchronization?
Thread Synchronization is the process of controlling the execution of multiple threads so that shared resources are accessed safely and consistently.
Synchronization ensures:
- Data integrity
- Consistent results
- Safe resource sharing
- Prevention of race conditions
- Better thread coordination
Note: Without synchronization, threads may interfere with each other and produce unexpected outcomes.
Why is Thread Synchronization Needed?
Consider the following scenario:
counter = 0
Two threads attempt to increase the counter:
counter += 1
Expected result:
Actual result might be:
or
1932
because both threads access and modify the variable simultaneously. This situation is called a race condition. Synchronization solves this problem.
What is a Race Condition?
A Race Condition occurs when multiple threads access and modify shared data at the same time.
Example:
import threading
counter = 0
def increment():
global counter
for i in range(100000):
counter += 1
thread1 = threading.Thread(
target=increment
)
thread2 = threading.Thread(
target=increment
)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(counter)
The output may vary each time because the threads compete for access to the same variable.
Synchronization Techniques in Python
Python provides several synchronization tools:
| Synchronization Tool | Purpose |
|---|---|
| Lock | Mutual exclusion |
| RLock | Reentrant locking |
| Semaphore | Limit thread access |
| Event | Thread signaling |
| Condition | Thread communication |
| Barrier | Synchronize thread completion |
These tools are available in the threading module.
Lock Object
A Lock is the simplest synchronization mechanism.
It ensures that only one thread accesses a critical section at a time.
Creating a Lock
import threading
lock = threading.Lock()
Acquiring and Releasing a Lock
lock.acquire()
# Critical Section
lock.release()
Example Using Lock
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for i in range(1000):
lock.acquire()
counter += 1
lock.release()
thread1 = threading.Thread(
target=increment
)
thread2 = threading.Thread(
target=increment
)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(counter)
Output:
Synchronization guarantees correct results.
Using Lock with Context Manager
A cleaner approach:
import threading
lock = threading.Lock()
def task():
with lock:
print(
"Protected Code"
)
The lock is automatically released.
What is a Critical Section?
A Critical Section is a portion of code where shared resources are accessed.
Example:
counter += 1
Only one thread should execute this section at a time.
Reentrant Lock (RLock)
An RLock allows the same thread to acquire a lock multiple times.
Create an RLock:
import threading
lock = threading.RLock()
Example:
import threading
lock = threading.RLock()
def outer():
lock.acquire()
inner()
lock.release()
def inner():
lock.acquire()
print("Inside")
lock.release()
Without RLock, this may cause deadlocks.
Semaphore
A Semaphore limits the number of threads that can access a resource simultaneously.
Example:
import threading
semaphore = threading.Semaphore(2)
Only two threads can enter at once.
Semaphore Example
import threading
import time
semaphore = threading.Semaphore(2)
def task():
with semaphore:
print("Working")
time.sleep("2")
for i in range(5):
threading.Thread(
target=task
).start()
At most two threads run concurrently.
Event Object
An Event allows one thread to signal another thread.
Create an Event:
import threading
event = threading.Event()
Event Example
import threading
event = threading.Event()
def wait_thread():
print("Waiting")
event.wait()
print("Started")
def start_thread():
event.set()
threading.Thread(
target=wait_thread
).start()
threading.Thread(
target=start_thread
).start()
Output:
Started
Condition Object
A Condition enables threads to communicate and coordinate execution.
Create a condition:
condition = threading.Condition()
Condition Example
import threading
condition = threading.Condition()
def consumer():
with condition:
condition.wait()
print("Consumed")
def producer():
with condition:
print("Produced")
condition.notify()
threading.Thread(
target=consumer
).start()
threading.Thread(
target=producer
).start()
Output:
Consumed
Barrier Object
A Barrier forces threads to wait until all participating threads reach a specific point.
Example:
import threading
barrier = threading.Barrier(3)
Three threads must reach the barrier before continuing.
Barrier Example
import threading
barrier = threading.Barrier(3)
def task():
print("Waiting")
barrier.wait()
print("Proceeding")
for i in range(3):
threading.Thread(
target=task
).start()
Output:
Waiting
Waiting
Proceeding
Proceeding
Proceeding
Deadlock in Thread Synchronization
A Deadlock occurs when two or more threads wait indefinitely for resources held by each other.
Example:
Thread A waits for Lock B
Thread B waits for Lock A
Neither thread can continue.
Deadlock Prevention
Acquire Locks Consistently
Always acquire locks in the same order.
Release Locks Quickly
Avoid holding locks unnecessarily.
Use Context Managers
with lock:
# code
This automatically releases locks.
Real-Life Example: Bank Account System
Imagine multiple users withdrawing money from the same account.
Without synchronization:
Incorrect Balance
With synchronization:
Accurate Balance
Example:
with lock:
balance -= amount
Real-Life Examples:
1. Ticket Booking System
Multiple users booking seats simultaneously.
Synchronization ensures:
- No duplicate bookings
- Accurate seat availability
- Safe transactions
2. Database Updates
Multiple threads updating records.
Using locks prevents:
- Data corruption
- Lost updates
- Inconsistent results
3. File Writing
Several threads writing to a file.
Without synchronization:
Corrupted Data
With synchronization:
with lock:
file.write(data)
Output remains correct.
Lock vs RLock
| Feature | Lock | RLock |
|---|---|---|
| Multiple Acquisitions | No | Yes |
| RLock | No | Yes |
| Complexity | Simple | More Advanced |
| Deadlock Risk | Higher | Lower |
Advantages of Thread Synchronization
| Advantage | Description |
|---|---|
| Data Integrity | Prevents corruption |
| Consistency | Produces predictable results |
| Safe Resource Sharing | Protects shared data |
| Better Coordination | Manages thread execution |
| Reliability | Reduces concurrency issues |
Common Mistakes
1. Forgetting to Release Locks
Incorrect:
lock.acquire()
# code
The lock remains held.
Correct:
lock.release()
or
with lock:
pass
2. Ignoring Synchronization
Shared resources become vulnerable to race conditions.
3. Overusing Locks
Excessive locking reduces performance.
Use synchronization only when necessary.
4. Creating Deadlocks
Acquiring locks in inconsistent order can freeze applications.
5. Holding Locks Too Long
Locks should protect only critical sections.
Conclusion
Python Thread Synchronization is essential for building reliable multithreaded applications. When multiple threads share resources, synchronization mechanisms such as Locks, RLocks, Semaphores, Events, Conditions, and Barriers help prevent race conditions and ensure data consistency.
Understanding synchronization techniques allows developers to create efficient, secure, and scalable applications for banking systems, ticket booking platforms, database management, file processing, and many other real-world scenarios.