Python线程安全之三大同步原语

开发 前端
在多线程编码中,因为由上下文切换,所以某个代码块需要作为一个原子单元执行(即不可分割),就需要使用锁的机制来保护;同时在修改共享可变数据的时候,一定也要通过锁机制保护;另外使用的第三方库可能不是线程安全的;不确定线程安全性时使用互斥锁是一种最佳实践。

使用同步原语进行通信和协调

在这些方法中,使用事件、条件和屏障对象等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:用于协调多个线程的执行步调,确保所有线程在同步点上会合后一起继续。
责任编辑:武晓燕 来源: 虞大胆的叽叽喳喳
相关推荐

2019-12-05 15:22:25

高可用网关配置

2024-04-03 08:53:16

PythonGIL线程

2023-12-25 09:58:25

sync包Go编程

2011-06-24 16:26:20

SEO

2019-09-28 23:17:41

zabbix运维监控

2012-12-11 10:35:39

MDM安全MDM

2022-12-20 16:10:31

2011-08-22 17:24:24

Ubuntu11.04

2010-03-16 15:32:26

2015-11-25 14:16:27

联想HPC

2009-03-26 18:36:27

Nehalem服务器IT

2013-01-04 11:40:54

2020-04-29 11:10:22

固件安全网络攻击漏洞

2015-10-30 15:30:54

LevelDBSSTableSybase

2010-03-16 16:34:06

Java编程语言

2023-10-07 08:05:17

数据分析模型行为分析

2021-07-30 06:58:27

python实现三角函数

2010-01-21 11:27:30

linux多线程机制线程同步

2022-03-26 19:14:44

SaaS安全勒索软件网络攻击

2022-03-25 12:17:00

云原生安全开发
点赞
收藏

51CTO技术栈公众号