Python Thread Synchronization – Locks, RLocks & Examples

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:

2000

Actual result might be:

1785
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:

2000

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:

Waiting
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:

Produced
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
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.

Related Python Tutorials