【4.0】知识点小结(线程进阶)

Dream-Z / 2023-06-26 / 原文

【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 高并发效果

  • 服务端

  • 客户端

【七】总结

  • 我们可以通过多进程下面开设多线程
    • 再通过多线程下面开设协程
  • 达到提升程序的执行效率