Thread synchronization is a technique that allows multiple threads to access shared resources in a controlled and synchronized manner. This is important because without synchronization, multiple threads can access shared resources simultaneously, leading to race conditions and other unpredictable behavior.
Lock Class
In Python, there are several mechanisms for synchronizing threads. One of the most commonly used mechanisms is the Lock
class, which provides a way to acquire and release a lock that can be used to protect critical sections of code.
To use a Lock
, you can create an instance of the Lock
class and then call the acquire()
method to acquire the lock. Once you have the lock, you can execute the critical section of code, and then call the release()
method to release the lock.
Here’s an example of using a Lock
to synchronize access to a shared resource:
import threading
lock = threading.Lock()
def process_data():
# Acquire the lock
lock.acquire()
try:
# Critical section of code goes here
print('Processing data...')
finally:
# Release the lock
lock.release()
# Start two threads
thread1 = threading.Thread(target=process_data)
thread2 = threading.Thread(target=process_data)
thread1.start()
thread2.start()
In the example above, the process_data()
function is called by two separate threads. When a thread calls the acquire()
method, it will block until the lock is available. This ensures that only one thread can execute the critical section of code at a time.
Semaphore Class
Another mechanism for synchronizing threads is the Semaphore
class, which provides a way to limit the number of threads that can access a shared resource concurrently. A semaphore is initialized with a certain number of permits, and each call to the acquire()
method reduces the number of available permits by 1. When the number of available permits reaches 0, any subsequent calls to acquire()
will block until a permit becomes available.
Here’s an example of using a Semaphore
to limit the number of threads that can access a shared resource concurrently:
import threading
semaphore = threading.Semaphore(2)
def process_data():
# Acquire a permit
semaphore.acquire()
try:
# Critical section of code goes here
print('Processing data...')
finally:
# Release the permit
semaphore.release()
# Start four threads
thread1 = threading.Thread(target=process_data)
thread2 = threading.Thread(target=process_data)
thread3 = threading.Thread(target=process_data)
thread4 = threading.Thread(target=process_data)
thread1.start()
thread2.start()
thread3.start()
thread4.start()
In the example above, the Semaphore
is initialized with a value of 2, which means that only two threads can access the critical section of code concurrently. When the third thread calls the acquire()
method, it will block until one of the other threads releases a permit by calling the release()
method.
Conclusion
There are other mechanisms for synchronizing threads in Python, such as the Event
class, which provides a way to signal between threads, and the Queue
class, which provides a thread-safe way to communicate between threads using a queue data structure.
It’s important to note that while thread synchronization can help prevent race conditions and other unpredictable behavior, it can also introduce performance overhead and complexity to your code. Therefore, it’s important to use thread synchronization judiciously and only when it’s necessary to ensure the correct operation of your program.
Exercises
To review these concepts, we will go through a series of exercises designed to test your understanding and apply what you have learned.
Write a program that creates a shared resource and uses a Lock
to synchronize access to the resource. The program should start two threads, and each thread should increment the shared resource 100 times. At the end, the program should print the final value of the shared resource.
import threading
# Create a shared resource
counter = 0
# Create a lock
lock = threading.Lock()
def increment_counter():
global counter
for i in range(100):
# Acquire the lock
lock.acquire()
try:
# Increment the counter
counter += 1
finally:
# Release the lock
lock.release()
# Start two threads
thread1 = threading.Thread(target=increment_counter)
thread2 = threading.Thread(target=increment_counter)
thread1.start()
thread2.start()
# Wait for the threads to finish
thread1.join()
thread2.join()
# Print the final value of the counter
print(counter)
Write a program that creates a shared resource and uses a Semaphore
to limit the number of threads that can access the resource concurrently. The program should start four threads, and each thread should increment the shared resource 100 times. At the end, the program should print the final value of the shared resource.
import threading
# Create a shared resource
counter = 0
# Create a semaphore with a limit of 2 permits
semaphore = threading.Semaphore(2)
def increment_counter():
global counter
for i in range(100):
# Acquire a permit
semaphore.acquire()
try:
# Increment the counter
counter += 1
finally:
# Release the permit
semaphore.release()
# Start four threads
thread1 = threading.Thread(target=increment_counter)
thread2 = threading.Thread(target=increment_counter)
thread3 = threading.Thread(target=increment_counter)
thread4 = threading.Thread(target=increment_counter)
thread1.start()
thread2.start()
thread3.start()
thread4.start()
# Wait for the threads to finish
thread1.join()
thread2.join()
thread3.join()
thread4.join()
# Print the final value of the counter
print(counter)
Write a program that creates a shared resource and uses a Condition
to synchronize access to the resource. The program should start two threads, and each thread should increment the shared resource 100 times. One thread should be allowed to run only when the shared resource is even, and the other thread should be allowed to run only when the shared resource is odd. At the end, the program should print the final value of the shared resource.
import threading
# Create a shared resource
counter = 0
# Create a condition
condition = threading.Condition()
def increment_counter_even():
global counter
for i in range(100):
# Acquire the condition
with condition:
# Wait until the counter is even
while counter % 2 != 0:
condition.wait()
# Increment the counter
counter += 1
# Notify other threads that the counter has been incremented
condition.notify()
def increment_counter_odd():
global counter
for i in range(100):
# Acquire the condition
with condition:
# Wait until the counter is odd
while counter % 2 != 1:
condition.wait()
# Increment the counter
counter += 1
# Notify other threads that the counter has been incremented
condition.notify()
# Start two threads
thread1 = threading.Thread(target=increment_counter_even)
thread2 = threading.Thread(target=increment_counter_odd)
thread1.start()
thread2.start()
# Wait for the threads to finish
thread1.join()
thread2.join()
# Print the final value of the counter
print(counter)
Write a program that creates a shared resource and uses a Semaphore
to synchronize access to the resource. The program should start two threads, and each thread should increment the shared resource 100 times. One thread should be allowed to run only when the shared resource is even, and the other thread should be allowed to run only when the shared resource is odd. At the end, the program should print the final value of the shared resource.
import threading
# Create a shared resource
counter = 0
# Create a semaphore
semaphore = threading.Semaphore()
def increment_counter_even():
global counter
for i in range(100):
# Acquire the semaphore
semaphore.acquire()
# Increment the counter
counter += 1
# Release the semaphore
semaphore.release()
def increment_counter_odd():
global counter
for i in range(100):
# Acquire the semaphore
semaphore.acquire()
# Increment the counter
counter += 1
# Release the semaphore
semaphore.release()
# Start two threads
thread1 = threading.Thread(target=increment_counter_even)
thread2 = threading.Thread(target=increment_counter_odd)
thread1.start()
thread2.start()
# Wait for the threads to finish
thread1.join()
thread2.join()
# Print the final value of the counter
print(counter)
Write a program that creates a shared resource and uses a Queue
to communicate between threads. The program should start two threads, and one thread should increment the shared resource 100 times, while the other thread should print the value of the shared resource after each increment. At the end, the program should print the final value of the shared resource.
import threading
import queue
# Create a shared resource
counter = 0
# Create a queue
q = queue.Queue()
def increment_counter():
global counter
for i in range(100):
# Increment the counter
counter += 1
# Put the new value of the counter in the queue
q.put(counter)
def print_counter():
global counter
for i in range(100):
# Get the value of the counter from the queue
counter = q.get()
# Print the value of the counter
print(counter)
# Start two threads
thread1 = threading.Thread(target=increment_counter)
thread2 = threading.Thread(target=print_counter)
thread1.start()
thread2.start()
# Wait for the threads to finish
thread1.join()
thread2.join()
# Print the final value of the counter
print(counter)