死锁和递归锁
【1】死锁
def task(i):
mutex_1.acquire()
print(f'{current_thread().name}抢到了第一把锁')
mutex_2.acquire()
print(f'{current_thread().name}抢到了第二把锁')
mutex_2.release() # 释放锁二
mutex_1.release() # 释放锁一
mutex_2.acquire()
print(f'{current_thread().name}抢到了第二把锁')
mutex_1.acquire()
time.sleep(1)
print(f'{current_thread().name}抢到了第一把锁')
mutex_1.release() # 释放锁一
mutex_2.release() # 释放锁二
if __name__ == '__main__':
t_list = [Thread(target=task, args=(i,)) for i in range(1, 8)]
for t in t_list:
t.start()
- 在这个例子中,我添加了两把锁(锁1,锁2),创建多个线程去抢锁
- 仔细看这个例子,会发现这是个死锁
- 线程1拿到锁1
- 其他线程会被堵住,因为线程1没有释放锁1
- 线程1拿到锁2
- 其他线程会被堵住,因为线程1没有释放锁1
- 线程1释放锁2
- 其他线程会被堵住,因为线程1没有释放锁1
- 线程1释放锁1
- 其他线程会争夺锁1
- 线程1拿到锁2
- 拿到锁1的线程不会释放锁1,因为他没有拿到锁2
- 线程1不会拿到锁1,其他程序没有释放锁1
- 程序夯住,这就是死锁线程
【2】递归锁
- 递归锁(Recursive Lock)是一种特殊类型的锁,它允许同一个线程多次获取同一资源的锁定。在多线程环境中,当一个线程已经持有某个锁时,如果它再次尝试获取同一个锁,通常情况下会导致死锁。但如果这个锁是递归锁,线程可以再次获得锁而不会阻塞。
- 递归锁内部维护了一个计数器和一个原始所有者线程的标识。当线程首次获取锁时,计数器会增加,锁的所有者被设置为当前线程。如果同一个线程再次获取锁,计数器会继续增加。每次线程释放锁时,计数器减一。只有当计数器回到零时,锁才会真正释放,其他线程才能获取它。
- 递归锁在 Python 中可以通过
threading
模块中的RLock
类实现。
from threading import Thread, RLock, current_thread
mutex = RLock()
def task(i):
mutex.acquire()
print(f'{current_thread().name}抢到了锁')
mutex.acquire()
print(f'{current_thread().name}抢到了锁')
mutex.release()
mutex.release()
if __name__ == '__main__':
t_list = [Thread(target=task, args=(i,)) for i in range(1, 11)]
for t in t_list:
t.start()
'''
Thread-1 (task)抢到了锁
Thread-1 (task)抢到了锁
Thread-2 (task)抢到了锁
Thread-2 (task)抢到了锁
Thread-3 (task)抢到了锁
Thread-3 (task)抢到了锁
Thread-4 (task)抢到了锁
Thread-4 (task)抢到了锁
Thread-5 (task)抢到了锁
Thread-5 (task)抢到了锁
Thread-6 (task)抢到了锁
Thread-6 (task)抢到了锁
Thread-7 (task)抢到了锁
Thread-7 (task)抢到了锁
Thread-8 (task)抢到了锁
Thread-8 (task)抢到了锁
Thread-9 (task)抢到了锁
Thread-9 (task)抢到了锁
Thread-10 (task)抢到了锁
Thread-10 (task)抢到了锁
进程已结束,退出代码0
'''
【3】信号量
-
计数器: 信号量内部维护了一个计数器,这个计数器表示可用的资源数量。当计数器大于零时,表示有资源可用;当计数器为零时,表示没有可用资源。
-
获取(Acquire)操作: 当一个线程尝试获取信号量时,如果计数器大于零,计数器就减一,线程获得资源的访问权限。如果计数器为零,线程会被阻塞,直到计数器变为大于零。
-
释放(Release)操作: 当线程完成对资源的使用后,它必须释放信号量。释放操作会使计数器加一,表示资源又变得可用。如果有其他线程正在等待这个信号量,其中一个线程(或多个,取决于计数器增加的值)将被唤醒并获得资源访问权限。
-
这里以一个停车场停车为例子
from threading import Thread, Semaphore
import time
import random
def task(semaphore, i):
semaphore.acquire()
print(f'宝马{i}号抢到了车位')
time.sleep(random.randint(1, 3))
semaphore.release()
print(f'宝马{i}离开了车位')
if __name__ == '__main__':
semaphore = Semaphore(5)
t_list = [Thread(target=task, args=(semaphore, i)) for i in range(1, 11)]
for t in t_list:
t.start()
'''
宝马1号抢到了车位
宝马2号抢到了车位
宝马3号抢到了车位
宝马4号抢到了车位
宝马5号抢到了车位
宝马3离开了车位
宝马1离开了车位
宝马6号抢到了车位
宝马4离开了车位
宝马8号抢到了车位
宝马7号抢到了车位
宝马2离开了车位
宝马9号抢到了车位
宝马5离开了车位
宝马10号抢到了车位
宝马7离开了车位
宝马10离开了车位
宝马9离开了车位
宝马6离开了车位
宝马8离开了车位
进程已结束,退出代码0
'''
- 前五个线程都会直接抢到这把锁,因为信号量为5,
- 之后就会释放一个锁进一个
【4】event事件
-
event事件可以让一个子进程或者子线程等待另一个子进程或者子线程执行完毕再执行
-
相当于join()方法可以让主进程等待调用join()方法的子线程执行完毕再执行。
-
Event
对象在 Python 的threading
模块中提供,它有以下几个关键的特性和方法:
- 状态标志:
Event
对象内部维护一个内部标志,它可以被设置为True
或False
。默认情况下,这个标志为False
。 - set() 方法: 当调用
set()
方法时,Event
对象的内部标志被设置为True
。这通常表示某个特定事件已经发生,或者某个条件已经满足。 - clear() 方法: 当调用
clear()
方法时,Event
对象的内部标志被重置为False
。 - wait() 方法: 线程调用
wait()
方法时会被阻塞,直到Event
对象的内部标志被设置为True
。如果在调用wait()
时内部标志已经是True
,那么线程将继续执行而不会阻塞。 - is_set() 方法: 返回
Event
对象内部标志的当前状态(True
或False
)。
- 这里以一个公交车上车的例子讲述
from threading import Thread, Event, Semaphore
import time
def bus(event):
print('公交车即将到站')
time.sleep(3)
print('公交车到站了')
event.set()
def waiter(i, event):
print(f'乘客{i}正在等车')
event.wait()
print(f'乘客{i}上车出发')
if __name__ == '__main__':
semaphore = Semaphore(5)
event = Event()
t_bus = Thread(target=bus, args=(event,))
t_waiter_list = [Thread(target=waiter, args=(i, event)) for i in range(1, 11)]
t_bus.start()
for t_waiter in t_waiter_list:
t_waiter.start()
'''
公交车即将到站
乘客1正在等车
乘客2正在等车
乘客3正在等车
乘客4正在等车
乘客5正在等车
乘客6正在等车
乘客7正在等车
乘客8正在等车
乘客9正在等车
乘客10正在等车
公交车到站了
乘客1上车出发
乘客2上车出发
乘客6上车出发
乘客8上车出发
乘客4上车出发
乘客9上车出发
乘客5上车出发
乘客10上车出发
乘客3上车出发
乘客7上车出发
进程已结束,退出代码0
'''
- 我起了一个bus的线程,还有多个waiter的线程
- 在bus的公交车到站后面加上一个set()
- 在乘客上车前加上一个wait()
- 程序运行到wait()会被阻塞
- 直到公交车到站,也就是set()将其内部的标志打为True,wait()才不会被阻塞