Introduction
When multiple threads access and modify shared resources simultaneously, unexpected results can occur. Synchronization is the process of controlling thread access to shared resources to ensure data consistency and prevent conflicts.
Synchronization is essential in:
-
Multithreaded Applications
-
Selenium Automation Frameworks
-
API Automation Frameworks
-
Banking Systems
-
Inventory Management Systems
-
Logging Systems
-
Database Operations
In this tutorial, you will learn synchronization concepts, race conditions, locks, semaphores, events, practical examples, automation testing use cases, common mistakes, and best practices.
What is Synchronization?
Synchronization ensures that only the appropriate thread accesses a shared resource at a given time.
Example
Imagine two people updating the same bank account balance simultaneously.
Without synchronization:
-
Person A reads balance = 1000
-
Person B reads balance = 1000
-
Person A adds 500 → 1500
-
Person B adds 300 → 1300
Final balance becomes 1300 instead of 1800.
This problem occurs because multiple threads accessed the shared resource simultaneously.
What is a Race Condition?
A race condition occurs when multiple threads access and modify shared data at the same time, causing unpredictable results.
Example
import threading
counter = 0
def increment():
global counter
for _ 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)
Expected Output
200000
Possible Output
183524
The result may vary because of a race condition.
Synchronization Tools in Python
Python provides synchronization primitives through the threading module.
| Tool | Purpose |
|---|---|
| Lock | Mutual exclusion |
| RLock | Reentrant lock |
| Semaphore | Limit access count |
| Event | Thread signaling |
| Condition | Wait/notify mechanism |
| Barrier | Synchronize thread completion |
Lock
A Lock allows only one thread to access a critical section at a time.
Creating a Lock
import threading
lock = threading.Lock()
Using Lock
Example
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock:
counter += 1
thread1 = threading.Thread(
target=increment
)
thread2 = threading.Thread(
target=increment
)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(counter)
Output
200000
Now the result is consistent.
Manual Lock Acquisition
Example
lock.acquire()
try:
# critical section
pass
finally:
lock.release()
Using with Statement
Preferred approach:
with lock:
# critical section
pass
Automatically releases the lock.
What is a Critical Section?
A critical section is the part of code where shared resources are accessed.
Example:
counter += 1
Only one thread should execute this section at a time.
RLock (Reentrant Lock)
A thread can acquire an RLock multiple times.
Example
import threading
lock = threading.RLock()
def task():
with lock:
print("Outer Lock")
with lock:
print("Inner Lock")
task()
Output
Outer Lock
Inner Lock
Semaphore
A semaphore allows a fixed number of threads to access a resource simultaneously.
Example
import threading
import time
semaphore = threading.Semaphore(2)
def worker(number):
with semaphore:
print(
f"Thread {number} Running"
)
time.sleep(2)
for i in range(5):
thread = threading.Thread(
target=worker,
args=(i,)
)
thread.start()
Behavior
Only 2 threads run at a time.
Event
An Event allows one thread to signal another thread.
Example
import threading
event = threading.Event()
def waiter():
print("Waiting...")
event.wait()
print("Started")
thread = threading.Thread(
target=waiter
)
thread.start()
event.set()
Output
Waiting...
Started
Condition
Used when threads need to wait for a specific condition.
Example
import threading
condition = threading.Condition()
items = []
def consumer():
with condition:
while not items:
condition.wait()
print(items.pop())
def producer():
with condition:
items.append("Data")
condition.notify()
threading.Thread(
target=consumer
).start()
threading.Thread(
target=producer
).start()
Barrier
A Barrier waits until a specified number of threads reach the same point.
Example
import threading
barrier = threading.Barrier(3)
def worker(number):
print(
f"Thread {number} Ready"
)
barrier.wait()
print(
f"Thread {number} Started"
)
for i in range(3):
threading.Thread(
target=worker,
args=(i,)
).start()
Real-World Example: Bank Account
Without Lock
balance = 1000
def withdraw(amount):
global balance
balance -= amount
Multiple withdrawals can corrupt data.
With Lock
lock = threading.Lock()
def withdraw(amount):
global balance
with lock:
balance -= amount
Safe execution.
Selenium Automation Example
Shared Report File
Without synchronization:
report.write("Test Passed")
Multiple threads may write simultaneously.
With Lock
lock = threading.Lock()
with lock:
report.write("Test Passed")
Prevents report corruption.
API Automation Example
Rate-limit concurrent API calls.
semaphore = threading.Semaphore(5)
Only 5 API requests execute simultaneously.
Common Mistakes Beginners Make
Not Protecting Shared Data
Incorrect
counter += 1
Race conditions may occur.
Correct
with lock:
counter += 1
Forgetting Lock Release
Incorrect
lock.acquire()
# exception occurs
# lock.release() never called
Can cause deadlocks.
Correct
with lock:
pass
Overusing Locks
Too many locks reduce performance.
Lock only critical sections.
Nested Locks Causing Deadlocks
Example
Thread A waits for Lock B.
Thread B waits for Lock A.
Both threads become stuck.
Best Practices
Use with Lock
with lock:
pass
Keep Critical Sections Small
with lock:
counter += 1
Avoid long operations inside locks.
Use Semaphore for Resource Pools
Example:
-
Database connections
-
API requests
-
Browser instances
Prefer Higher-Level Constructs
Use:
ThreadPoolExecutor
when possible.
Avoid Deadlocks
Always acquire locks in a consistent order.
Advantages of Synchronization
-
Prevents race conditions
-
Maintains data consistency
-
Ensures thread safety
-
Improves application reliability
Limitations
-
Adds complexity
-
May reduce performance
-
Incorrect usage can cause deadlocks
Synchronization Tools Summary
| Tool | Purpose |
|---|---|
| Lock | One thread at a time |
| RLock | Same thread can re-acquire |
| Semaphore | Limited concurrent access |
| Event | Thread signaling |
| Condition | Wait/notify |
| Barrier | Wait for all threads |
Conclusion
Synchronization is one of the most important concepts in multithreaded programming. Without synchronization, shared data can become corrupted due to race conditions and concurrent access issues.
Python provides powerful synchronization primitives such as Lock, RLock, Semaphore, Event, Condition, and Barrier to coordinate thread execution safely.
Understanding these concepts is essential when building reliable automation frameworks, web applications, APIs, and enterprise systems.
Frequently Asked Questions (FAQs)
What is synchronization?
Synchronization controls access to shared resources among multiple threads.
What is a race condition?
A situation where multiple threads modify shared data simultaneously, producing unpredictable results.
Which synchronization tool is most commonly used?
threading.Lock()
What is a semaphore?
A synchronization mechanism that limits the number of threads accessing a resource.
What causes deadlocks?
Deadlocks occur when threads wait indefinitely for resources held by each other.
Key Takeaways
-
Synchronization prevents conflicts between threads.
-
Race conditions occur when shared data is modified concurrently.
-
Lockprovides mutual exclusion. -
RLockallows reentrant locking. -
Semaphorelimits concurrent access. -
Eventenables thread signaling. -
Conditionsupports wait/notify communication. -
Barriersynchronizes multiple threads. -
Use
with lock:whenever possible. -
Proper synchronization is critical for thread-safe applications.
