第七章 基于线程的并行 -- 锁对象、递归锁对象、条件对象

第零章 学前准备
第一章 数据结构 – 基本数据类型
第一章 数据结构 – 字符串
第一章 数据结构 – 列表、元组和切片
第一章 数据结构 – 字典
第一章 数据结构 – 集合
第一章 – 数组、队列、枚举
第一章 数据结构 – 序列分类
第二章 控制流程
第三章 函数也是对象 – 函数定义以及参数
第三章 函数也是对象 – 高阶函数以及装饰器
第三章 函数也是对象 – lambda 表达式、可调用函数及内置函数
第四章 面向对象编程 – 自定义类、属性、方法和函数
第四章 面向对象编程–魔术方法1
第四章 面向对象编程 – 魔术方法2
第四章 面向对象编程 – 可迭代的对象、迭代器和生成器
第四章 面向对象编程 – 继承、接口
第四章 面向对象编程 – 对象引用
第四章 面向对象编程–案例
第五章 文件操作
第六章 异常
第七章 基于线程的并行 – threading
第七章 基于线程的并行 – 锁对象、递归锁对象、条件对象


第七章 基于线程的并行 – 锁对象、递归锁对象、条件对象

7.3 锁对象

原始锁是一个在锁定时不属于特定线程的同步基元组件。原始锁处于 “锁定” 或者 “非锁定” 两种状态之一。它被创建时为非锁定状态。它有两个基本方法, acquire()release() 。当状态为非锁定时, acquire() 将状态改为 锁定 并立即返回。当状态是锁定时, acquire() 将阻塞至其他线程调用 release() 将其改为非锁定状态,然后 acquire() 调用重置其为锁定状态并返回。 release() 只在锁定状态下调用; 它将状态改为非锁定并立即返回。如果尝试释放一个非锁定的锁,则会引发 RuntimeError 异常。锁同样支持 上下文管理协议。当多个线程在 acquire() 等待状态转变为未锁定被阻塞,然后 release() 重置状态为未锁定时,只有一个线程能继续执行;至于哪个等待线程继续执行没有定义,并且会根据实现而不同。所有方法的执行都是原子性的。

  1. class threading.Lock:实现原始锁对象的类。一旦一个线程获得一个锁,会阻塞随后尝试获得锁的线程,直到它被释放;任何线程都可以释放它。需要注意的是 Lock 其实是一个工厂函数,返回平台支持的具体锁类中最有效的版本的实例。
  2. acquire(blocking=True, timeout=-1)可以阻塞或非阻塞地获得锁。当调用时参数 blocking 设置为 True (缺省值),阻塞直到锁被释放,然后将锁锁定并返回 True 。在参数 blocking 被设置为 False 的情况下调用,将不会发生阻塞。如果调用时 blocking 设为 True 会阻塞,并立即返回 False ;否则,将锁锁定并返回 True。当浮点型 timeout 参数被设置为正值调用时,只要无法获得锁,将最多阻塞 timeout 设定的秒数。timeout 参数被设置为 -1 时将无限等待。当 blocking 为 false 时,timeout 指定的值将被忽略。如果成功获得锁,则返回 True,否则返回 False (例如发生 超时 的时候)。
  3. release():释放一个锁。这个方法可以在任何线程中调用,不单指获得锁的线程。当锁被锁定,将它重置为未锁定,并返回。如果其他线程正在等待这个锁解锁而被阻塞,只允许其中一个允许。在未锁定的锁调用时,会引发 RuntimeError 异常。没有返回值。
  4. locked():如果获得了锁则返回真值。
from abc import ABC, abstractmethod
import time
from datetime import datetime
from random import randint
import threading


class BankCard:
    def __init__(self, card_number, balance):
        self.__balance = balance
        self.card_number = card_number

    def __repr__(self):
        return f"BankCard({
      
      self.card_number}:{
      
      self.balance})"

    @property
    def balance(self):
        return self.__balance

    @balance.setter
    def balance(self, balance):
        if isinstance(balance, (int, float)) and balance >= 0:
            self.__balance = balance
        else:
            raise ValueError(
                f'{
      
      balance} must be int or float and >= 0')


class PayMethod:
    lock = threading.Lock()  # 注意,该锁不能放在实例中

    def __init__(self, bank_cards):
        self.bank_cards = bank_cards
        self.tag = self.__class__.__name__

    def pay_with_lock(self, fee, bank_card: BankCard):
        with PayMethod.lock:
            self.pay(fee, bank_card)

    def pay(self, fee, bank_card: BankCard):
        """未加锁,余额扣除有误"""
        start_date_time = datetime.now()
        print(f"{
      
      self.tag}\t开始交易,时间:{
      
      start_date_time}")

        print(f"{
      
      self.tag}\t1、开始查询银行卡余额")
        delay_seconds = randint(1, 5)
        time.sleep(delay_seconds)
        print(f"{
      
      self.tag}\t网络延迟{
      
      delay_seconds}秒,查询余额成功:{
      
      str(bank_card)}")

        print(f"{
      
      self.tag}\t2、计算消费后余额")
        post_balance = bank_card.balance-fee

        print(f"{
      
      self.tag}\t3、更新银行卡余额")
        delay_seconds = randint(1, 5)
        time.sleep(delay_seconds)
        try:
            bank_card.balance = post_balance
        except:
            print(f"{
      
      self.tag}\t消费{
      
      fee}元,银行卡余额{
      
      bank_card.balance},余额不足,交易失败")
            return
        print(f"{
      
      self.tag}\t网络延迟{
      
      delay_seconds}秒,更新银行卡余额成功:{
      
      str(bank_card)}")

        end_date_time = datetime.now()
        print(f"{
      
      self.tag}\t结束交易,时间:{
      
      end_date_time}")

        print(f"{
      
      self.tag}\t交易成功,开始时间:{
      
      start_date_time},结束时间:{
      
      end_date_time},交易耗时:{
      
      end_date_time-start_date_time}, 消费金额:{
      
      fee},卡片余额:{
      
      str(bank_card)}")


class WecheatPay(PayMethod):
    pass


class AliPay(PayMethod):
    pass


def run(locked: bool):
    print("-"*30)
    print("with lock" if locked else "without lock")
    pay_method = f"pay{
      
      '_with_lock' if locked else ''}"
    bank_card = BankCard("000000", 1000)
    wecheatpay = WecheatPay([bank_card])
    alipay = AliPay([bank_card])
    wecheat = threading.Thread(target=getattr(wecheatpay, pay_method), args=(
        100, wecheatpay.bank_cards[0]))
    ali = threading.Thread(target=getattr(alipay, pay_method), args=(
        300, alipay.bank_cards[0]))
    wecheat.start()
    ali.start()
    while any((ali.is_alive(), wecheat.is_alive())):
        pass
    print("*"*10)
    print(f"查询余额:{
      
      bank_card}")


if __name__ == "__main__":
    run(True)
    run(False)
------------------------------
with lock
WecheatPay    开始交易,时间:2022-12-04 22:12:36.858542
WecheatPay    1、开始查询银行卡余额
WecheatPay    网络延迟3秒,查询余额成功:BankCard(000000:1000)
WecheatPay    2、计算消费后余额
WecheatPay    3、更新银行卡余额
WecheatPay    网络延迟1秒,更新银行卡余额成功:BankCard(000000:900)
WecheatPay    结束交易,时间:2022-12-04 22:12:40.898742
WecheatPay    交易成功,开始时间:2022-12-04 22:12:36.858542,结束时间:2022-12-04 22:12:40.898742,交易耗时:0:00:04.040200, 消费金额:100,卡片余额:BankCard(000000:900)
AliPay    开始交易,时间:2022-12-04 22:12:40.930654
AliPay    1、开始查询银行卡余额
AliPay    网络延迟1秒,查询余额成功:BankCard(000000:900)
AliPay    2、计算消费后余额
AliPay    3、更新银行卡余额
AliPay    网络延迟5秒,更新银行卡余额成功:BankCard(000000:600)
AliPay    结束交易,时间:2022-12-04 22:12:47.028304
AliPay    交易成功,开始时间:2022-12-04 22:12:40.930654,结束时间:2022-12-04 22:12:47.028304,交易耗时:0:00:06.097650, 消费金额:300,卡片余额:BankCard(000000:600)
**********
查询余额:BankCard(000000:600)
------------------------------
without lock
WecheatPay    开始交易,时间:2022-12-04 22:12:47.028304
WecheatPay    1、开始查询银行卡余额
AliPay    开始交易,时间:2022-12-04 22:12:47.029274
AliPay    1、开始查询银行卡余额
WecheatPay    网络延迟3秒,查询余额成功:BankCard(000000:1000)
WecheatPay    2、计算消费后余额
WecheatPay    3、更新银行卡余额
AliPay    网络延迟5秒,查询余额成功:BankCard(000000:1000)
AliPay    2、计算消费后余额
AliPay    3、更新银行卡余额
WecheatPay    网络延迟3秒,更新银行卡余额成功:BankCard(000000:900)
WecheatPay    结束交易,时间:2022-12-04 22:12:53.072072
WecheatPay    交易成功,开始时间:2022-12-04 22:12:47.028304,结束时间:2022-12-04 22:12:53.072072,交易耗时:0:00:06.043768, 消费金额:100,卡片余额:BankCard(000000:900)
AliPay    网络延迟1秒,更新银行卡余额成功:BankCard(000000:700)
AliPay    结束交易,时间:2022-12-04 22:12:53.104015
AliPay    交易成功,开始时间:2022-12-04 22:12:47.029274,结束时间:2022-12-04 22:12:53.104015,交易耗时:0:00:06.074741, 消费金额:300,卡片余额:BankCard(000000:700)
**********
查询余额:BankCard(000000:700)

7.4 递归锁对象

重入锁是一个可以被同一个线程多次获取的同步基元组件。在内部,它在基元锁的锁定/非锁定状态上附加了 “所属线程” 和 “递归等级” 的概念。在锁定状态下,某些线程拥有锁 ; 在非锁定状态下, 没有线程拥有它。若要锁定锁,线程调用其 acquire() 方法;一旦线程拥有了锁,方法将返回。若要解锁,线程调用 release() 方法。 acquire()/release() 对可以嵌套;只有最终 release() (最外面一对的 release() ) 将锁解开,才能让其他线程继续处理 acquire() 阻塞。递归锁也支持 上下文管理协议。

  1. class threading.RLock
    此类实现了重入锁对象。重入锁必须由获取它的线程释放。一旦线程获得了重入锁,同一个线程再次获取它将不阻塞;线程必须在每次获取它时释放一次。需要注意的是 RLock 其实是一个工厂函数,返回平台支持的具体递归锁类中最有效的版本的实例。
  2. acquire(blocking=True, timeout=-1):可以阻塞或非阻塞地获得锁。当无参数调用时: 如果这个线程已经拥有锁,递归级别增加一,并立即返回。否则,如果其他线程拥有该锁,则阻塞至该锁解锁。一旦锁被解锁(不属于任何线程),则抢夺所有权,设置递归等级为一,并返回。如果多个线程被阻塞,等待锁被解锁,一次只有一个线程能抢到锁的所有权。在这种情况下,没有返回值。当发起调用时将 blocking 参数设为真值,则执行与无参数调用时一样的操作,然后返回 True。当发起调用时将 blocking 参数设为假值,则不进行阻塞。 如果一个无参数调用将要阻塞,则立即返回 False;在其他情况下,执行与无参数调用时一样的操作,然后返回 True。当发起调用时将浮点数的 timeout 参数设为正值时,只要无法获得锁,将最多阻塞 timeout 所指定的秒数。 如果已经获得锁则返回 True,如果超时则返回假值。
  3. release():释放锁,自减递归等级。如果减到零,则将锁重置为非锁定状态(不被任何线程拥有),并且,如果其他线程正被阻塞着等待锁被解锁,则仅允许其中一个线程继续。如果自减后,递归等级仍然不是零,则锁保持锁定,仍由调用线程拥有。只有当前线程拥有锁才能调用这个方法。如果锁被释放后调用这个方法,会引起 RuntimeError 异常。没有返回值。

7.5 条件对象

条件变量总是与某种类型的锁对象相关联,锁对象可以通过传入获得,或者在缺省的情况下自动创建。当多个条件变量需要共享同一个锁时,传入一个锁很有用。锁是条件对象的一部分,你不必单独地跟踪它。条件变量服从 上下文管理协议:使用 with 语句会在它包围的代码块内获取关联的锁。 acquire()release() 方法也能调用关联锁的相关方法。其它方法必须在持有关联的锁的情况下调用。 wait() 方法释放锁,然后阻塞直到其它线程调用 notify() 方法或 notify_all() 方法唤醒它。一旦被唤醒, wait() 方法重新获取锁并返回。它也可以指定超时时间。

The notify() method wakes up one of the threads waiting for the condition variable, if any are waiting. The notify_all() method wakes up all threads waiting for the condition variable.

注意: notify() 方法和 notify_all() 方法并不会释放锁,这意味着被唤醒的线程不会立即从它们的 wait() 方法调用中返回,而是会在调用了 notify() 方法或 notify_all() 方法的线程最终放弃了锁的所有权后返回。

使用条件变量的典型编程风格是将锁用于同步某些共享状态的权限,那些对状态的某些特定改变感兴趣的线程,它们重复调用 wait() 方法,直到看到所期望的改变发生;而对于修改状态的线程,它们将当前状态改变为可能是等待者所期待的新状态后,调用 notify() 方法或者 notify_all() 方法。例如,下面的代码是一个通用的无限缓冲区容量的生产者-消费者情形:

# Consume one item
with cv:
    while not an_item_is_available():
        cv.wait()
    get_an_available_item()

# Produce one item
with cv:
    make_an_item_available()
    cv.notify()

使用 while 循环检查所要求的条件成立与否是有必要的,因为 wait() 方法可能要经过不确定长度的时间后才会返回,而此时导致 notify() 方法调用的那个条件可能已经不再成立。这是多线程编程所固有的问题。 wait_for() 方法可自动化条件检查,并简化超时计算。

# Consume an item
with cv:
    cv.wait_for(an_item_is_available)
    get_an_available_item()

选择 notify() 还是 notify_all() ,取决于一次状态改变是只能被一个还是能被多个等待线程所用。例如在一个典型的生产者-消费者情形中,添加一个项目到缓冲区只需唤醒一个消费者线程。

  1. class threading.Condition(lock=None)
    实现条件变量对象的类。一个条件变量对象允许一个或多个线程在被其它线程所通知之前进行等待。如果给出了非 Nonelock 参数,则它必须为 Lock 或者 RLock 对象,并且它将被用作底层锁。否则,将会创建新的 RLock 对象,并将其用作底层锁。
  2. acquire(*args):请求底层锁。此方法调用底层锁的相应方法,返回值是底层锁相应方法的返回值。
  3. release():释放底层锁。此方法调用底层锁的相应方法。没有返回值。
  4. wait(timeout=None):等待直到被通知或发生超时。如果线程在调用此方法时没有获得锁,将会引发 RuntimeError 异常。这个方法释放底层锁,然后阻塞,直到在另外一个线程中调用同一个条件变量的 notify()notify_all() 唤醒它,或者直到可选的超时发生。一旦被唤醒或者超时,它重新获得锁并返回。当提供了 timeout 参数且不是 None 时,它应该是一个浮点数,代表操作的超时时间,以秒为单位(可以为小数)。当底层锁是个 RLock ,不会使用它的 release() 方法释放锁,因为当它被递归多次获取时,实际上可能无法解锁。相反,使用了 RLock 类的内部接口,即使多次递归获取它也能解锁它。 然后,在重新获取锁时,使用另一个内部接口来恢复递归级别。
  5. wait_for(predicate, timeout=None):等待,直到条件计算为真。 predicate 应该是一个可调用对象而且它的返回值可被解释为一个布尔值。可以提供 timeout 参数给出最大等待时间。这个实用方法会重复地调用 wait() 直到满足判断式或者发生超时。返回值是判断式最后一个返回值,而且如果方法发生超时会返回 False 。忽略超时功能,调用此方法大致相当于编写:
    while not predicate():
        cv.wait()
    
    因此,规则同样适用于 wait() :锁必须在被调用时保持获取,并在返回时重新获取。 随着锁定执行判断式。
  6. notify(n=1)
    默认唤醒一个等待这个条件的线程。如果调用线程在没有获得锁的情况下调用这个方法,会引发 RuntimeError 异常。这个方法唤醒最多 n 个正在等待这个条件变量的线程;如果没有线程在等待,这是一个空操作。当前实现中,如果至少有 n 个线程正在等待,准确唤醒 n 个线程。但是依赖这个行为并不安全。未来,优化的实现有时会唤醒超过 n 个线程。注意:被唤醒的线程并没有真正恢复到它调用的 wait() ,直到它可以重新获得锁。 因为 notify() 不释放锁,其调用者才应该这样做。
  7. notify_all()
    唤醒所有正在等待这个条件的线程。这个方法行为与 notify() 相似,但并不只唤醒单一线程,而是唤醒所有等待线程。如果调用线程在调用这个方法时没有获得锁,会引发 RuntimeError 异常。
import threading
import time
from datetime import datetime


class Bread:
    shelflife = 15

    def __init__(self, birthday: datetime, bread_store):
        self.birthday = birthday
        self.bread_store = bread_store

    @property
    def is_past_date(self):
        return (datetime.now()-self.birthday).seconds > Bread.shelflife


class Baker:
    def __init__(self, name: str, bread_store, on_duty: bool, productivity: int):
        self.name = name
        self.bread_store = bread_store
        self.on_duty = on_duty
        self.productivity = productivity
        super().__init__()

    def bake(self):
        while self.bread_store.opened:
            time.sleep(self.productivity)
            with self.bread_store.bread_condition:
                self.bread_store.bread_condition.wait_for(
                    self.bread_store.bread_not_enough)
                birthday = datetime.now()
                bread = Bread(birthday, self.bread_store)
                self.bread_store.bread_shelf.add(bread)
                print(f"Produce:{
      
      self.name} make bread: {
      
      birthday}")
                self.bread_store.bread_condition.notify_all()
                self.bread_store.bread_condition.wait()


class BreadStore:
    def __init__(self, max_bread_number: int, bakers: set, diners: set, bread_shelf: set):
        self.max_bread_number = max_bread_number
        self.bread_shelf = bread_shelf
        self.bakers = bakers
        self.diners = diners
        self.bread_condition: threading.Condition = threading.Condition()
        self.closed = False
        self.start_time = datetime.now()

    def bread_available(self):
        return len(self.bread_shelf) > 5

    def bread_not_enough(self):
        return len(self.bread_shelf) < self.max_bread_number

    @property
    def opened(self):
        return (datetime.now()-self.start_time).seconds <= 300

    def produce_bread(self):
        for baker in filter(lambda baker: baker.on_duty, self.bakers):
            threading.Thread(target=baker.bake, name=baker.name).start()

    def sell_bread(self):
        for diner in self.diners:
            threading.Thread(target=diner.eat, name=diner.name).start()

    def remove_past_date(self):
        while self.opened:
            with self.bread_condition:
                past_date_set = set()
                for bread in self.bread_shelf:
                    if bread.is_past_date:
                        past_date_set.add(bread)
                        print(
                            f"Remove:{
      
      bread.birthday} is past-date, remove it from shelf")
                self.bread_shelf -= past_date_set


class BreadLover:
    def __init__(self, name, bread_store, speed):
        self.name = name
        self.bread_store = bread_store
        self.speed = speed
        super().__init__()

    def eat(self):
        while self.bread_store.opened:
            with self.bread_store.bread_condition:
                self.bread_store.bread_condition.wait_for(
                    self.bread_store.bread_available)
                bread = self.bread_store.bread_shelf.pop()
                print(f"Sell:{
      
      self.name} eated bread {
      
      bread.birthday}")
                self.bread_store.bread_condition.notify_all()
                self.bread_store.bread_condition.wait()
                time.sleep(self.speed)


if __name__ == "__main__":
    bread_store = BreadStore(
        max_bread_number=15, bakers=set(), diners=set(), bread_shelf=set())
    bread_store.bakers.add(Baker("xiao", bread_store, True, 5))
    bread_store.bakers.add(Baker("ming", bread_store, True, 6))
    bread_store.bakers.add(Baker("tian", bread_store, True, 7))
    bread_store.bakers.add(Baker("shan", bread_store, True, 3))
    bread_store.bakers.add(Baker("di", bread_store, True, 4))
    bread_store.diners.add(BreadLover("ying", bread_store, 5))
    bread_store.diners.add(BreadLover("yang", bread_store, 5))

    bread_store.produce_bread()
    bread_store.sell_bread()
    threading.Thread(target=bread_store.remove_past_date).start()

猜你喜欢

转载自blog.csdn.net/qq_31654025/article/details/132781101