暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

python并发变成之-线程同步-互斥锁(Lock)、死锁和可重入锁(Rlock)

小儿来一壶枸杞酒泡茶 2021-05-12
574

线程安全

线程安全:多线程竞争同一个资源保证数据一致性和完整性的一个过程,通常一般是采取加锁机制,进行相关数据临界点的互斥

线程安全一般是:多线程环境下对一些【全局变量及静态变量】引起的同时具有【写操作权限】的时候会出现数据不一致或数据污染的情况,此时会了避免此类情况的发生,一般采取就是加锁的机制。

如果一般的单纯只是具体【读】操作的权限的,通常这个这个【全局变量及静态变量】是线程安全的。

所以线程不安全一般是强调多线程下对【全局变量及静态变量】等资源的写操作权限的情况下引发问题异常。

真正会引导数据冲突的,其实不是读操作,而是写操作。(所以通常再分析代码的时候,可以看是否存在写操作,且写的操作是否会被拆分成多个字节码进行一个资源的写操作)

线程不安全的示例:

import threading, time

number = 0

def my_run(msg):
    print("子线程的名称:", threading.current_thread().getName())
    print("开始任务", msg)
    global number
    for _ in range(1000000):
        number += 1
    print("任务结束,运行处理结果:", number)


if __name__ == '__main__':
    start_time = time.time()

    listls = []
    for i in range(3):
          t = threading.Thread(name='线程%s'%(i), target=my_run, args=("我是你大爷!",))
        listls.append(t)
        t.start()

    [ls.join() for ls in listls]

    print('运行总耗时:', time.time() - start_time)


输出结果:

子线程的名称: 线程0
开始任务 我是你大爷!
子线程的名称: 线程1
任务结束,运行处理结果:开始任务子线程的名称:  运行总耗时:1000000
 0.06382918357849121
 线程2我是你大爷!

开始任务 我是你大爷!
任务结束,运行处理结果:2000000
任务结束,运行处理结果:2441493

区别示例

    for i in range(3):
        t = threading.Thread(name='线程%s'%(i),  target=my_run, args=("我是你大爷!",))
        # listls.append(t)
        t.start()
        t.join()

如果是放在 启动时候,就join()的话,会按一定的顺序输出:

子线程的名称: 线程0
开始任务 我是你大爷!
任务结束,运行处理结果:1000000
子线程的名称: 线程1
开始任务 我是你大爷!
任务结束,运行处理结果:2000000
子线程的名称: 线程2
开始任务 我是你大爷!
任务结束,运行处理结果:3000000


PS:join()顺序位置很关键。前后不一样!

之前我们有了解过,数据出现不一致主要是因为我们的【多线程同时执行写操作】的时候,会出现,那么我可以再多线程操作处理的时候,只允许一个线程进行写操作的方式,创建一个标识来进行处理,另一个循环等待标志变化。

但是这个方式,处理起来相对的麻烦!也不推荐!仅参考了解!

说明:上述的示例中主要是通过一个线程来管理我们的标识的方式来调度线程!确保有且只有一个线程对我们的一个数据有写操作的权限的进行保障数据的安全的!

另一种让数据安全:一种方式是使用上面的join谁先来就先join的方式,

还有另一种方式就是加互斥锁:

线程互斥锁保证数据安全

基于from threading import Thread,Lock,threading自带的锁对象,这种情况下,牺牲了并发执行效率,但是保证了数据安全。

注意点加锁的时候超时:mutex.acquire(timeout=1)

import threading, time

number = 0
mutex = threading.Lock()


def my_run(msg):
    print("子线程的名称:", threading.current_thread().getName())
    print("开始任务", msg)
    global number
    # 加锁
    mutex.acquire()
    for _ in range(1000000):
        number += 1
    # 解锁
    mutex.release()

    print("任务结束,运行处理结果:", number)


if __name__ == '__main__':
    start_time = time.time()

    listls = []
    for i in range(3):
        t = threading.Thread(name='线程%s' % (i), target=my_run, args=("我是你大爷!",))
        # listls.append(t)
        t.start()

    [ls.join() for ls in listls]

    print('运行总耗时:', time.time() - start_time)


PS:对于互斥锁的范围的理解,对于资源的上锁应该遵循【最小原则】,也就是说,我锁的资源,仅仅是针对多线程都要竞争去抢的那个资源,而除非有必要,我们不应该把锁的范围限定在整个的函数或(整个代码段内)。

我们应该尽量的避免在持有锁的情况下调用外部的方法,尽量将锁的范围缩小,将同步代码块仅限定于需要保护那些资源内。

线程锁场景-死锁:

默认的lock不能识别lock当前被哪个线程持有。如果任何线程正在访问共享资源,那么试图访问共享资源的其他线程将被阻塞,即使锁定共享资源的线程也是如此。

什么是死锁?

多个线程场景下,线程在执行过程中,线程之间都互相等待对方释放对方手里锁,死锁是一种因争夺资源而造成的一种互相等待的,导致程序无法执行现象。(多锁)

还以一种就是:当两个或多个线程试图访问相同的资源时,有效地阻止了彼此访问该资源,导致程序无法执行现象,这就是所谓的死锁。(单锁)

死锁示例现象:

多个锁的情况下:

import time
from threading import Thread,Lock

def action_a(lock_a,lock_b):
    # 使用A锁开始 --获取锁
    print('action_a任务---使用lock_a进行上锁操作')
    lock_a.acquire()
    time.sleep(0.5)
    print('action_a任务---目前--拿到了A锁')

    print('-----》'*10)
    print("action_a任务---开想获取使用lock_b的锁使用权----进行上锁操作")
    print("lock_b 已经TM 被action_b任务 给抢了!!!别想了xxxxxxxx!!!")
    lock_b.acquire()
    print('action_a任务-----拿到了B锁')

    # 释放B的的锁
    lock_b.release()
    # 释放a的的锁
    lock_a.release()

def action_b(lock_a,lock_b):
    print('action_b任务---使用lock_b进行上锁操作')
    # 使用b锁开始 --获取锁
    lock_b.acquire()

    print('action_b任务:-------拿到了B锁')
    print("action_b任务想获取lock_a的锁的使用权----进行上锁操作")
    print("lock_a的锁 已经TM 被action_a任务 给抢了!!!别想了-----!!!")
    lock_a.acquire()
    print('action_b任务拿到了lock_a的锁的使用权限')

    # 释放锁
    lock_a.release()
    lock_b.release()

if __name__ == '__main__':
    # 创建两个锁
    lock_a = Lock()
    lock_b = Lock()
    # 启动任务执行
    t1 = Thread(target=action_a,args=(lock_a,lock_b))
    t2 = Thread(target=action_b,args=(lock_a,lock_b))
    t1.start()
    t2.start()

输出结果:

action_a任务---使用lock_a进行上锁操作
action_b任务---使用lock_b进行上锁操作
action_b任务:-------拿到了B锁
action_b任务想获取lock_a的锁的使用权----进行上锁操作
lock_a的锁 已经TM 被action_a任务 给抢了!!!别想了-----!!!
action_a任务---目前--拿到了A锁
-----》-----》-----》-----》-----》-----》-----》-----》-----》-----》
action_a任务---开想获取使用lock_b的锁使用权----进行上锁操作
lock_b 已经TM 被action_b任务 给抢了!!!别想了xxxxxxxx!!!
·······
·······
········
然后下面的就没信息出现了·······················
一直等各自释放锁~~~~你不让我不放的情况下就一直永远等下去了~~~~


单个锁的情况下:

import threading

# 创建一个lock对象
lock = threading.Lock()

# 初始化共享资源
number = 0

# 本线程访问共享资源
print("主线程开始上锁")
lock.acquire()
number = number + 1
print("执行完成:",number)
# lock.release()
print("主线程开始继续要上锁---这个地方会死锁啦!!!都没有被释放!除非你先调用lock.release()")
# 这个线程访问共享资源会被阻塞
lock.acquire()
number = number + 2
lock.release()

print("释放锁!!!")

print(number)

输出结果:

主线程开始上锁
执行完成:1
主线程开始继续要上锁---这个地方会死锁啦!!!都没有被释放!除非你先调用lock.release()

死锁的破解之法:Rlock可重入锁(递归锁RLoc)

PS:多线程环境下的多层锁的场景的使用,必须要用递归锁

可重入锁(或RLock)用于竞争资源的线程的出现的阻塞状态,如果多线程的共享资源在RLock中,那么可以安全地再次多次的进行acquire锁。

RLocked资源可以被不同的线程重复访问,即使它在被不同的线程调用时仍然可以正常工作。

PS:在同一线程中可用被多次acquire。如果使用RLock,那么acquire和release必须成对出现,调用了n次acquire锁请求,则必须调用n次的release才能在线程中释放锁对象.

PS:RLock的优势在于,在同一个线程里可以多次申请锁,而Lock则不能,必须在释放之后才能再次申请

#!/usr/bin/evn python
# -*- coding: utf-8 -*-
import time
from threading import Thread,RLock

def action_a(lock_a,lock_b):
    # 使用A锁开始 --获取锁
    print('action_a任务---使用lock_a进行上锁操作')
    lock_a.acquire()
    time.sleep(0.5)
    print('action_a任务---目前--拿到了A锁')

    print('-----》'*10)
    print("action_a任务---开想获取使用lock_b的锁使用权----进行上锁操作")
    print("lock_b 已经TM 被action_b任务 给抢了!!!等着吧xxxxxxxx!!!")
    lock_b.acquire()
    print('action_a任务-----拿到了B锁')

    # 释放B的的锁
    lock_b.release()
    # 释放a的的锁
    lock_a.release()

def action_b(lock_a,lock_b):
    print('action_b任务---使用lock_b进行上锁操作')
    # 使用b锁开始 --获取锁
    lock_b.acquire()

    print('action_b任务:-------拿到了B锁')

    print('>>>>>>》' * 10)
    print("action_b任务想获取lock_a的锁的使用权----进行上锁操作")
    print("lock_a的锁 已经TM 被action_a任务 给抢了!!!等着吧-----!!!")
    lock_a.acquire()
    print('action_b任务拿到了lock_a的锁的使用权限')

    # 释放锁
    lock_a.release()
    lock_b.release()

if __name__ == '__main__':
    # 创建同一个锁
    lock_a = lock_b= RLock()
    #
    # 启动任务执行
    t1 = Thread(target=action_a,args=(lock_a,lock_b))
    t2 = Thread(target=action_b,args=(lock_a,lock_b))
    t1.start()
    t2.start()

输出结果:

action_a任务---使用lock_a进行上锁操作
action_b任务---使用lock_b进行上锁操作
action_a任务---目前--拿到了A锁
-----》-----》-----》-----》-----》-----》-----》-----》-----》-----》
action_a任务---开想获取使用lock_b的锁使用权----进行上锁操作
lock_b 已经TM 被action_b任务 给抢了!!!等着吧xxxxxxxx!!!
action_a任务-----拿到了B锁
action_b任务:-------拿到了B锁
>>>>>>》>>>>>>》>>>>>>》>>>>>>》>>>>>>》>>>>>>》>>>>>>》>>>>>>》>>>>>>》>>>>>>》
action_b任务想获取lock_a的锁的使用权----进行上锁操作
lock_a的锁 已经TM 被action_a任务 给抢了!!!等着吧-----!!!
action_b任务拿到了lock_a的锁的使用权限

流程说明:

互斥锁和可重入锁(跌归锁)对比:

对比维度lockrlock
所属权lock对象不可以被任何线程拥有rlock对象可以被多个线程拥有
锁获取lock对象acquire后无法再被其他线程获取,除非持有线程进行release释放rlock对象可以被其他线程多次acquire获取到锁
锁释放lock对象可被任何线程释放rlock对象只能被持有的线程acquire释放
锁释放次数任意线程都可以释放acquire和release必须对应比例的出现在持有线程中

个人其他博客地址

简书:https://www.jianshu.com/u/d6960089b087

掘金:https://juejin.cn/user/2963939079225608

小钟同学 | 文 【原创】| QQ:308711822

  • 1:本文相关描述主要是个人的认知和见解,如有不当之处,还望各位大佬指正。
  • 2:关于文章内容,有部分内容参考自互联网整理,如有链接会声明标注;如没有及时标注备注的链接的,如有侵权请联系,我会立即删除处理哟。


文章转载自小儿来一壶枸杞酒泡茶,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论