Python – Thread Synchronization

Introduction

Thread synchronization is essential in multithreaded programming to prevent race conditions and ensure data integrity when multiple threads access shared resources. Python provides several synchronization primitives in the threading module, such as Locks, RLocks, Semaphores, Conditions, and Events.

Synchronization Primitives

1. Lock

A Lock is the simplest form of synchronization primitive. It ensures that only one thread can access a resource at a time.

Example

import threading

lock = threading.Lock()
shared_resource = 0

def increment():
    global shared_resource
    for _ in range(1000000):
        with lock:
            shared_resource += 1

# Create threads
threads = [threading.Thread(target=increment) for _ in range(2)]

# Start threads
for thread in threads:
    thread.start()

# Wait for all threads to complete
for thread in threads:
    thread.join()

print(f"Final value of shared_resource: {shared_resource}")

Output

Final value of shared_resource: 2000000

2. RLock (Reentrant Lock)

An RLock (Reentrant Lock) allows a thread to acquire the same lock multiple times without causing a deadlock.

Example

import threading

rlock = threading.RLock()
shared_resource = 0

def increment():
    global shared_resource
    for _ in range(1000000):
        with rlock:
            shared_resource += 1

# Create threads
threads = [threading.Thread(target=increment) for _ in range(2)]

# Start threads
for thread in threads:
    thread.start()

# Wait for all threads to complete
for thread in threads:
    thread.join()

print(f"Final value of shared_resource: {shared_resource}")

Output

Final value of shared_resource: 2000000

3. Semaphore

A Semaphore controls access to a resource with a fixed number of permits.

Example

import threading
import time

semaphore = threading.Semaphore(2)

def access_resource(thread_id):
    with semaphore:
        print(f"Thread {thread_id} accessing resource")
        time.sleep(2)
        print(f"Thread {thread_id} releasing resource")

# Create threads
threads = [threading.Thread(target=access_resource, args=(i,)) for i in range(4)]

# Start threads
for thread in threads:
    thread.start()

# Wait for all threads to complete
for thread in threads:
    thread.join()

print("All threads have finished execution.")

Output

Thread 0 accessing resource
Thread 1 accessing resource
Thread 0 releasing resource
Thread 1 releasing resource
Thread 2 accessing resource
Thread 3 accessing resource
Thread 2 releasing resource
Thread 3 releasing resource
All threads have finished execution.

4. Condition

A Condition allows threads to wait for certain conditions to be met before continuing execution. It is often used in conjunction with a Lock or RLock.

Example

import threading

condition = threading.Condition()
queue = []

def consumer():
    with condition:
        condition.wait()  # Wait for the condition to be notified
        while queue:
            item = queue.pop(0)
            print(f"Consumed: {item}")

def producer():
    with condition:
        for i in range(5):
            queue.append(i)
            print(f"Produced: {i}")
        condition.notify_all()  # Notify all waiting threads

# Create threads
consumer_thread = threading.Thread(target=consumer)
producer_thread = threading.Thread(target=producer)

# Start threads
consumer_thread.start()
producer_thread.start()

# Wait for all threads to complete
consumer_thread.join()
producer_thread.join()

print("All threads have finished execution.")

Output

Produced: 0
Produced: 1
Produced: 2
Produced: 3
Produced: 4
Consumed: 0
Consumed: 1
Consumed: 2
Consumed: 3
Consumed: 4
All threads have finished execution.

5. Event

An Event is a simple synchronization primitive that allows one thread to signal one or more other threads that an event has occurred.

Example

import threading
import time

event = threading.Event()

def waiter():
    print("Waiting for event to be set")
    event.wait()  # Wait for the event to be set
    print("Event has been set!")

def setter():
    time.sleep(2)
    print("Setting the event")
    event.set()

# Create threads
waiter_thread = threading.Thread(target=waiter)
setter_thread = threading.Thread(target=setter)

# Start threads
waiter_thread.start()
setter_thread.start()

# Wait for all threads to complete
waiter_thread.join()
setter_thread.join()

print("All threads have finished execution.")

Output

Waiting for event to be set
Setting the event
Event has been set!
All threads have finished execution.

Conclusion

Thread synchronization is crucial for managing concurrent access to shared resources and preventing race conditions in multithreaded programs. Python’s threading module provides several synchronization primitives such as Locks, RLocks, Semaphores, Conditions, and Events to help you manage thread synchronization effectively. Understanding these primitives and how to use them will enable you to write robust and efficient multithreaded applications in Python.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top