135. Thread Synchronization with threading.Lock

Thread synchronization is essential when working with multithreading in Python, especially when threads need to access shared resources. The threading.Lock is a simple way to prevent race conditions and ensure that only one thread accesses a resource at a time.

1. Basic Example of Thread Synchronization with Lock

In this example, we create multiple threads that try to increment a shared counter. We use a Lock to prevent race conditions, ensuring the counter is incremented safely.

Copy

import threading

# Shared resource
counter = 0

# Create a lock object
counter_lock = threading.Lock()

# Function to increment the counter safely
def increment_counter():
    global counter
    with counter_lock:  # Acquire the lock before modifying the shared resource
        for _ in range(100000):
            counter += 1  # Increment the shared counter

# Create threads
threads = []
for _ in range(5):
    thread = threading.Thread(target=increment_counter)
    threads.append(thread)
    thread.start()

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

# Print the result after all threads have completed
print(f"Counter value after threads finish: {counter}")

Explanation:

  • Shared Resource: The counter variable is shared between multiple threads.

  • Lock: counter_lock = threading.Lock() creates a lock object that is used to synchronize access to the shared resource.

  • Thread Function: Inside increment_counter(), we use the with counter_lock: statement to ensure that only one thread can increment the counter at a time. This prevents race conditions, where multiple threads could simultaneously modify counter and cause inconsistent results.

  • Thread Execution: Five threads are created, each calling the increment_counter() function, and each thread safely increments the counter.

Without the lock, the threads might overwrite each other's increments, leading to an incorrect final value. With the lock in place, the output will always be 500000, as the counter is incremented one step at a time.

2. Using Lock with Multiple Shared Resources

In more complex scenarios, you may need to synchronize access to multiple shared resources. Here's how you can do that with multiple locks:

Copy

Explanation:

  • Multiple Locks: In this case, we have two shared resources: counter1 and counter2, each with its own lock (counter1_lock and counter2_lock).

  • Synchronization: We acquire the appropriate lock for each counter to ensure thread safety when incrementing both counters.

3. Deadlock Prevention in Locking

While using locks, it's important to avoid deadlocks, where two or more threads are waiting for each other to release a lock, leading to an infinite wait. A simple rule of thumb is to acquire locks in a consistent order to prevent deadlocks.

Copy

Explanation:

  • Lock Order: The locks are acquired in a consistent order: lock1 first, then lock2. This avoids the possibility of deadlock by preventing circular wait conditions.

4. Using Lock for Critical Sections

In cases where only a small portion of code (a critical section) needs synchronization, using a Lock for just that section ensures minimal blocking.

Copy

Explanation:

  • Critical Section: Only the part of the function where the shared resource shared_data is modified is locked. This minimizes the time the lock is held, improving performance in scenarios where only a small portion of code needs synchronization.

5. Locking in Producer-Consumer Model

In a more advanced scenario, you can use locks in a producer-consumer model, where one thread (the producer) adds items to a queue, and another thread (the consumer) processes them.

Copy

Explanation:

  • Producer: The producer adds items to the shared queue. Access to the queue is synchronized using a lock.

  • Consumer: The consumer processes items from the queue, ensuring safe access to the shared resource with a lock.


Conclusion:

Thread synchronization with threading.Lock is crucial to prevent race conditions when multiple threads access shared resources. Using locks ensures that only one thread can modify or access the resource at a time, ensuring data consistency and avoiding errors caused by concurrent access.

Last updated