python 线程锁

2023-11-19

目录

1、线程安全介绍

2、threading5种常见锁

2.1 同步锁(互斥锁)Lock

(1) 基本介绍及相关方法

(2) 给案例加lock锁

(3)with语句

2.2 递归锁Rlock

(1)基本介绍及相关方法

(2) 给案例加递归锁

(3) with 语句

2.3 条件锁Condition

(1)基本介绍和方法

(2)with语句

(3)案例

2.4 事件锁Event

(1)基本介绍和方法

(2)使用方式

(3)案例

2.5 信号量锁Semaphore

(1)基本介绍和方法

(2)案例

(3)with语句

3、死锁现象

4、总结

1、线程安全介绍

线程安全的问题最主要还是由线程切换导致的。

线程安全是多线程或多进程编程中的一个概念,在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

线程不安全案例:

创建2个线程:线程1对num进行一千万次+1的操作;线程2对num进行一千万次-1的操作

import threading

num = 0

def add():
    global num
    for i in range(10_000_000):
        num += 1

def sub():
    global num
    for i in range(10_000_000):
        num -= 1

if __name__ == "__main__":
    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

    print("num result : %s" % num)

运行三次结果分别如下:

num result : 645898
num result : -331131
num result : 901713

num最后并不是我们所想象的结果0。因此需要通过来保障线程切换的时机。

2、threading5种常见锁

threading模块中提供了5种最常见的锁:

  • 同步锁:lock(一次只能放行一个)

  • 递归锁:rlock(一次只能放行一个)

  • 条件锁:condition(一次可以放行任意个)

  • 事件锁:event(一次全部放行)

  • 信号量锁:semaphore(一次可以放行特定个)

2.1 同步锁(互斥锁)Lock

(1) 基本介绍及相关方法

  • 互斥指的是某一资源同一时刻仅能有一个访问者对其进行访问,具有唯一性和排他性,但是互斥无法限制访问者对资源的访问顺序,即访问是无序的
  • 同步是指在互斥的基础上(大多数情况),通过其他机制实现访问者对资源的有序访问
  • 同步其实已经实现了互斥,是互斥的一种更为复杂的实现,因为它在互斥的基础上实现了有序访问的特点
# 生成互斥锁对象
lock = threading.Lock()

# 获取锁,未获取到程序将会阻塞(当一个线程在执行被上锁的代码块时,将不允许切换到其他线程)
lock.acquire()

# 释放锁
lock.release()

# 判断该锁对象是否处于上锁状态
lock.locked()

(2) 给案例加lock锁

import threading

num = 0
lock = threading.Lock()  # 生成互斥锁对象

def add():
    global num
    lock.acquire()   # 上锁
    for i in range(10_000_000):
        num += 1
    lock.release()   # 释放锁

def sub():
    global num
    lock.acquire()  # 上锁
    for i in range(10_000_000):
        num -= 1
    lock.release()    # 释放锁

if __name__ == "__main__":
    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

    print("num result : %s" % num)

执行上述代码,每次结果均为0。

上述代码完全变成了串行的状态,对于这种计算密集型I/O业务来说,还不如直接使用串行化单线程执行来得快,所以这个例子仅作为一个示例,不能概述锁真正的用途。

(3)with语句

由于threading.Lock()对象中实现了enter__()与__exit()方法,故可以使用with语句进行上下文管理形式的加锁解锁操作:

import threading

num = 0
lock = threading.Lock()


def add():
    with lock:
        # 自动加锁
        global num
        for i in range(10_000_000):
            num += 1
        # 自动解锁


def sub():
    with lock:
        # 自动加锁
        global num
        for i in range(10_000_000):
            num -= 1
        # 自动解锁


if __name__ == "__main__":
    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

    print("num result : %s" % num)

上述代码与(2)中代码效果一样。

2.2 递归锁Rlock

(1)基本介绍及相关方法

        有时候在一些逻辑当中,可能需要多次获取锁也就是嵌套锁,如果这种情况还是使用互斥锁的话就会造成死锁,这个时候就应该用到可重入锁(递归锁) RLock()

        递归锁是同步锁的一个升级版本,在同步锁的基础上可以做到连续重复使用多次acquire()后再重复使用多次release()的操作,但是一定要注意加锁次数和解锁次数必须一致,否则也将引发死锁现象。

lock = threading.RLock()   # 返回一个递归锁对象

# 获取锁,未获取到程序将会阻塞(当一个线程在执行被上锁的代码块时,将不允许切换到其他线程)
lock.acquire()

# 释放锁
lock.release()

# 判断该锁对象是否处于上锁状态,返回布尔值
lock.locked()

(2) 给案例加递归锁

        下面这段操作如果使用同步锁则会发生死锁现象,因为第二次去获得锁的时候锁已经被拿走了,锁还没有释放又再一次请求,这样就形成死锁了。

        但是递归锁不会,递归锁在同一个线程里面可以连续多次调用 acquire,除了这一点,可重入锁和互斥锁没有其他区别。


import threading

num = 0
lock = threading.RLock()   # 返回一个递归锁对象


def add():
    lock.acquire()
    lock.acquire()
    global num
    for i in range(10_000_000):
        num += 1
    lock.release()
    lock.release()


def sub():
    lock.acquire()
    lock.acquire()
    global num
    for i in range(10_000_000):
        num -= 1
    lock.release()
    lock.release()


if __name__ == "__main__":
    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

    print("num result : %s" % num)

(3) with 语句

由于threading.RLock()对象中实现了enter__()与__exit()方法,故可以使用with语句进行上下文管理形式的加锁解锁操作:

import threading

num = 0
lock = threading.RLock()   # 返回一个递归锁对象

def add():
    with lock:
        global num
        for i in range(10_000_000):
            num += 1

def sub():
    with lock:
        global num
        for i in range(10_000_000):
            num -= 1

if __name__ == "__main__":
    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

    print("num result : %s" % num)

2.3 条件锁Condition

(1)基本介绍和方法

Condition 是 python 中的条件锁,内部也是通过 Lock() 和 RLock() 锁实现,所以具有他们的阻塞特性, 并且在这基础上增加了暂停线程运行的功能,所以可以用来同步复杂的线程间通信。

# 生成一个条件锁对象
cond = threading.Condition()

# 上锁
cond.acquire()

# 解锁
cond.release()

# 挂起线程,直到收到一个 notify 通知才会被唤醒
cond.wait()

# 唤醒一个 Condition 的 waiting 池中的线程
cond.notify()

# 唤醒所有 Condition 的 waiting 池中的线程
cond.notify_all()

(2)with语句

由于threading.Condition()对象中实现了enter__()与__exit()方法,故可以使用with语句进行上下文管理形式的加锁解锁操作。

(3)案例

该案例使用with语句实现加锁解锁。

import threading

class Zhou(threading.Thread):
    def __init__(self, cond):
        super().__init__(name="周杰伦")
        self.cond = cond
                 
    def run(self):
        with self.cond:
            # Condition()对象中也实现了__enter__()与__exit__()魔法方法,所以也是可以通过 with 语句调用的
            print("{}: 海平面远方开始阴霾, 悲伤要怎么平静纯白".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{}: 你用唇语说你要离开, 那难过无声慢了下来 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{}: 转身离开, 你有话说不出来 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{}: 我们的爱, 差异一直存在, 等待竟累积成伤害 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{}: 蔚蓝的珊瑚海, 错过瞬间苍白 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{}: 热情不再, 笑容勉强不来, 爱深埋珊瑚海".format(self.name))
        
                         


class Liang(threading.Thread):
    def __init__(self, cond):
        super().__init__(name="梁心颐")
        self.cond = cond
        
    def run(self):
        with self.cond:
            # 在调用with cond 之后才能调用 wait 或者 notify 方法
            self.cond.wait()
            print("{}: 我的脸上始终挟带, 一抹浅浅的无奈".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{}: 汹涌潮水, 你听明白, 不是浪而是泪海 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{}: 海鸟跟鱼相爱, 只是一场意外 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{}: 转身离开, 分手说不出来 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{}: 当初彼此, 不够成熟坦白".format(self.name))
            self.cond.notify()
                         


if __name__ == "__main__":
    cond = threading.Condition()
    liang = Liang(cond)
    zhou = Zhou(cond)
    # 这里的启动顺序很重要
    liang.start()
    zhou.start()

两个线程通过 cond 通信,阻塞自己并唤醒对方,从而达到有序执行,打印结果如下:

周杰伦: 海平面远方开始阴霾, 悲伤要怎么平静纯白
梁心颐: 我的脸上始终挟带, 一抹浅浅的无奈
周杰伦: 你用唇语说你要离开, 那难过无声慢了下来 
梁心颐: 汹涌潮水, 你听明白, 不是浪而是泪海 
周杰伦: 转身离开, 你有话说不出来 
梁心颐: 海鸟跟鱼相爱, 只是一场意外 
周杰伦: 我们的爱, 差异一直存在, 等待竟累积成伤害 
梁心颐: 转身离开, 分手说不出来 
周杰伦: 蔚蓝的珊瑚海, 错过瞬间苍白 
梁心颐: 当初彼此, 不够成熟坦白
周杰伦: 热情不再, 笑容勉强不来, 爱深埋珊瑚海

2.4 事件锁Event

(1)基本介绍和方法

Event 是 python 中的事件锁,它是根据条件锁来实现的,条件锁一次可以放行任意个子线程,而事件锁一次只能放行全部。

Event 对象中有一个信号标志,默认为 False,可以把这个标志看做为一个红绿灯,当红灯时所有子线程都暂停运行,并进入“等待”状态,当绿灯时所有子线程都恢复“运行”。

如果一个线程等待一个 Event 对象,那么这个 Event 对象的标志将决定这个线程是否会被阻塞。需要注意的是,如果一个线程将 Event 对象的标志设置为真,那么所有等待这个 Event 对象的线程都将会被放行。

# 生成一个事件锁对象
eve = threading.Event()


# 将事件锁设置为红灯状态,即所有线程暂停运行
eve.clear()
 
# 判断事件锁的状态,红灯为False,绿灯为True
eve.is_set()


# 将当前线程设置’等待‘状态,只有该线程接到"绿灯通知"或者超时时间到期之后才会继续运行,
#  在等待状态下的线程将允许系统根据策略自行切换到其他线程中运行
eve.wait(timeout=None) 


# 将事件锁设置为绿灯状态,即所有线程恢复运行
eve.set()

(2)使用方式

事件锁 Event 并没有实现魔法方法 enter() 和 exit(),所以不能通过 with 语句调用,只能按照常规方式。

(3)案例

我们来看一个模拟交通红绿灯的例子

import time
import threading


def light(eve):
    print(f'当前时间:{time.ctime()}, 红灯还有 5s 结束!')
    time.sleep(5)
    print(f'当前时间:{time.ctime()}, 绿灯亮!')
    eve.set()  # 设置事件锁标志为 True


def car(eve, name):
    print(f'当前时间:{time.ctime()}, 车 {name} 正在等红灯')
    eve.wait()  # 将当前线程设置为等待状态,等待事件锁标志为 True 再执行
    print(f'当前时间:{time.ctime()}, 车 {name} 开始通行')


if __name__ == '__main__':
    eve = threading.Event()  # 事件锁默认标志为 False
    t1 = threading.Thread(target=light, args=(eve,))
    t1.start()

    for each in 'ABCDE':
        t2 = threading.Thread(target=car, args=(eve, each))
        t2.start()

执行结果如下:

当前时间:Wed Jun 28 15:15:35 2023, 红灯还有 5s 结束!
当前时间:Wed Jun 28 15:15:35 2023, 车 A 正在等红灯
当前时间:Wed Jun 28 15:15:35 2023, 车 B 正在等红灯
当前时间:Wed Jun 28 15:15:35 2023, 车 C 正在等红灯
当前时间:Wed Jun 28 15:15:35 2023, 车 D 正在等红灯
当前时间:Wed Jun 28 15:15:35 2023, 车 E 正在等红灯
当前时间:Wed Jun 28 15:15:40 2023, 绿灯亮!
当前时间:Wed Jun 28 15:15:40 2023, 车 A 开始通行
当前时间:Wed Jun 28 15:15:40 2023, 车 B 开始通行
当前时间:Wed Jun 28 15:15:40 2023, 车 E 开始通行
当前时间:Wed Jun 28 15:15:40 2023, 车 D 开始通行
当前时间:Wed Jun 28 15:15:40 2023, 车 C 开始通行

可见所有 car 线程启动后,并没有完全执行,而是在 eve.wait() 这里阻塞住了,这个时候事件锁的标志为 False,当5秒后,事件锁执行 eve.set() 将标志设置为 Ture 之后,所有的 car 线程才开始又继续执行。

2.5 信号量锁Semaphore

(1)基本介绍和方法

信号量锁也是根据条件锁来做的,它内部维护了一个计数器,启动线程计数器减1,计数器为 0 的时候线程就会被阻塞,然后线程结束计数器再加1。

它与条件锁和事件锁的区别如下:

  • 条件锁:一次可以放行任意个处于“等待”状态的线程

  • 事件锁:一次可以放行全部的处于“等待”状态的线程

  • 信号量锁:通过规定,成批的放行特定个处于“上锁”状态的线程

# 生成一个信号量锁对象
sem = threading.Semaphore()


# 上锁,内置计数器减一
sem.acquire()


# 解锁,内置计数器加一
sem.release()

(2)案例

假设有11个人需要上厕所,但是厕所里面只有三个坑位,所以一次只能有三个线程并发

import threading
import time


def toilet(sem, name):
    sem.acquire()
    time.sleep(3)
    print(f"当前时间: {time.ctime()}, person_{name} 上厕所完毕!", )
    sem.release()


if __name__ == "__main__":
    sem = threading.Semaphore(3)   # 最多同时运行3个线程
    for i in range(11):
        t = threading.Thread(target=toilet, args=(sem, str(i)))
        t.start()

运行结果如下:


当前时间: Wed Jun 28 15:23:36 2023, person_0 上厕所完毕!
当前时间: Wed Jun 28 15:23:36 2023, person_1 上厕所完毕!
当前时间: Wed Jun 28 15:23:36 2023, person_2 上厕所完毕!
当前时间: Wed Jun 28 15:23:39 2023, person_4 上厕所完毕!
当前时间: Wed Jun 28 15:23:39 2023, person_5 上厕所完毕!
当前时间: Wed Jun 28 15:23:39 2023, person_3 上厕所完毕!
当前时间: Wed Jun 28 15:23:42 2023, person_6 上厕所完毕!
当前时间: Wed Jun 28 15:23:42 2023, person_7 上厕所完毕!
当前时间: Wed Jun 28 15:23:42 2023, person_8 上厕所完毕!
当前时间: Wed Jun 28 15:23:45 2023, person_9 上厕所完毕!
当前时间: Wed Jun 28 15:23:45 2023, person_10 上厕所完毕!

从输出控制台可以看到,同一时间只有3个线程运行,隔3秒之后,再次启动三个线程,直到11个人全部上完厕所为止。

(3)with语句

Semaphore 里面也实现了 __enter__() 与 __exit__() 两个魔法方法,所以也是可以通过 with 语句进行加锁解锁操作,下面的代码与上面的实现效果一样

def toilet(sem, name):
    with sem:
        time.sleep(3)
        print(f"当前时间: {time.ctime()}, person_{name} 上厕所完毕!", )

3、死锁现象

一般来说,形成死锁有两种形式

① 一个线程里面嵌套获取一把互斥锁。

② 多个线程不按顺序同时获取多个锁,例如:线程1嵌套获取ab两个锁,线程2嵌套获取ba两个锁,当线程1获取到了a在等待b,而线程2获取到了b在等待a,两个线程就形成了一个循环等待的圆,从而形成死锁。

对于第一种情况,可以使用可重入锁。

对于第二情况,其实和 mysql 中的死锁性质差不多,因争夺资源而造成的一种死循环,保证多个线程获取锁的顺序是同样的即可避免这种情况。

对于同步锁来说,一次acquire()必须对应一次release(),不能出现连续重复使用多次acquire()后再重复使用多次release()的操作,这样会引起死锁造成程序的阻塞。

4、总结

threading 模块中的5种锁

① 互斥锁:Lock,一次只能放行一个,可以通过 with 语句调用。

② 可重入锁:RLock,一次只能放行一个,可以通过 with 语句调用。

③ 条件锁:Condition,一次可以放行任意个,可以通过 with 语句调用。

④ 事件锁:Event,一次全部放行,不能通过 with 语句调用。

⑤ 信号量锁:semaphore,一次可以放行特定个,可以通过 with 语句调用。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

python 线程锁 的相关文章

随机推荐

  • 8 月份 火火火火 的 GitHub 开源项目

    本期推荐开源项目目录 1 一个清新文艺的微社区 2 虚拟桌宠模拟器 3 Docusign开源替代方案 4 单词肌肉记忆锻炼软件 5 中文对话式大语言模型 01 一个清新文艺的微社区 一个美观清新的微社区开源项目 整套系统使用 Go Zinc
  • OpenCV实战(29)——视频对象追踪

    OpenCV实战 29 视频对象追踪 0 前言 1 追踪视频中的对象 2 中值流追踪器算法原理 3 完整代码 小结 系列链接 0 前言 我们已经学习了如何跟踪图像序列中点和像素的运动 但在多数应用中 通常要求追踪视频中的特定移动对象 首先确
  • Qt实现阴影边框,可拖动,可缩放窗口(二)

    通过paintEvent来实现的 绘制方形的阴影没问题 但是绘制圆角阴影的话 发现圆角不够圆润 而且有断裂的感觉 pragma once include
  • ChatGPT引爆变革:首个被颠覆的行业揭秘!

    随着人工智能的飞速发展 自然语言处理技术逐渐渗透到内容创作领域 作为一种先进的对话型AI系统 ChatGPT正改变着传统的写作方式 本文将探讨ChatGPT如何颠覆内容创作行业 以及其中的一些引人入胜的案例 ChatGPT是基于GPT架构的
  • 蓝牙App设计2:使用Android Studio制作一个蓝牙软件(包含:代码实现等)

    前言 蓝牙聊天App设计全部有三篇文章 一 UI界面设计 二 蓝牙搜索配对连接实现 三 蓝牙连接聊天 这篇文章是 二 蓝牙搜索配对连接实现 课程1 Android Studio小白安装教程 以及第一个Android项目案例 Hello Wo
  • 习题8-8 判断回文字符串 (20分)

    本题要求编写函数 判断给定的一串字符是否为 回文 所谓 回文 是指顺读和倒读都一样的字符串 如 XYZYX 和 xyzzyx 都是回文 函数接口定义 bool palindrome char s 函数palindrome判断输入字符串cha
  • 白月黑羽教python_白月黑羽Python在线教程

    推荐白月黑羽Python在线教程 白月黑羽 站在初学者的角度为大家安排了Python学习教程 帮助大家迅速掌握程序开发技能 http www python3 vip doc tutorial python home 变量 和 注释 本文目录
  • yaml 学习笔记

    yaml学习 配置 Yet Another Markup Language 发音 j m l 1 yaml文件规则 1 区分大小写 2 使用缩进表示层级关系 3 使用空格键缩进 而非Tab键缩进 4 缩进的空格数目不固定 只需要相同层级的元
  • Spring Cache @Cacheable @CachePut @CacheEvict 讲解和使用案例

    原文链接 https blog csdn net lpw cn article details 84642647 Spring的缓存管理可谓是相当的方便 与其他功能的风格一致 同样的在需要管理的地方添加一个注解 可以是方法 也可以是类 使用
  • fortify 漏洞扫描的几种解决方式

    1 关于Log的问题 Log Forging 整个系统中 对于Log的问题最多 可以采用以下方式进行解决 解决方案如下 1 只输出必要的日志 功能上线前屏蔽大多数的调试日志 2 过滤掉非法字符 2 关于创建File Path Manipul
  • 解构与迭代器

    开始 let x y x 1 y 2 正常的写法 let x y x 1 y 2 console log x y 1 2 let x y 1 2 console log x y 1 2 加上iterator方法 如果我们希望这段代码不报错
  • 计算机二级中的9种运算问题:笛卡尔积,自然连接,交,并,选择,投影。。。

    这九种运算分为7种二元运算 2种一元运算 用文字和例子来分别解释上面几个概念 7种二元运算 1 笛卡儿积 已知 如果算X1和X2的笛卡尔积 则 首先将属性 或者叫标题 A B C 和 D E 和在一块形成新的一组标题
  • python安装bert模型_BERT模型的简单使用(Python)

    不涉及原理 只是快速开始使用 更详细的请参考官方文档 https bert as service readthedocs io en latest index html 下载预训练模型 下载可能需要一些时间 等待的时候可以先去下一步把pyt
  • 某度翻译最新版Acs-Token逆向分析

    某度翻译最新版Acs Token逆向分析 免责声明 本文章中所有内容仅供学习交流使用 不用于其他任何目的 不提供完整代码 抓包内容 敏感网址 数据接口等均已做脱敏处理 严禁用于商业用途和非法用途 否则由此产生的一切后果均与作者无关 擅自使用
  • MySQL题库(4)

    301 考虑 Web 项目的安全性 MD5 算法的不可逆性可以保证加密数据的绝对安全 判断 题 A 正确 B 错误 正确答案 B 302 符合范式的数据表设计比反范式的数据表查询性能更高 判断题 A 正确 B 错误 正确答案 B 303 相
  • 关于文件描述符的close-on-exec标志位

    引言 我们在使用一些系统调用对文件描述符进行操作时 常常会碰到是否为文件描述符赋予CLOEXEC属性的情况 例如 open函数中的flags参数可指定O CLOEXEC标志 int open const char pathname int
  • 基于TF-IDF算法个人文件管理系统——机器学习+人工智能+神经网络(附Python工程全部源码)

    目录 前言 总体设计 系统整体结构图 系统流程图 运行环境 模块实现 1 数据预处理 2 词频计算与数据处理 3 数据计算与对比验证 系统测试 工程源代码下载 其它资料下载 前言 本项目旨在通过应用TF IDF算法 将新下载的课件进行自动分
  • SQL中使用IN关键字时,因空数组替换参数导致了SQL语法错误

    场景 在近期的工作中 发现了这么一个问题 在某一搜索页面 需要同时查询多种订单类型 但在页面中未选择任何订单类型 API通过UI端传来的空订单类型数组进行查询时 抛出了SQL的语法错误 原SQL SELECT FROM dbo order
  • go get 下载包时提示 could not read Username

    问题 在下载公司内部的包时 出现如下提示 go get module example com somepkg common git ls remote q origin in somepath xxxxxxxxxxxxxxxxxxxxxxx
  • python 线程锁

    目录 1 线程安全介绍 2 threading5种常见锁 2 1 同步锁 互斥锁 Lock 1 基本介绍及相关方法 2 给案例加lock锁 3 with语句 2 2 递归锁Rlock 1 基本介绍及相关方法 2 给案例加递归锁 3 with