互斥锁、死锁及GIL全局解释器锁
互斥锁
在生产者消费者模型中,我们需要一个消息队列、文件、数据库来充当我们的缓冲区完成进程间的通信,而进程同时处理数据是存在不安全性的,这个时候就需要对操作数据的代码进行加锁处理,让处理某一个数据的进程只能同时存在一个。
multiprocessing模块的Queue所产生的消息队列实际上是管道加锁的结构,已经帮我们加过互斥锁了,我们可以通过文件来演示一下,互斥锁怎么用。
互斥锁实操
from multiprocessing import Process, Lock # 导入锁类,threading中也有锁类
import time
import json
import random
def search(name):
with open(r'data.json', 'r', encoding='utf8') as f:
data = json.load(f)
print('%s查看票 目前剩余:%s' % (name, data.get('ticket_num')))
def buy(name):
# 先查询票数
with open(r'data.json', 'r', encoding='utf8') as f:
data = json.load(f)
# 模拟网络延迟
time.sleep(random.randint(1, 3))
# 买票
if data.get('ticket_num') > 0:
with open(r'data.json', 'w', encoding='utf8') as f:
data['ticket_num'] -= 1
json.dump(data, f)
print('%s 买票成功' % name)
else:
print('%s 买票失败 非常可怜 没车回去了!!!' % name)
def run(name, mutex):
search(name)
mutex.acquire() # 抢锁
buy(name)
mutex.release() # 释放锁
if __name__ == '__main__':
mutex = Lock() # 主进程产生一把锁
for i in range(10):
p = Process(target=run, args=('用户%s号' % i, mutex)) # 将锁当参数传入任务
p.start()
互斥锁注意事项
-
互斥锁不能滥用,因为其目的是让异步并发的程序在某些节点保持同步来保障数据的安全。
如果整个任务都加了互斥锁,那么进程间会全部串行,那么多进程就失去了意义。
-
互斥锁建议只在数据的增改部分进行加锁,保障数据安全即可。
-
互斥锁也有很多种,如数据库中的行锁、表锁,本质都是保障数据的安全,只是范围不一样。
死锁现象
from threading import Thread,Lock
import time
mutexA = Lock() # 产生锁A和锁B两把锁
mutexB = Lock()
class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print(f'{self.name}抢到了A锁')
mutexB.acquire()
print(f'{self.name}抢到了B锁')
mutexB.release()
print(f'{self.name}释放了B锁')
mutexA.release()
print(f'{self.name}释放了A锁')
def func2(self):
mutexB.acquire()
print(f'{self.name}抢到了B锁')
time.sleep(1) # 进入IO
mutexA.acquire()
print(f'{self.name}抢到了A锁')
mutexA.release()
print(f'{self.name}释放了A锁')
mutexB.release()
print(f'{self.name}释放了B锁')
for i in range(10):
obj = MyThread()
obj.start()
"""
开启的多个进程抢锁,当某个进程的func2抢到B锁后进入IO后,转到其他某个进程的func1抢到A锁后
想抢B锁时发现B锁已经被抢了,所以阻塞又切换进程,切换到那个抢到B锁的进程时,发现它也卡在抢A锁。
于是,所有的进程都在等锁,都陷入了阻塞,整个程序就停滞了,卡死。
"""
这只是死锁的一种简单情况,我们尚可理清原因,实际的死锁更加复杂,也容易出现。
所以我们一般会用别人加好锁的程序,来防止我们死锁的发生。
信号量 —— Semaphore
可以简单理解为多个互斥锁,我们用Semaphore(n)
来定义多把互斥锁,这个锁可以被n个进程\线程抢到手中,当第n+1个进程\线程想抢锁就会进入等待。
而信号量的另一种描述是:在多线程环境下,用于保证多个关键的资源\代码不被并发调用。当进入关键代码段时,必须占用一个信号量,结束关键代码段后,则必须释放这个信号量。
from threading import Thread, Lock, Semaphore
import time
import random
sp = Semaphore(5) # 信号量为5的锁
class MyThread(Thread):
def run(self):
sp.acquire() # 拿到一个信号量
print(self.name)
time.sleep(random.randint(1, 3))
sp.release() # 释放一个信号量
for i in range(20):
t = MyThread()
t.start()
GIL全局解释器锁
什么是GIL
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.
以上是官方文档中关于GIL(global interpreter lock)的描述。
-
只有在CPython中会存在全局解释器锁
除此以外还有JPython和PyPython,最常用的就是CPython
-
这个锁会限制多线程同时使用解释器执行代码
这也就意味着,CPython中的多线程无法利用多核优势,其多线程只能并发不能并行。
-
这个解释器锁必要的原因是由于CPython的内存管理(垃圾回收机制)不是线程安全的
可能导致这个线程使用解释器创建数据时,还没绑定变量的间隙就被另外一个线程根据引用计数为0当做垃圾清除掉了,导致数据值丢失。
验证GIL的存在
from threading import Thread
num = 100
def task():
global num
num -= 1
t_list = []
for i in range(100):
t = Thread(target=task) # 开100个线程分别让num变量指向的值-1
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(num) # 等待所有线程结束后得到num的值为0
如果没有锁的存在,那么在num绑定num-1的结果前,就可能有其他线程的num先一步按上一个num进行计算。
GIL锁是否可以代替一般的锁?
答案是否定的,GIL是解释器级别的锁,它只能保障线程间不会因为内存管理问题导致的数据丢失复用问题。但是数据处理的间隙,可能还是会出现数据安全问题。
import time
from threading import Thread,Lock
num = 100
def task(mutex):
global num
# mutex.acquire()
count = num
time.sleep(0.1)
num = count - 1
# mutex.release()
mutex = Lock()
t_list = []
for i in range(100):
t = Thread(target=task,args=(mutex,))
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(num) # 如果注释处不加锁,那么数据处理间隙是可能出现数据安全问题的
python多线程的作用
既然python多线程无法利用CPU多核优势,无法实现并行,是否就没用了?
这的确在很大程度上降低了并发的上限,但是python的多线程还是可以实现单核并发的,也就是说在遇到IO比较多的时候,多线程依旧可以节省大量的时间,而如果是计算密集型的程序,那python的多线程所带来的速度提升就十分有限了。
关于python中的多进程和多线程面对单核\多核与计算密集\IO密集的时候,其提升的分析:
条件与环境 | 分析 | 结论 |
---|---|---|
单核IO密集 | 都是通过切换的方式实现异步,而进程开辟需要更多时间 | 多线程有一定优势 |
单核计算密集 | 都是通过切换的方式实现异步,而进程开辟需要更多时间 | 多线程有一定优势 |
多核IO密集 | IO密集会频繁切换经常进入阻塞,所以多进程的优势并不明显,反而线程消耗的资源和时间少 | 多线程有一点点优势 |
多核计算密集 | 因为python的线程无法利用多核优势,所以面对始终需要CPU工作的计算密集型程序,还是能利用多核的多进程能节省更多时间 | 多进程有巨大优势 |