【4.0】知识点小结(线程进阶)
【4.0】知识点小结(线程进阶)
【一】什么是死锁与递归锁
- 死锁是指两个或多个进程,在执行过程中,因争夺资源而造成了互相等待的一种现象。
- 即两个或多个进程持有各自的锁并试图获取对方持有的锁,从而导致被阻塞,不能向前执行,最终形成僵局。
- 在这种情况下,系统资源利用率极低,系统处于一种死循环状态。
- 递归锁(也叫可重入锁)是一种特殊的锁,它允许一个线程多次请求同一个锁,称为“递归地”请求锁
- 在该线程释放锁之前,会对锁计数器进行累加操作,线程每成功获得一次锁时,都要进行相应的解锁操作,直到锁计数器清零才能完全释放该锁。
- 递归锁能够保证同一线程在持有锁时能够再次获取该锁,而不被自己所持有的锁所阻塞,从而避免死锁的发生。
- 但是注意要正常使用递归锁,避免过多地获取锁导致性能下降。
【1】死锁
from threading import Thread, Lock
import time
metexA = Lock()
metexB = Lock()
# 类只要加括号多次 产生的肯定不同的对象
# 如果你想要实现多次加括号等到的是相同的对象 - 单例模式
class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
metexA.acquire()
# self.name:获取当前线程名
print(f'{self.name} 抢到了A锁')
metexB.acquire()
print(f'{self.name} 抢到了B锁')
metexB.release()
metexA.release()
def func2(self):
metexB.acquire()
# self.name:获取当前线程名
print(f'{self.name} 抢到了A锁')
time.sleep(2)
metexA.acquire()
print(f'{self.name} 抢到了B锁')
metexA.release()
metexB.release()
def main():
for i in range(10):
t = MyThread()
t.start()
if __name__ == '__main__':
main()
# Thread-1 抢到了A锁
# Thread-1 抢到了B锁
# Thread-1 抢到了A锁
# Thread-2 抢到了A锁
# 线程卡死
# 开启十个线程 第一个线程走完第一圈 回到原地抢 A 结果第二个线程已经拿到了A 导致AB卡死
【2】递归锁
可以被连续的 acquire 和 release
但是只能被第一个抢到这把锁上执行上述操作
他的内部有一个计数器,每acquire一次计数 +1 每release一次 计数-1
只要计数不为0,那么其他人都无法抢到该锁
from threading import Thread, Lock, RLock
import time
# 两个变量同时指向一把锁
metexA = metexB = RLock()
# 类只要加括号多次 产生的肯定不同的对象
# 如果你想要实现多次加括号等到的是相同的对象 - 单例模式
class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
metexA.acquire()
# self.name:获取当前线程名
print(f'{self.name} 抢到了A锁')
metexB.acquire()
print(f'{self.name} 抢到了B锁')
metexB.release()
metexA.release()
def func2(self):
metexB.acquire()
# self.name:获取当前线程名
print(f'{self.name} 抢到了A锁')
time.sleep(2)
metexA.acquire()
print(f'{self.name} 抢到了B锁')
metexA.release()
metexB.release()
def main():
for i in range(10):
t = MyThread()
t.start()
if __name__ == '__main__':
main()
# Thread-1 抢到了A锁
# Thread-1 抢到了B锁
# Thread-1 抢到了A锁
# Thread-1 抢到了B锁
# Thread-2 抢到了A锁
# Thread-2 抢到了B锁
# Thread-2 抢到了A锁
# Thread-2 抢到了B锁
# Thread-4 抢到了A锁
# Thread-4 抢到了B锁
# Thread-4 抢到了A锁
# 不会卡主正常进行
【二】信号量
- 信号量在不同的阶段可能对应不同的技术点
- 在并发编程中信号量指的是锁
如果我们将互斥锁比喻成一个厕所
那信号量就相当于多个厕所
【1】基础版 1.0
通过信号量限制线程抢的线程开设数
线程数是5 , 只能每次5个人一起抢,一起走
【三】Event事件
- 一些进程/线程需要等待另外一些进程/线程运行完毕后才能运行,类似于发射信号一样
【四】线程q
- 同一个进程下的多个线程数据是共享的
- 为什么在同一个进程下还要使用队列?
- 因为队列 = 管道 + 锁
- 使用队列是为了保证数据的安全
【1】后进先出 q
【2】PriorityQueue设置队列优先级
数字越小优先级越高
import queue
# (2)优先级 q
# 可以给防止队列中的数据设置队列优先级
q = queue.PriorityQueue(3)
# (优先级,参数)
q.put((10, '111'))
q.put((100, '222'))
q.put((0, '333'))
q.put((-5, '444'))
print(q.get())
# (-5, '444')
# (优先级, 参数)
# 数字越小优先级越高
【五】进程池与线程池
【1】TCP实现并发的效果的原理
-
没开设一个客户端
- 就会有一个服务端服务
-
服务端
- 客户端
无论是开设进程还是开设线程,都需要消耗资源
只不过开始线程消耗的资源比开始进程相对少一些
硬件的开发速度永远赶不上软件开发速度
我们的宗旨是保证计算机硬件正常工作的情况下最大程度的利用它
【2】池的概念
【1】什么是池?
- 池是用来保证计算机硬件安全的情况下最大限度的利用计算机
- 池降低了程序的运行效率,但是保证了计算机硬件的安全,从而保证程序的正常运行
【2】基础版 2.0
【3】升级版 3.0
【4】进阶版 4.0
【5】迭代版 5.0
【6】增强版 6.0 - pool.shutdown()
等待所有子线程结束后再打印程序运行结果
【3】进程池的使用
【1】基础版 1.0
开设进程的进程 ID 号不会发生改变
【2】进阶版 2.0 - add_done_callback(call_back)
add_done_callback(call_back) 返回值正是我们的上面 pool的返回的结果
【3】升级版 - 3.0
【4】总结
# 导入模块
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# 构建池子
pool = ProcessPoolExecutor(5)
# 提交任务
pool.submit(task, i).add_done_callback(call_back)
【六】协程
-
进程
- 资源单位
-
线程
- 执行单位
-
协程
- 假想存在概念:单线程下实现并发的效果
- 在代码层面上监控所有的 IO 操作,一旦遇到 IO 就在代码层级完成切换
- 让CPU一直运行,没有IO,从而提升程序的运行效率
-
多道技术
-
切换 + 并发状态
-
切换不一定是提升效率,也可能是降低效率
-
IO 切换:提升效率
-
无IO 切换:降低效率
-
-
代码如何实现?
- 保存上一次的执行状态,下一次继续执行
- yield 方法(保存状态)
- 保存上一次的执行状态,下一次继续执行
-
-
CPU切换的状态
- 程序遇到IO
- 程序长时间占用
【1】验证切换是否一定提升效率?
【1】基础版 - 1.0
最原始的计算方式:串行
【2】升级版 - 2.0
切换 + yield --- 时间变长
【3】增强版 - 3.0 - gevent模块
- 基础版 - 单线程操作
- 升级版 - 多线程 - 切换 IO
程序运行的时间取自运行时间最长的那个
【2】协程实现 TCP 高并发效果
- 服务端
- 客户端
【七】总结
- 我们可以通过多进程下面开设多线程
- 再通过多线程下面开设协程
- 达到提升程序的执行效率