概念
threading.Lock
同步锁,可以用于保证多个线程对共享数据的独占访问。
当一个线程获取了锁之后,其他线程在此期间将不能再次获取该锁,直到该线程释放锁。这样就可以保证共享数据的独占访问,从而避免数据不一致的问题;但是错误的使用Lock也会发生死锁等待的情况;
无锁
当使用多线程访问并修改公共资源时,若不加锁则会导致数据与预期结果不同;
import threading
# 创建一个Lock对象
lock = threading.Lock()
# 共享资源
counter = 0
# 线程函数
def worker():
global counter
# 获取锁
# 对共享资源进行操作
for i in range(1000000):
counter += 1
print("Counter value: ", counter)
# 创建两个线程并启动它们
t1 = threading.Thread(target=worker)
t2 = threading.Thread(target=worker)
t1.start()
t2.start()
t1.join()
t2.join()
######################## 结果 ############################
Counter value: 1264715
Counter value: 1326630
Process finished with exit code 0
############################################################
究其原因,我们可以假设:
- 当线程t1拿到counter=100时,还没来得及+1;
- 此时线程t2也拿到了counter=100。
- 现在当t1和t2对counter进行加1后,counter值都变成了101并返回给全局变量。
- 我们发现,两个线程在同时使用和修改同一全局变量counter时,总数少加了一个1。
- 因为一个线程在对公共资源做读写的时候,其它线程也能对它进行读写,两个线程拿到了一样的值,这就导致了最后产生的结果数据不一致。
为了防止这个问题,我们对公共资源进行使用时,一定要进行加锁保护;
加锁
import threading
# 创建一个Lock对象
lock = threading.Lock()
# 共享资源
counter = 0
# 线程函数
def worker():
global counter
# 获取锁
lock.acquire()
try:
# 对共享资源进行操作
for i in range(1000000):
counter += 1
print("Counter value: ", counter)
finally:
# 释放锁
lock.release()
# 创建两个线程并启动它们
t1 = threading.Thread(target=worker)
t2 = threading.Thread(target=worker)
t1.start()
t2.start()
t1.join()
t2.join()
######################## 结果 #############################
Counter value: 1000000
Counter value: 2000000
Process finished with exit code 0
############################################################
因为加了锁,两个线程成功地对计数器分别进行了递增操作。
究其原因,我们可以假设:
- 当线程t1先获取到了锁lock,并对数据处理部分进行了加锁,假设t1拿到counter=1;
- 此时线程t2也想拿counter,但是因为获取不到锁lock,就只能等待lock被释放。
- 当线程t1完全执行完递增操作后,将锁lock给释放掉了;
- 此时t2突然发现它能获取到锁lock了,于是就对t2的数据处理部分进行了加锁,然后执行数据递增逻辑。
- 直到t2的完全执行完递增操作后,将锁lock释放;
- 我们发现,两个线程由于锁的作用,只能顺序性的获取需要的公共资源counter,保证了线程流程执行中公共资源使用的独占性。
因为加锁而且没有出现竞态,所以结果是准确且符合预期;
但同时,在多线程中错误的加锁顺序可能会导致锁等待,也就是俗称的“死锁"现象。
死锁
死锁指的是在多线程或分布式系统中,两个或多个线程或进程因互相等待对方释放资源而陷入无限等待的状态,无法继续执行的情况。
当多个线程或进程互相竞争同一组资源时,如果每个线程都持有一些资源,并且都在等待另一个线程释放它所需要的资源时,就会发生死锁。这种情况下,所有的线程都被阻塞,无法继续执行,从而导致系统出现僵死状态。
import threading
# 创建两个Lock对象
lock1 = threading.Lock()
lock2 = threading.Lock()
# 线程函数1
def worker1():
lock1.acquire()
print("Worker 1 acquired lock 1")
lock2.acquire()
print("Worker 1 acquired lock 2")
lock2.release()
print("Worker 1 released lock 2")
lock1.release()
print("Worker 1 released lock 1")
# 线程函数2
def worker2():
lock2.acquire()
print("Worker 2 acquired lock 2")
lock1.acquire()
print("Worker 2 acquired lock 1")
lock1.release()
print("Worker 2 released lock 1")
lock2.release()
print("Worker 2 released lock 2")
# 创建两个线程并启动它们
t1 = threading.Thread(target=worker1)
t2 = threading.Thread(target=worker2)
t1.start()
t2.start()
t1.join()
t2.join()
######################## 结果 ############################
Worker 1 acquired lock 1
Worker 2 acquired lock 2
...无尽的等待
############################################################
在上面的代码中,我们创建了两个Lock对象,并在两个线程中使用了不同的顺序来获取这两个锁。
线程1首先获取了lock1,然后尝试获取lock2;而线程2则首先获取了lock2,然后尝试获取lock1。
由于两个线程获取锁的顺序不同,它们【可能】会在某个时刻同时持有一个锁,但尝试获取另一个锁时会被阻塞。
例如,当线程1获取了lock1,而线程2获取了lock2时,它们都会等待另一个锁的释放,从而导致死锁。
这时候程序将不再继续执行,而是一直处于等待状态。
这里如果想要要避免死锁,就需要确保不同线程获取锁的顺序是一致的。
也就是说,如果一个线程首先获取了lock1,那么它在尝试获取lock2时也应该按照相同的顺序来获取,而不是反过来。
解决死锁
死锁的产生原因通常是由于多个线程对共享资源的竞争,同时又没有良好的资源分配策略或锁的获取顺序导致的。在设计多线程或分布式系统时,避免死锁是一个重要的问题。常用的避免死锁的方法包括:
- 加锁顺序的规范化
- 资源分配策略的优化
- 使用超时等待等机制。
🎉如果对你有所帮助,可以点赞、关注、收藏起来,不然下次就找不到了🎉
【点赞】⭐️⭐️⭐️⭐️⭐️
【关注】⭐️⭐️⭐️⭐️⭐️
【收藏】⭐️⭐️⭐️⭐️⭐️
Thanks for watching.
–Kenny