Python - Synchronizing Threads
When multiple threads access shared data at the same time, problems can occur such as incorrect results or data corruption. This is known as a race condition.
To solve this problem, Python provides thread synchronization techniques.
Thread synchronization ensures that only one thread accesses shared resources at a time, or that threads coordinate properly with each other.
In this tutorial, you will learn what thread synchronization is, why it is needed, and how to implement it in Python.
What is Thread Synchronization?
Thread synchronization is:
A mechanism that controls the access of multiple threads to shared resources.
It ensures:
- Data consistency
- Safe execution
- Avoidance of race conditions
Why Do We Need Synchronization?
Without synchronization:
- Multiple threads modify data at the same time
- Results become unpredictable
- Data may get corrupted
Example of Problem Without Synchronization
import threading
counter = 0
def increment():
global counter
for _ in range(100000):
counter += 1
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start()
t2.start()
t1.join()
t2.join()
print("Final Counter:", counter)Expected vs Actual Output
Expected:
200000Actual (may vary):
Less than 200000Why This Happens
Because:
- Both threads access
counterat the same time - Operations overlap
- Data is overwritten
This is called a race condition.
Solution: Thread Synchronization
Python provides several ways to synchronize threads.
Method 1: Using Lock
A Lock allows only one thread to access a resource at a time.
Syntax
lock = threading.Lock()Example with Lock
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
lock.acquire()
counter += 1
lock.release()
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start()
t2.start()
t1.join()
t2.join()
print("Final Counter:", counter)Better Way: Using with Statement
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock:
counter += 1Method 2: RLock (Reentrant Lock)
An RLock allows a thread to acquire the same lock multiple times.
lock = threading.RLock()Example
import threading
lock = threading.RLock()
def task():
with lock:
print("Outer lock")
with lock:
print("Inner lock")
t = threading.Thread(target=task)
t.start()Method 3: Semaphore
A Semaphore allows multiple threads to access a resource with a limit.
Example
import threading
semaphore = threading.Semaphore(2)
def task(name):
with semaphore:
print(name, "is running")
threads = []
for i in range(5):
t = threading.Thread(target=task, args=(f"Thread-{i}",))
threads.append(t)
t.start()
for t in threads:
t.join()Method 4: Event
An Event is used to signal between threads.
Example
import threading
import time
event = threading.Event()
def wait_for_event():
print("Waiting for event...")
event.wait()
print("Event received!")
def trigger_event():
time.sleep(2)
event.set()
t1 = threading.Thread(target=wait_for_event)
t2 = threading.Thread(target=trigger_event)
t1.start()
t2.start()Method 5: Condition
Condition is used for complex thread communication.
Example
import threading
condition = threading.Condition()
data_ready = False
def consumer():
with condition:
while not data_ready:
condition.wait()
print("Consumed data")
def producer():
global data_ready
with condition:
data_ready = True
condition.notify()
t1 = threading.Thread(target=consumer)
t2 = threading.Thread(target=producer)
t1.start()
t2.start()Types of Synchronization
1. Mutual Exclusion
Only one thread accesses resource at a time (Lock).
2. Thread Coordination
Threads communicate and wait (Event, Condition).
3. Resource Limiting
Control number of threads (Semaphore).
Real-World Example
Bank Account System
import threading
balance = 1000
lock = threading.Lock()
def withdraw(amount):
global balance
with lock:
if balance >= amount:
balance -= amount
print("Withdraw:", amount)
else:
print("Insufficient funds")
t1 = threading.Thread(target=withdraw, args=(700,))
t2 = threading.Thread(target=withdraw, args=(700,))
t1.start()
t2.start()
t1.join()
t2.join()
print("Final Balance:", balance)Advantages of Synchronization
- Prevents data corruption
- Ensures consistency
- Controls thread execution
- Improves reliability
Disadvantages
- Slower performance
- Can cause deadlocks
- Increases complexity
Common Mistakes
1. Forgetting to release lock
lock.acquire()
# missing release2. Overusing locks
Reduces performance.
3. Deadlocks
Threads waiting forever.
Best Practices
1. Use with lock
with lock:
# safe code2. Keep critical section small
3. Avoid nested locks when possible
4. Always test concurrency scenarios
Summary
Thread synchronization in Python ensures safe access to shared resources when multiple threads run simultaneously. It prevents race conditions and maintains data consistency using tools like Lock, RLock, Semaphore, Event, and Condition.
Key Takeaways
- Synchronization prevents race conditions
- Use Lock for mutual exclusion
- Use Semaphore for limiting access
- Use Event/Condition for coordination
- Essential for safe multithreading
Mastering thread synchronization is critical for building reliable and safe concurrent Python applications.


0 Comments