守护线程,线程互斥锁,GIL全局解释器锁,死锁和递归锁
Ⅰ 守护线程
- Python中的主线程是程序的起始线程,即程序启动时自动创建的第一个线程,它执行程序的主体逻辑。
- 主线程 : 其实就是你的主进程
- 线程是在进程内部开设的 ---> 进程死了 ---> 线程会随着进程死掉而死掉
- 守护线程则是在后台运行并依赖于主线程或非守护线程的存在。
【一】主线程死亡,子线程未死亡
- 主线程结束运行后不会马上结束,而是等待其他非守护子线程结束之后才会结束
- 如果主线程死亡就代表者主进程也死亡,随之而来的是所有子线程的死亡
from threading import Thread
import time
def task(name):
print(f'{name}is running')
time.sleep(3)
print(f'{name}is over')
t1 = Thread(target=task, args=('silence',))
t1.start()
print('主线程')
# silenceis running
# 主线程
# silenceis over
【二】主线程死亡,子线程也死亡
from threading import Thread
import time
def task(name):
print(f'{name}is running')
time.sleep(3)
print(f'{name}is over')
t1 = Thread(target=task, args=('silence',))
# 开启守护线程 , 主线程结束,子线程随之结束
t1.daemon = True
t1.start()
print('主线程')
# silenceis running
# 主线程
【三】迷惑性例子-Y一个加守护,一个不加
from threading import Thread
import time
def task(name):
print(f'{name}is running')
time.sleep(3)
print(f'{name}is over')
t1 = Thread(target=task, args=('silence',))
t2 = Thread(target=task, args=('mark',))
# 开启守护线程 , 主线程结束,子线程随之结束
t1.daemon = True
t1.start()
t2.start()
print('主线程')
'''主线程结束运行后不会马上结束,而是等待其他非守护子线程结束之后才会结束'''
# silenceis running
# markis running
# 主线程
# markis over silenceis over
【四】小练习
from threading import Thread
from multiprocessing import Process
import time
def foo():
print(123)
time.sleep(1)
print('end123')
def bar():
print(456)
time.sleep(3)
print('end456')
if __name__ == '__main__':
t1 = Thread(target=foo)
t2 = Thread(target=bar)
t1.daemon = True
t1.start()
t2.start()
print('main-------------')
# 123
# 456
# main-------------
# end123
# end456
Ⅱ 线程互斥锁
- 互斥锁其实是用在线程上面的
- 进程之间数据隔离 ---> 不需要加锁处理每一个进程间的数据
- 线程是共享用一个进程下的数据
【一】问题
- 所有子线程都会进行阻塞操作,导致最后的改变只是改了一次
from threading import Thread
import time
money = 100
def work_one():
global money
money -= 1
def work():
# 第一次声明拿到全局的 money = 100
global money
# 模拟获取到车票信息 temp = money = 100
temp = money
# 模拟网络延迟 # 切换 下一个线程开始
time.sleep(2)
# 模拟购票 相当于是每一个线程都拿到了初始的数据,每一个线程都在修改初始的数据
# 在修改数据的时候加锁 ---> 当前线程拿到数据 --> 修改数据 00 修改完成后放回数据 -- 下一个线程拿数据
# temp = 100 -1
# money = 99
money = temp - 1
def main():
task_list = [Thread(target=work) for i in range(100)]
[task.start() for task in task_list]
[task.join() for task in task_list]
# 所有子线程结束后打印 money
# print(money) # --> 0 work_one
# time.sleep(2)不加模拟延迟 线程会串行处理数据 一百次循环之后money = 100 就变成了money=0
print(money) # --> 99 work
# 加上time.sleep(2)模拟延迟 多个线程会同时拿到money = 100 延迟之后-1 就是99
if __name__ == '__main__':
main()
【二】解决
# from threading import Lock 加锁处理
# lock.acquire()
# lock.release()
# 在延迟线程拿数据时加锁处理
from threading import Thread, Lock
import time
money = 100
def work_one():
global money
money -= 1
def work(lock):
# 第一次声明拿到全局的 money = 100
global money
# 加锁:在数据要发生更改之前进行加锁处理
lock.acquire()
# 模拟获取到车票信息 temp = money = 100
temp = money
# 模拟网络延迟 # 切换 下一个线程开始
time.sleep(2)
# 模拟购票 相当于是每一个线程都拿到了初始的数据,每一个线程都在修改初始的数据
# 在修改数据的时候加锁 ---> 当前线程拿到数据 --> 修改数据 00 修改完成后放回数据 -- 下一个线程拿数据
# temp = 100 -1
# money = 99
money = temp - 1
# 在处理完数据之后进行解锁
lock.release()
def main():
lock = Lock()
task_list_one = [Thread(target=work_one) for i in range(100)]
[task.start() for task in task_list_one]
[task.join() for task in task_list_one]
# 所有子线程结束后打印 money
print(money) # --> 0 work_one
if __name__ == '__main__':
main()
Ⅲ GIL全局解释器锁
【一】GIL全局解释器锁介绍
【1】官方解释
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)
结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
'''
垃圾回收机制
引用计数,标记清除,分代回收
GIL只存在于Cpython解释器中,不是python的特征
GIL是一把互斥锁用于阻止同一个进程下的多个线程同时执行
原因是因为CPython解释器中的垃圾回收机制不是线程安全的
强调:同一个进程下的多个线程不能同时执行即不能利用多核优势
'''
【2】Python解释器版本
- Cpython
- Jpython
- Pypypython
- 但是普遍使用的都是Cpython解释器
【二】为什么GIL需要存在
- CPython解释器中的垃圾回收机制不是线程安全的
- 反向验证GIL的存在:
- 如果不存在会产生垃圾回收机制与正常线程之间数据错乱
- GIL是加在CPython解释器上面的互斥锁
- 同一个进程下的多个线程要想执行必须先抢GIL锁 所以同一个进程下多个线程肯定不能同时运行 即无法利用多核优势
【三】GIL锁与普通互斥锁的区别
【1】普通版
- 当睡了 0.1s 后
- 所有线程都去抢那把 GIL 锁住的数据,当所有子线程都抢到后再去修改数据就变成了 99
from threading import Thread
import time
money = 100
def work():
global money
temp = money
time.sleep(0.1)
money = temp - 1
def main():
task_list = [Thread(target=work) for i in range(100)]
[task.start() for task in task_list]
[task.join() for task in task_list]
print(money)
if __name__ == '__main__':
main()
# 99
【2】升级版
- 谁先抢到谁就先处理数据
from threading import Thread
import time
money = 100
def work():
global money
temp = money
money = temp -1
def main():
task_list = [Thread(target=work) for i in range(100)]
[task.start() for task in task_list]
[task.join() for task in task_list]
print(money)
if __name__ == '__main__':
main()
# 0
【3】终极版
- 自动加锁并解锁
- 子线程启动 , 后先去抢 GIL 锁 , 进入 IO 自动释放 GIL 锁 , 但是自己加的锁还没解开 ,其他线程资源能抢到 GIL 锁,但是抢不到互斥锁
- 最终 GIL 回到 互斥锁的那个进程上,处理数据
from threading import Thread, Lock
import time
mutex = Lock()
money = 100
def work():
global money
# 自动执行 加锁 再解锁操作
with mutex:
temp = money
# 只要进入 IO 会自动释放 GIL 锁
time.sleep(0.1)
money = temp -1
def main():
task_list = [Thread(target=work) for i in range(100)]
[task.start() for task in task_list]
[task.join() for task in task_list]
print(money)
if __name__ == '__main__':
main()
# 0
【四】GIL导致多线程无法利用多核优势
【1】 Cpython 解释器中 GIL
- 在 Cpython 解释器中 GIL 是一把互斥锁,用来阻止同一个进程下的多个线程的同时进行
- 同一个进程下的多个线程无法利用这一优势?
- Python的多线程是不是一点用都没有?
- 因为在 Cpython 中的内存管理不是线程安全的
- ps:内存管理(垃圾回收机制)
- 应用计数
- 标记清除
- 分代回收
- ps:内存管理(垃圾回收机制)
【2】Python的多线程作用
- 同一个进程下的多线程无法利用多核优势,是不是就没用了
- 多线程是否有用要看情况
- 单核
- 四个任务(IO密集型/计算密集型)
- 多核
- 四个任务(IO密集型/计算密集型)
- 单核
- 多线程是否有用要看情况
(1)单个CPU
- 多个IO密集型任务
- 多进程:浪费资源 无法利用多个CPU
- 多线程:节省资源 切换+保存状态
- 多个计算密集型任务
- 多进程:耗时更长 创建进程的消耗+切换消耗
- 多线程:耗时较短 切换消耗
(2)多个CPU
- 多个IO密集型任务
- 多进程:浪费资源 多个CPU无用武之地
- 多线程:节省资源 切换+保存状态
- 多个计算密集型任务
- 多进程:利用多核 速度更快
- 多线程:速度较慢
结论:多进程和多线程都有应用场景 尤其是多线程并不是没有用
(3)计算密集型(计算任务重)
- 一直处在计算运行中
- 每个任务都需要 10s
- 单核
- 多进程:额外消耗资源
- 多线程:减少开销
- 多核
- 多进程:总耗时 10s
- 多线程:总耗时 40s+
(4)IO密集型(多阻塞)
- 存在多个 IO 阻塞切换操作
- 每个任务都需要 10s
- 多核
- 多进程:相对浪费资源
- 多线程:更加节省资源
- 多核
- 每个任务都需要 10s
(5)优劣小结
- 强调:同一个进程下的多个线程不能同时执行即不能利用多核优势
- 很多不懂python的程序员会喷python是垃圾 速度太慢 有多核都不能用
- 反怼:虽然用一个进程下的多个线程不能利用多核优势 但是还可以开设多进程!!!
- 再次强调:python的多线程就是垃圾!!!
- 反怼:要结合实际情况
- 如果多个任务都是I0密集型的 那么多线程更有优势(消耗的资源更少)
- 多道技术:切换+保存状态
- 如果多个任务都是计算密集型 那么多线程确实没有优势 但是可以用多进程
- CPU越多越好
- 如果多个任务都是I0密集型的 那么多线程更有优势(消耗的资源更少)
- 以后用python就可以多进程下面开设多线程从而达到效率最大化
所有的解释型语言都无法做到同一个进程下多个线程利用多核优势
【3】小结
- 计算是消耗cpu的:
- 代码执行,算术,for都是计算
- io不消耗cpu:
- 打开文件,写入文件,网络操作都是io
- 如果遇到io,该线程会释放cpu的执行权限,cpu转而去执行别的线程
- 由于python有gil锁,开启多条线程,统一时刻,只能有一条线程在执行
- 如果是计算密集型,开了多线程,同一时刻,只有一个线程在执行
- 多核cpu,就会浪费多核优势
- 如果是计算密集型,我们希望,多个核(cpu),都干活,同一个进程下绕不过gil锁
- 所以我们开启多进程,gil锁只能锁住某个进程中得线程,开启多个进程,就能利用多核优势
- io密集型---》只要遇到io,就会释放cpu执行权限
- 进程内开了多个io线程,线程多半都在等待,开启多进程是不能提高效率的,反而开启进程很耗费资源,所以使用多线程即可
(1)计算密集型任务(多进程)
- 计算密集型任务主要是指需要大量的CPU计算资源的任务,其中包括执行代码、进行算术运算、循环等。
- 在这种情况下,使用多线程并没有太大的优势。
- 由于Python具有全局解释器锁(Global Interpreter Lock,GIL),在同一时刻只能有一条线程执行代码,这意味着在多线程的情况下,同一时刻只有一个线程在执行计算密集型任务。
- 但是,如果使用多进程,则可以充分利用多核CPU的优势。
- 每个进程都有自己独立的GIL锁,因此多个进程可以同时执行计算密集型任务,充分发挥多核CPU的能力。
- 通过开启多个进程,我们可以将计算密集的任务分配给每个进程,让每个进程都独自执行任务,从而提高整体的计算效率。
(2)IO密集型任务(多线程)
- IO密集型任务主要是指涉及大量输入输出操作(如打开文件、写入文件、网络操作等)的任务。
- 在这种情况下,线程往往会因为等待IO操作而释放CPU执行权限,不会造成太多的CPU资源浪费。
- 因此,使用多线程能够更好地处理IO密集型任务,避免了频繁切换进程的开销。
- 当我们在一个进程中开启多个IO密集型线程时,大部分线程都处于等待状态,开启多个进程却不能提高效率,反而会消耗更多的系统资源。
- 因此,在IO密集型任务中,使用多线程即可满足需求,无需开启多个进程。
(3)总结
- 计算密集型任务使用多进程可以充分利用多核CPU的优势,而IO密集型任务使用多线程能够更好地处理IO操作,避免频繁的进程切换开销。
- 根据任务的特性选择合适的并发方式可以有效提高任务的执行效率。
【五】GIL特点总结
- 1.GIL 不是python的特点而是Cpython解释器的特点
- 2.GIL 保证解释器级别的数据的安全
- 3.GIL会导致同一个进程下的多个线程的无法同时进行即无法利用多核优势
- 4.针对不同的数据还是需要加不同的锁处理
- 5.解释型语言的通病:同一个进程下的多个线程无法利用多核优势
Ⅳ 死锁和递归锁
【一】死锁
【1】介绍
- 死锁是指两个或多个进程,在执行过程中,因争夺资源而造成了互相等待的一种现象。
- 即两个或多个进程持有各自的锁并试图获取对方持有的锁,从而导致被阻塞,不能向前执行,最终形成僵局。
- 在这种情况下,系统资源利用率极低,系统处于一种死循环状态。
【2】例子
- 要吃饭,必须具备盘子和筷子
- 但是一个人拿着盘子等筷子。
- 另一个人拿着筷子等盘子
【3】解决办法
- 锁不要有多个,一个足够
- 如果真的发生了死锁的问题,必须迫使一方先交出锁
【4】示例
from threading import Thread, Lock
import time
# 产生两把
mutexA = Lock()
mutexB = Lock()
class MyThread(Thread):
def run(self):
self.f1()
self.f2()
def f1(self):
mutexA.acquire()
print(f'{self.name}抢到了A锁')
mutexB.acquire()
print(f'{self.name}抢到了B锁')
mutexA.release()
mutexB.release()
def f2(self):
mutexB.acquire()
print(f'{self.name}抢到了B锁')
time.sleep(2)
mutexA.acquire()
print(f'{self.name}抢到了A锁')
mutexA.release()
mutexB.release()
for i in range(10):
t = MyThread()
t.start()
# Thread-1抢到了A锁
# Thread-1抢到了B锁
# Thread-1抢到了B锁
# Thread-2抢到了A锁
【二】递归锁
【1】介绍
- 递归锁(也叫可重入锁)是一种特殊的锁,它允许一个线程多次请求同一个锁,称为“递归地”请求锁
- 在该线程释放锁之前,会对锁计数器进行累加操作,线程每成功获得一次锁时,都要进行相应的解锁操作,直到锁计数器清零才能完全释放该锁。
- 递归锁能够保证同一线程在持有锁时能够再次获取该锁,而不被自己所持有的锁所阻塞,从而避免死锁的发生。
- 但是注意要正常使用递归锁,避免过多地获取锁导致性能下降。
【2】示例
- 可以被连续的 acquire 和 release
- 但是只能被第一个抢到这把锁上执行上述操作
- 他的内部有一个计数器,每acquire一次计数 +1 每release一次 计数-1
- 只要计数不为0,那么其他人都无法抢到该锁
from threading import Thread, Lock, RLock
import time
import random
# 两个变量同时指向一把锁
metexA = metexB = RLock()
# 类只要加括号多次 产生的肯定不同的对象
# 如果你想要实现多次加括号等到的是相同的对象 - 单例模式
class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
print(f'func1 {self.name} :>>>> 开始加锁A ')
metexA.acquire()
print(f'func1 {self.name} 抢到了A锁')
print(f'func1 {self.name} :>>>> 开始加锁B ')
metexB.acquire()
print(f'func1 {self.name} 抢到了B锁')
metexB.release()
print(f'func1 {self.name} :>>>> 释放锁B ')
metexA.release()
print(f'func1 {self.name} :>>>> 释放锁A ')
def func2(self):
print(f'func2 {self.name} :>>>> 开始加锁B ')
metexB.acquire()
print(f'func2 {self.name} 抢到了B锁')
sleep_time = random.randint(1, 3)
print(f'func2 {self.name} 开始睡觉 :>>>> {sleep_time}s')
time.sleep(sleep_time)
print(f'func2 {self.name} 睡觉结束 :>>>> {sleep_time}s')
print(f'func2 {self.name} :>>>> 开始加锁A ')
metexA.acquire()
print(f'func2 {self.name} 抢到了A锁')
metexA.release()
print(f'func2 {self.name} :>>>> 释放锁A ')
metexB.release()
print(f'func2 {self.name} :>>>> 释放锁B ')
def main():
for i in range(10):
t = MyThread()
t.start()
if __name__ == '__main__':
main()
分类:
网络并发编程
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY