文章目录
- 1. 什么是线程锁
- 1.1 互斥锁(threading.Lock)
- 1.2 递归锁/重入锁(threading.RLock)
- 2. 为什么要使用线程锁
- 3. 应用场景
- 4. 代码实现
- 4.1 对比阻塞锁和非阻塞锁
- 4.2 什么是死锁?
- 如何避免死锁
- 5. 线程锁的缺点
- 6. 参考文献
1. 什么是线程锁
在python官方文档中有两个线程锁的类,一个是class threading.Lock,另一个是class threading.RLock。
1.1 互斥锁(threading.Lock)
threading.Lock也就是原始锁,锁只有两种状态"locked"和"unlocked"。当这种锁在创建后是unlocked。
常用的方法有lock.acquire()和lock.release(),这两个方法是用来改变锁的状态的,前者是unlocked变为locked,后者正相反。
一旦一个线程获得一个锁,会阻塞随后尝试获得锁的其它线程,直到该锁被释放;当然,任何线程都可以释放它。
"""
threading.Lock常用方法介绍
“”“
acquire(blocking=True, timeout=-1)
可以阻塞或非阻塞地获得锁。
当调用时参数 blocking 设置为 True (缺省值),阻塞直到锁被释放,然后将锁锁定并返回 True 。
在参数 blocking 被设置为 False 的情况下调用,将不会发生阻塞。如果调用时 blocking 设为 True 会阻塞,并立即返回 False ;否则,将锁锁定并返回 True。
当浮点型 timeout 参数被设置为正值调用时,只要无法获得锁,将最多阻塞 timeout 设定的秒数。timeout 参数被设置为 -1 时将无限等待。当 blocking 为 False 时,timeout 指定的值将被忽略。
如果成功获得锁,则返回 True,否则返回 False (例如发生 超时 的时候)。
release()
释放一个锁。这个方法可以在任何线程中调用,不单指获得锁的线程。
当锁被锁定,将它重置为未锁定,并返回。如果其他线程正在等待这个锁解锁而被阻塞,只允许其中一个允许。
在未锁定的锁调用时,会引发 RuntimeError 异常。
没有返回值。
简单的例子
import threading
创建一个锁
lock = threading.Lock()
# 上锁,可以指定阻塞[timeout]秒,超过这个时间将不再阻塞其它线程。
lock.acquire([timeout])
# 解锁
lock.release()
1.2 递归锁/重入锁(threading.RLock)
重入锁是一个可以被同一个线程多次获取的同步基元组件。在内部,它在基元锁的锁定/非锁定状态上附加了 “所属线程” 和 “递归等级” 的概念。在锁定状态下,某些线程拥有锁 ; 在非锁定状态下, 没有线程拥有它。
class threading.RLock
此类实现了重入锁对象。重入锁必须由获取它的线程释放。一旦线程获得了重入锁,同一个线程再次获取它将不阻塞;线程必须在每次获取它时释放一次。
acquire()/release() 对可以嵌套;只有最终 release() (最外面一对的 release() ) 将锁解开,才能让其他线程继续处理 acquire() 阻塞。
"""
threading.RLock常用方法介绍
“”“
acquire(blocking=True, timeout=-1)
可以阻塞或非阻塞地获得锁。
当无参数调用时: 如果这个线程已经拥有锁,递归级别增加一,并立即返回。否则,如果其他线程拥有该锁,则阻塞至该锁解锁。一旦锁被解锁(不属于任何线程),则抢夺所有权,设置递归等级为一,并返回。如果多个线程被阻塞,等待锁被解锁,一次只有一个线程能抢到锁的所有权。在这种情况下,没有返回值。
当调用时参数 blocking 设置为 True ,和没带参数调用一样做同样的事,然后返回 True 。
当 blocking 参数设置为 False 的情况下调用,不进行阻塞。如果一个无参数的调用已经阻塞,立即返回false;否则,执行和无参数调用一样的操作,并返回True。
当浮点数 timeout 参数被设置为正值调用时,只要无法获得锁,将最多阻塞 timeout 设定的秒数。 如果锁被获取返回 True,如果超时返回False。
release()
释放锁,自减递归等级。如果减到零,则将锁重置为非锁定状态(不被任何线程拥有),并且,如果其他线程正被阻塞着等待锁被解锁,则仅允许其中一个线程继续。如果自减后,递归等级仍然不是零,则锁保持锁定,仍由调用线程拥有。
只有当前线程拥有锁才能调用这个方法。如果锁被释放后调用这个方法,会引起 RuntimeError 异常。
没有返回值。
2. 为什么要使用线程锁
相信大家都知道,在python中多线程是共享全局变量的,多进程是各自都拥有一份属于自己的所有变量。既然多个线程使用同样的一些全局变量,会导致全局变量的不同步。例如:(python版)创建两个线程,其中一个输出1-52,另外一个输出A-Z。输出格式要求:12A 34B 56C 78D 依次类推,该面试题要求使用两个线程,但是要我们控制两个线程的输出是有序的(因为大家都知道,多线程的执行是无序的,咱也不知道输出出来是个什么鬼样子)。所以,当遇到这些情况的时候,需要我们控制线程的访问顺序。
如果不使用线程锁就可能会造成严重的错误。例如,
当我们取钱的时候,你自己去ATM机上取500块钱,同时二弟通过支付宝在同一张银行卡也取500块钱,三弟通过微信也在这张银行卡上取500块钱。如果没有线程锁就可能会出现,银行同时给你们三人各500块钱,然而你的银行卡上只扣了500块钱。
3. 应用场景
I/O密集型操作 需要资源保持同步。因为python的多线程是假的多线程,也就是说,如果我的机器有16个cpu核,但是通过这一个脚本来运行的时候,只会启动一个进程,不管在这个进程中你开了多少了个线程,始终只会使用这16个核中的1个核,因此,造成了在cpu计算密集型时,使用多线程效率低下。
锁的应用场景:
独占锁: 锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候。
共享锁: 如果共享资源是不可变的值时,所有线程每一次读取它都是同一样的值,这样的情况就不需要锁。
使用锁的注意事项:
少用锁,必要时用锁。使用了锁,多线程访问被锁的资源时,就变成了串行,要么排队执行,要么争抢执行。
加锁时间越短越好,不需要就立即释放锁。
不使用锁时,有了效率,但是结果是错的。
使用了锁,变成了串行,效率地下,但是结果是对的。
4. 代码实现
4.1 对比阻塞锁和非阻塞锁
阻塞锁和非阻塞锁的区别就在于:
阻塞锁:
当这个锁处于locked状态时,其余线程都会阻塞。此时程序无法执行其余线程的代码
非阻塞锁:
当这个锁处于locked状态时,其余线程不会阻塞。此时其余线程的代码仍然能够执行。
什么意思呢?就是说,**多个线程可以同时使用同一个锁,而不会被阻塞!!**例如,使用同一个Lock锁对象时,第二个线程仍可以使用锁,且第一个锁不会被阻塞。
# coding=utf-8
import threading
logic = """
测试线程锁中的acquire()方法中,采用阻塞和不阻塞的区别
"""
def blocking(lockk, i):
try:
lockk.acquire(blocking=True)
print(i)
lockk.release()
except Exception as e:
print(e)
def unblocking(lockk, i):
try:
lockk.acquire(blocking=False)
print(i)
lockk.release()
except Exception as e:
print(e)
if __name__ == '__main__':
lock = threading.Lock()
for i in range(20):
t = threading.Thread(target=blocking, args=(lock, i))
t.start()
for i in range(20):
t = threading.Thread(target=unblocking, args=(lock, i))
t.start()
两次的输出如图所示
当时使用阻塞的线程锁时,创建的这20个线程是按照指定的顺序输出。
但是当使用非阻塞的线程锁时,这20个线程的输出并不是按照我们期望的顺序输出,而且其中还出现了异常"release unlocked lock"。意思就是,我们释放了没有上锁的锁。why?
这是因为多个线程同时使用了一个锁,也就是锁线程A上了锁之后,线程B也来上锁,当然这两次调用acquire()方法的返回值是不一样的,因为同一时间只有一个线程能够上锁成功。在释放锁的时候也是,多个线程同时释放一个锁,只有一个能够释放成功,那么这一时刻其它释放该锁的线程都会出现异常!!!
4.2 什么是死锁?
一般来说加锁以后还要有一些功能实现,在释放之前还有可能抛异常,一旦抛出异常,锁是无法释放,但是当前线程可能因为这个异常被终止了,这就产生了死锁。
如何避免死锁
1、使用 try…except…finally 语句处理异常、保证锁的释放
2、with 语句上下文管理,锁对象支持上下文管理。只要实现了__enter__和__exit__魔术方法的对象都支持上下文管理。
只要防止出现异常导致程序终止库可以了
with some_lock:
# do something...
相当于:
some_lock.acquire()
try:
# do something...
except Exception as e:
# do something
finally:
some_lock.release()
5. 线程锁的缺点
虽然线程锁解决了上面提到了,同步全局变量的问题,但是因为加锁之后,同一时间只能有一个线程操作这个全局变量,这就阻塞了其余的线程,在时间效率上肯定是减慢了不少,这也是么有办法的事情。