Practical Thread Synchronization Techniques
Now that we've covered the basic concepts of synchronizing shared data in Python threads, let's explore some practical examples of using the various synchronization primitives.
Using Locks
Locks are the most basic synchronization primitive in Python. They ensure that only one thread can access a critical section of code at a time. Here's an example of using a lock to protect a shared counter:
import threading
counter = 0
lock = threading.Lock()
def increment_counter():
global counter
for _ in range(1000000):
with lock:
counter += 1
threads = []
for _ in range(2):
t = threading.Thread(target=increment_counter)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Final counter value: {counter}")
In this example, the lock
object is used to ensure that only one thread can access the critical section of code where the counter
variable is incremented.
Using Semaphores
Semaphores are used to control access to a limited number of resources. Here's an example of using a semaphore to limit the number of concurrent database connections:
import threading
import time
database_connections = 3
connection_semaphore = threading.Semaphore(database_connections)
def use_database():
with connection_semaphore:
print(f"{threading.current_thread().name} acquired a database connection.")
time.sleep(2) ## Simulating database operation
print(f"{threading.current_thread().name} released a database connection.")
threads = []
for _ in range(5):
t = threading.Thread(target=use_database)
threads.append(t)
t.start()
for t in threads:
t.join()
In this example, the connection_semaphore
is used to limit the number of concurrent database connections to 3. Each thread must acquire a "permit" from the semaphore before it can use a database connection.
Using Condition Variables
Condition variables allow threads to wait for certain conditions to be met before continuing their execution. Here's an example of using a condition variable to coordinate the production and consumption of items in a queue:
import threading
import time
queue = []
queue_size = 5
queue_condition = threading.Condition()
def producer():
with queue_condition:
while len(queue) == queue_size:
queue_condition.wait()
queue.append(1)
print(f"{threading.current_thread().name} produced an item. Queue size: {len(queue)}")
queue_condition.notify_all()
def consumer():
with queue_condition:
while not queue:
queue_condition.wait()
item = queue.pop(0)
print(f"{threading.current_thread().name} consumed an item. Queue size: {len(queue)}")
queue_condition.notify_all()
producer_threads = [threading.Thread(target=producer) for _ in range(2)]
consumer_threads = [threading.Thread(target=consumer) for _ in range(3)]
for t in producer_threads + consumer_threads:
t.start()
for t in producer_threads + consumer_threads:
t.join()
In this example, the queue_condition
variable is used to coordinate the production and consumption of items in a queue. Producers wait for the queue to have available space, while consumers wait for the queue to have items.
These examples demonstrate how you can use the various synchronization primitives provided by Python's threading
module to effectively manage shared resources and avoid common concurrency issues.