使用同步原语进行通信和协调
在这些方法中,使用事件、条件和屏障对象等synchronization原语可以促进多个线程之间的通信和协调。
1:事件信号
可以使用事件对象进行信号通信,让一个线程向一个或多个线程通知某个操作,具体操作如下,先创建一个Event事件,事件对象有一个内部标记,默认为False,可以使用.set()设置标记为True,也可以使用.clear() 将其重置为False,当其它线程调用.wait() 方法时,它会阻塞,直到事件对象的内部标志被设置为True。
举个例子:
import threading
import time
from concurrent.futures import ThreadPoolExecutor
bank_open = threading.Event()
transactions_open = threading.Event()
def serve_customer(customer_data):
print(f"{customer_data['name']} 正在等待银行开门。")
bank_open.wait()
print(f"{customer_data['name']} 进入了银行")
if customer_data["type"] == "WITHDRAW_MONEY":
print(f"{customer_data['name']} 正在等待交易开始。")
transactions_open.wait()
print(f"{customer_data['name']} 开始交易。")
# 模拟执行交易的时间
time.sleep(2)
print(f"{customer_data['name']} 完成交易并离开了银行")
else:
# 模拟其他银行业务的时间
time.sleep(2)
print(f"{customer_data['name']} 已离开银行")
customers = [
{"name": "客户 1", "type": "WITHDRAW_MONEY"},
{"name": "客户 2", "type": "CHECK_BALANCE"},
{"name": "客户 3", "type": "WITHDRAW_MONEY"},
{"name": "客户 4", "type": "WITHDRAW_MONEY"},
{"name": "客户 5", "type": "WITHDRAW_MONEY"},
{"name": "客户 6", "type": "WITHDRAW_MONEY"},
]
with ThreadPoolExecutor(max_workers=4) as executor:
for customer_data in customers:
executor.submit(serve_customer, customer_data)
print("银行经理正在准备开门。")
time.sleep(2)
print("银行现在开门了!")
bank_open.set() # 发出银行开门的信号
time.sleep(3)
print("交易现在开放!")
transactions_open.set()
print("所有客户已完成交易。")
猜猜结果是什么:
• 事件控制:bank_open和transactions_open两个事件标记,控制银行何时开门以及交易何时开始,所有客户在银行开门前会被阻塞,等待bank_open.set(),而需取款的客户会继续等待transactions_open.set() 才能执行取款操作。
• 线程池的使用:ThreadPoolExecutor限制了同时执行的线程数,最多服务4个客户,当一个客户完成服务后,线程池会释放一个线程,这样新客户可以继续进入银行。
• CHECK_BALANC类型的客户不需要等待transactions_open事件,因此会在银行开门后直接完成操作并离开。
客户 1 正在等待银行开门。
客户 2 正在等待银行开门。
客户 3 正在等待银行开门。
客户 4 正在等待银行开门。
客户 5 正在等待银行开门。
客户 6 正在等待银行开门。
银行经理正在准备开门。
银行现在开门了!
客户 1 进入了银行
客户 2 进入了银行
客户 3 进入了银行
客户 4 进入了银行
客户 1 正在等待交易开始。
客户 3 正在等待交易开始。
客户 4 正在等待交易开始。
客户 2 已离开银行
客户 5 进入了银行
客户 5 正在等待交易开始。
客户 6 进入了银行
客户 6 正在等待交易开始。
交易现在开放!
客户 1 开始交易。
客户 3 开始交易。
客户 4 开始交易。
客户 5 开始交易。
客户 1 完成交易并离开了银行
客户 3 完成交易并离开了银行
客户 4 完成交易并离开了银行
客户 6 开始交易。
客户 5 完成交易并离开了银行
客户 6 完成交易并离开了银行
所有客户已完成交易。
在需要同时向多个等待线程发出状态变化信号的情况下,事件对象尤其有用。
Conditions条件等待
import random
import threading
import time
from concurrent.futures import ThreadPoolExecutor
customer_available_condition = threading.Condition()
# Customers waiting to be served by the Teller
customer_queue = []
def now():
return time.strftime("%H:%M:%S")
def serve_customers():
while True:
with customer_available_condition:
# Wait for a customer to arrive
while not customer_queue:
print(f"{now()}: Teller is waiting for a customer.")
customer_available_condition.wait()
# Serve the customer
customer = customer_queue.pop(0)
print(f"{now()}: Teller is serving {customer}.")
# Simulate the time taken to serve the customer
time.sleep(random.randint(1, 5))
print(f"{now()}: Teller has finished serving {customer}.")
def add_customer_to_queue(name):
with customer_available_condition:
print(f"{now()}: {name} has arrived at the bank.")
customer_queue.append(name)
customer_available_condition.notify()
customer_names = [
"Customer 1",
"Customer 2",
"Customer 3",
"Customer 4",
"Customer 5",
]
with ThreadPoolExecutor(max_workers=6) as executor:
teller_thread = executor.submit(serve_customers)
for name in customer_names:
# Simulate customers arriving at random intervals
time.sleep(random.randint(1, 3))
executor.submit(add_customer_to_queue, name)
利用条件对象condition来协调生产者-消费者模型中的线程通信,使线程在特定条件满足时再继续执行,从而有效管理多线程中的执行流程。
Condition对象(customer_available_condition)既用作锁来保护共享资源(customer_queue),也用作线程间的通信工具。通过wait()和notify()方法,柜员可以等待客户到来,客户到达后再通知柜员开始服务,从而避免了“忙等”。
在with上下文管理器中,condition对象确保在临界区内自动加锁和释放锁,保护共享资源customer_queue,serve_customers()中的无限循环让柜员可以持续服务来访的客户,而在队列为空时,通过wait()等待,避免无效的资源占用,使用condition实现同步,使得只有在客户队列非空时柜员才会服务,避免了资源的浪费和繁琐的轮询。
可能的输出如下:
10:15:08: Teller is waiting for a customer.
10:15:09: Customer 1 has arrived at the bank.
10:15:09: Teller is serving Customer 1.
10:15:11: Customer 2 has arrived at the bank.
10:15:12: Teller has finished serving Customer 1.
10:15:12: Teller is serving Customer 2.
10:15:13: Teller has finished serving Customer 2.
10:15:13: Teller is waiting for a customer.
10:15:14: Customer 3 has arrived at the bank.
10:15:14: Teller is serving Customer 3.
10:15:15: Customer 4 has arrived at the bank.
10:15:17: Customer 5 has arrived at the bank.
10:15:18: Teller has finished serving Customer 3.
10:15:18: Teller is serving Customer 4.
10:15:22: Teller has finished serving Customer 4.
10:15:22: Teller is serving Customer 5.
10:15:25: Teller has finished serving Customer 5.
10:15:25: Teller is waiting for a customer.
Barriers
import random
import threading
import time
from concurrent.futures import ThreadPoolExecutor
teller_barrier = threading.Barrier(3)
def now():
return time.strftime("%H:%M:%S")
def prepare_for_work(name):
print(f"{now()}: {name} is preparing their counter.")
# Simulate the delay to prepare the counter
time.sleep(random.randint(1, 3))
print(f"{now()}: {name} has finished preparing.")
# Wait for all tellers to finish preparing
teller_barrier.wait()
print(f"{now()}: {name} is now ready to serve customers.")
tellers = ["Teller 1", "Teller 2", "Teller 3"]
with ThreadPoolExecutor(max_workers=4) as executor:
for teller_name in tellers:
executor.submit(prepare_for_work, teller_name)
print(f"{now()}: All tellers are ready to serve customers.")
Barrier用于多线程场景中,当多个线程都到达指定的同步点(即wait()方法)后,所有线程才能继续执行,在银行场景中,Barrier确保所有柜员准备就绪后才能开始为客户服务。
Barrier(3)指定了屏障点需要3个线程才能通过,确保所有3个柜员必须完成准备才会继续,一旦最后一个柜员完成准备,所有线程(柜员)同时通过屏障,开始为客户服务。
总结
在多线程编码中,因为由上下文切换,所以某个代码块需要作为一个原子单元执行(即不可分割),就需要使用锁的机制来保护;同时在修改共享可变数据的时候,一定也要通过锁机制保护;另外使用的第三方库可能不是线程安全的;不确定线程安全性时使用互斥锁是一种最佳实践。
同步工具包括:
- • Lock 和 RLock:用于实现互斥锁,确保某段代码在一个线程执行时不被其他线程打断。
- • Semaphore:用于限制资源的并发访问次数,可以控制同时运行的线程数量。
- • Event:用于在线程间发送信号,通知某些条件已满足。
- • Condition:允许线程等待特定条件并在条件满足时继续执行,常用于生产者-消费者模型。
- • Barrier:用于协调多个线程的执行步调,确保所有线程在同步点上会合后一起继续。