Parallel Processing
Lab 3: Advanced Thread Synchronization
Eng: Ebrahim Elgazar
OBJECTIVES FOR TODAY
Understand Reentrancy: What is an RLock and why a
normal Lock can cause deadlock.
Solve the Producer-Consumer Problem: A classic
concurrency pattern.
Master Synchronization Primitives:
Semaphore: Manage access to a pool of resources.
Condition: Wait for complex state changes.
Event: Simple, one-way signaling.
Use the Right Tool: Learn why Queue is often the best
solution for producer-consumer scenarios.
Write robust, thread-safe code.
2
RECAP - THE PROBLEM WITH [Link]
A simple Lock provides mutual exclusion. Only one thread can hold the lock at a time.
lock = [Link]()
def critical_section():
[Link]()
try:
# ... do work on shared resource ...
finally:
[Link]()
But what if a function that holds a lock calls another function that tries to acquire the same
lock?
3
DEADLOCK WITH A SIMPLE LOCK
lock = [Link]()
def outer_function():
print("Thread trying to acquire lock...")
[Link]() # Success!
print("Thread acquired lock.")
inner_function() # This function also needs the
lock
[Link]()
PROBLEM: THE THREAD HOLDS THE LOCK
def inner_function(): AND IS NOW WAITING FOR ITSELF TO
print("Thread trying to acquire lock AGAIN...") RELEASE IT. IT WILL WAIT FOREVER.
[Link]() # DEADLOCK!
print("This will never be printed.")
[Link]() 4
THE SOLUTION: REENTRANT LOCK (RLOCK)
An RLock solves this problem. It can be acquired multiple times by the same
thread.
How it works:
It tracks the "owning" thread.
It maintains a recursion level counter.
First acquire() by Thread A:
Owner -> Thread A
Counter -> 1
Second acquire() by Thread A:
Owner is still Thread A, so no blocking.
Counter -> 2
First release() by Thread A:
Counter -> 1
Second release() by Thread A:
Counter -> 0
The lock is now fully released and available to other threads. 5
THE SOLUTION: REENTRANT LOCK (RLOCK)
import threading
rlock = [Link]()
shared = 0
AN RLOCK SOLVES THIS PROBLEM. IT CAN BE def func():
ACQUIRED MULTIPLE TIMES BY THE SAME global shared
THREAD. # Outer critical section
[Link]()
try:
# Inner critical section
KEY TAKEAWAY: USE RLOCK WHEN A THREAD
MAY NEED TO RE-ACQUIRE A LOCK IT ALREADY [Link]()
HOLDS, SUCH AS IN RECURSIVE OR NESTED try:
FUNCTIONS. shared += 1
finally:
[Link]() # Releases inner acquisition
finally:
[Link]() # Releases outer acquisition
6
THE PRODUCER-CONSUMER PROBLEM
A fundamental pattern in concurrent programming.
Producer: Creates data and puts it into a shared buffer.
Consumer: Takes data from the shared buffer and processes it.
Shared Buffer: A temporary storage space (e.g., a variable, a list, a queue).
Synchronization Challenges:
The Consumer must not consume from an empty buffer.
The Producer must not add to a full buffer (if size is limited).
7
SEMAPHORE
A Semaphore is a counter that controls access to a limited number of resources.
[Link](): Decrements the counter. Blocks if counter is zero.
[Link](): Increments the counter. Wakes up a waiting thread.
Analogy: A Parking Garage
Semaphore(5) is a garage with 5 spots.
acquire() is a car entering. If full, it waits.
release() is a car leaving, freeing up a spot.
Binary Semaphore: Semaphore(1) behaves like a Lock.
7
SOLVING PRODUCER-CONSUMER WITH A
SEMAPHORE
# Semaphore starts at 0. The consumer will block
This is a clever signaling mechanism: immediately.
item_available = [Link](0)
We need to signal when an item is available shared_item = None
for consumption.
class Producer(Thread):
We'll use a semaphore initialized to zero. def run(self):
# ... produce an item ...
shared_item = item
print(f"Produced: {item}")
item_available.release() # Signal: "An item is
ready!" (Counter -> 1)
The Consumer calls acquire() first and blocks.
The Producer produces an item and calls class Consumer(Thread):
release(), which unblocks the consumer. def run(self):
This creates perfect alternation. print("Consumer is waiting...")
item_available.acquire() # Waits until counter >
0
print(f"Consumed: {shared_item}")
6
CONDITION
Condition object bundles a Lock with a waiting mechanism.
More expressive than a Semaphore for complex conditions.
Key Methods:
[Link]() / [Link](): Manages the underlying lock.
[Link]():
Releases the lock.
Puts the thread to sleep.
Wakes up and re-acquires the lock when notified.
[Link](): Wakes up one waiting thread.
cond.notify_all(): Wakes up all waiting threads.
7
PRODUCER-CONSUMER WITH CONDITION
condition = [Link]()
shared_item = None
class Producer(Thread):
def run(self):
with condition:
# ... produce an item ...
The canonical pattern for shared_item = item
print(f"Produced: {item}")
wait() is a while loop to guard [Link]() # Wake up the consumer
against "spurious wakeups". class Consumer(Thread):
def run(self):
with condition:
while shared_item is None:
print("Consumer waiting...")
[Link]() # Releases lock and waits
print(f"Consumed: {shared_item}")
shared_item = None
7
The End
Thank Your For Listening