Python 多线程编程-02-threading 模块-锁的使用

目  录

1. 线程同步

1.1 线程同步概念

1.2 线程同步实现

1.3 死锁

1.3.1 死锁的产生

1.3.2 死锁的必要条件

1.3.3 死锁的解决

2. threading 模块中线程锁

2.1 threading.Lock

2.1.1 threading.Lock 属性和方法

2.1.2 threading.Lock 使用示范

 2.2 threading.RLock 可重入锁

2.2.1 threading.RLock 属性和方法

 2.2.2 threading.RLock 使用示范


                                          Python 多线程编程目录

​​​​​​​​​​​​Python 多线程编程-01-threading 模块初识

Python 多线程编程-02-threading 模块-锁的使用

 Python 多线程编程-03-threading 模块 - Condition

Python 多线程编程-04-threading 模块 - Event

Python 多线程编程-05-threading 模块 - Semaphore 和 BoundedSemaphore

Python 多线程编程-06-threading 模块 - Timer

Python 多线程编程-07-threading 模块 - Barrie

1. 线程同步

1.1 线程同步概念

        多线程编程有一个非常重要的方面:同步。此处的同步不是指一起行动,而是协同步调,多个线程按预定的先后次序进行运行。

        常见的有两种情况:

        a)例如某些资源,数据库的某个表格、某个文件,不希望(也不应该)被多个线程同时执行,这样就会产生竞争。那么如何协调这种竞争,可以理解为线程同步。这种内存中的资源可以理解为临界区。

        b)需要若干个线程按照特定的顺序完成一组工作,如线程 a 打开文件,输入文字,线程负责 b 修改颜色和字体大小,线程 c 在此基础上插入图片。那么也需要线程同步。

1.2 线程同步实现

        程序员可以选择合适的同步原语,或者线程控制机制来执行同步。最常见的有:锁/互斥,以及信号量。

        锁是所有机制中最简单、最低级的机制,而信号量多用于多线程竞争有限资源的情况。

        所谓的锁,可以理解为内存中的一个整型数,有两种状态:空闲、上锁。acquire 加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功。如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。

1.3 死锁

1.3.1 死锁的产生

        死锁是指两个或两个以上的进程/线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。死锁的原因包括系统资源不足、进程/线程运行推进顺序不合适、资源分配不当等。

        比如线程 A 占据了资源 n,在等待临界区资源 m;线程 B 此时占据了 m,但是在等待临界区资源 n,A 只有获得了 m 才能释放 n,而 B 只有获得了 n 才能释放 m,所以彼此之间就造成了死锁,造成了资源的极大浪费。

1.3.2 死锁的必要条件

        产生死锁的四个必要条件: 

        (1) 互斥条件:一个资源每次只能被一个进程使用,这是对资源的要求。

        (2) 请求与保持条件:一个进程/线程因请求资源而阻塞时,对已获得的资源保持不放,这是对进程/线程的要求。

        (3) 不剥夺条件:进程/线程已获得的资源,在末使用完之前,不能强行剥夺。

        (4) 循环等待条件:若干进程/线程之间形成一种头尾相接的循环等待资源关系。

        这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立。

1.3.3 死锁的解决

        对于死锁主要有以下三种处理方式:

        (1) 检测与解除策略:检测死锁并且恢复。这是在系统工作中,死锁发生后进行的。

        (2) 避免策略:事先对资源进行动态分配,以避免死锁。这是在死锁发生前进行的,主要通过合理的资源分配实现。

        (3) 预防策略:通过破除死锁四个必要条件之一,来防止死锁产生。例如设置资源可以被剥夺;请求资源的线程请求失败时候不是阻塞,而是转而去做其他事情;诸如此类等。

2. threading 模块中线程锁

2.1 threading.Lock

2.1.1 threading.Lock 属性和方法

        threading.Lock 类的属性和方法,已经废弃不再赘述。

threading.Lock 类的属性和方法
序号属性和方法描述
1

方法:acquire(blocking=True,

timeout=-1)

如果没有参数,且这个锁已经被锁定了,哪怕是被同一个线程锁定,这个线程将会阻塞,需要等待另外一个线程释放获取锁后,返回 True。

如果有参数,只有当参数是 True时候,才会阻塞。

方法的返回值反应释放获取了锁。阻塞操作是可以中断的。

2方法:locked()返回一个布尔值,指示锁的状态
3方法:release()释放锁,允许在等待该锁的阻塞队列中的某个线程获得这个锁。这个锁当前应该是被锁定的状态,但是不能是锁定这个锁的线程再次锁定它。


 

2.1.2 threading.Lock 使用示范

先看一段没有使用 Lock 的多线程代码,在多个线程给同一个列表插入数据并且打印数据时候,结果会看起来比较乱。这是我在 jupter 编辑器里面执行的,看起来还不太明显,如果我在 python idle 里面就会更明显。

import threading
import time

my_list=[]
lock=threading.Lock()

def show_list(n):
    my_list.append(n)
    print(my_list)

def show_list_withLock(n):
    lock.acquire()
    my_list.append(n)
    print(my_list)
    lock.release()

numbers=list(range(10))

for num in numbers:
    t= threading.Thread(target=show_list, args=(num, ))
    t.start()

 在 idle 可以明显看到,打印的数据顺序和插入的数据顺序是不一致的。

 如果使用 threading.Lock 呢,因为每次对列表的操作都必须获得锁,所以就避免了数据紊乱。

 如果我对 show_list_withLock 函数做一些修改,原来 lock.acquire() 默认为 lock.acquire(blocking=True),现在我把它设置为 blocking=False。此时,如果获得锁成功还好,如果没有获得锁,但是这个线程并不会阻塞,大家可以看到,可能会出现 "RuntimeError: release unlocked lock ",即释放了并没有获得的锁。但是,并不是一定会出现这种错误。

import threading
import time

my_list=[]
lock=threading.Lock()

def show_list(n):
    my_list.append(n)
    print(my_list)

def show_list_withLock(n):
    lock.acquire(blocking=False)
    my_list.append(n)
    print(my_list)
    lock.release()

numbers=list(range(10))

for num in numbers:
    t= threading.Thread(target=show_list_withLock, args=(num, ))
    t.start()

 所以大家写代码时候要注意,对于未阻塞的锁,需要进行判断 acquire 时候是否获得 True。

请注意,一个线程获得的锁,可以由其他线程进行释放! 

在上面的代码示意中,每个线程会去 acquire 锁,如果这个锁被其他线程获取了,则 acquire 失败。但是,一个线程获得的锁,可以由其他线程进行释放! 

参看下面代码,在主线程中获取了锁但是没有释放,然后子线程中一直无法获得锁,一直在阻塞状态,这时候就进入了死等待状态。一直等我在其他线程敲下了lock.release(),子线程才得以继续。

import threading
import time

my_list=[0,1,2]
lock=threading.Lock()


def show_list_withLock(n):
    lock.acquire()
    print("show_list_withLock:Now sleep 5 seconds!")
    print("show_list_withLock1",time.ctime())
    time.sleep(5)
    my_list.append(n)
    print(my_list)
    print("show_list_withLock2",time.ctime())
    lock.release()


print("Get the lock and sleep 10 seconds!")
print("MainThread 1",time.ctime())
lock.acquire()
time.sleep(10)
print("MainThread 2",time.ctime())

t= threading.Thread(target=show_list_withLock, args=(3, ))
t.start()

 

 另外 acquire() 函数另一个参数是timeout=-1,表示等待获取锁的时间,-1表示一直等待。如果超过timeout设定的时间还没有获取到锁就会有获取锁失败。如果使用上 timeout为正值,也可以有效地减少等待时间,减少死锁的发生。

 2.2 threading.RLock 可重入锁

2.2.1 threading.RLock 属性和方法

        threading.RLock 和 threading.Lock 的区别在于,同一个线程它可以再次获得已持有的锁(锁递归),而不用阻塞等待。可重入锁只能被所有者释放。使用可重入锁需要注意,有几次acquire,必须对应几次release。
        1、threading.Lock 同一时刻只能被上锁一次,而 threading.RLock 可以被同一线程上 N 次锁
        2、threading.Lock 可以被非所有者释放,而 threading.RLock 只能被所有者释放

threading.RLock 类的属性和方法
序号属性和方法描述
1

方法:acquire

(blocking=True)

锁定锁,返回一个布尔值指示是否锁定。blocking 指示我们是否等待这个锁可用。blocking = False 且另外一个线程占据了这个锁,那么立刻返回 False。blocking = True, 且另外一个线程占据了这个锁,那么则等待这个锁释放,拿到这个锁,然后返回 True。

请注意,阻塞操作是可中断的。在所有其他情况下,该方法将立即返回True。

确切地说,如果当前线程已经持有锁,则其内部计数器简单地递增。如果没有人拿着锁,获取锁,其内部计数器初始化为1。

2方法:release()释放锁,允许在等待该锁的阻塞队列中的某个线程获得这个锁。这个锁当前应该是被锁定的状态,但是必须是锁定这个锁的线程再次锁定它。

 2.2.2 threading.RLock 使用示范

针对 “threading.RLock 可以被同一线程上 N 次锁 ” 的特性,下面的程序不会出现阻塞。

import threading
lock = threading.RLock()
print(lock.acquire())
print(lock.acquire())
lock.release()
lock.release()

但是下面的代码每个线程依次 acquire 和 release 时候,可能就会报错 “

RuntimeError: cannot release un-acquired lock

”。请注意,是可能,而不是肯定,因为这和机器实时运行状态相关。

之所以会报错,是因为可能没有获得可重入锁。

那么使用 RLock 时候,代码设计就要小心。

my_list=[]
lock=threading.RLock()
threads=[]

def show_list_withLock(n):
    lock.acquire()
    print("Current n is ==>",n)
    my_list.append(n)
    print(my_list)
    time.sleep(n)
    print(n,time.ctime())
    lock.release()

numbers=list(range(10))

for num in numbers:
    t= threading.Thread(target=show_list_withLock, args=(num, ))
    threads.append(t)
    
for t in threads:
    t.start()

 

'''

要是大家觉得写得还行,麻烦点个赞或者收藏吧,想个博客涨涨人气,非常感谢!

'''

  • 19
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江南野栀子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值