线程相关锁
一、线程同步锁(互斥锁)
多线程抢占资源的数据安全问题
# 案例一
from threading import Thread
import time
n = 10
def func():
global n
temp = n
time.sleep(0.2)
n = temp - 1
if __name__ == '__main__':
lis = []
for i in range(10):
t = Thread(target=func)
t.start()
lis.append(t)
for i in lis:
i.join()
print(n) # 9
'''
因为将n的递减操作拆分为两个阶段,一个赋值,一个减值,中间休眠0.2s
此时,当10个线程去修改n的时候,就会出现在0.2s之前10个线程拿到的n都是10
最后每个线程返回的修改值都是9,这样就产生了数据不同步的安全问题
所以,当延迟时间过长或者数据量过大时,使用多线程同时访问同一个数据时,不安全
'''
# 案例二
from threading import Thread
n = 0
def task():
global n
for i in range(100000):
n += 1
if __name__ == '__main__':
lis = []
for i in range(3):
t = Thread(target=task)
t.start()
lis.append(t)
for i in lis:
i.join()
print(n) # 274155
'''
当启动三个线程来执行task时
发现结果永远小于300000
这也验证了多个线程修改同一个数据时会引发数据安全问题
**********所以,有了进程锁的概念***********
'''
使用线程同步锁解决数据安全问题
# 案例一解决方法
from threading import Thread,Lock
import time
n = 10
lock = Lock()
def func():
global n
lock.acquire()
temp = n
time.sleep(0.2)
n = temp - 1
lock.release()
if __name__ == '__main__':
lis = []
for i in range(10):
t = Thread(target=func)
t.start()
lis.append(t)
for i in lis:
i.join()
print(n) # 0
# 案例二解决方
from threading import Thread,Lock
n = 0
lock = Lock()
def task():
global n
lock.acquire()
for i in range(100000):
n += 1
lock.release()
if __name__ == '__main__':
lis = []
for i in range(3):
t = Thread(target=task)
t.start()
lis.append(t)
for i in lis:
i.join()
print(n) # 300000
'''
给要处理的数据加一把锁,限定同一时间只能有一个线程来访问这段代码
同样,在保证了数据安全的同时,牺牲了效率
'''
二、死锁
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁:
# 著名的科学家吃面问题
from threading import Thread,Lock
import time
noodle_lock = Lock()
fork_lock = Lock()
def eat1(name):
noodle_lock.acquire()
print(f'{name}拿到面条了')
fork_lock.acquire()
time.sleep(1)
print(f'{name}拿到叉子了')
print(f'{name}吃面')
fork_lock.release()
noodle_lock.release()
def eat2(name):
fork_lock.acquire()
print(f'{name}拿到叉子了')
time.sleep(1)
noodle_lock.acquire()
print(f'{name}拿到面条了')
print(f'{name}吃面')
noodle_lock.release()
fork_lock.release()
Thread(target=eat1,args=('alex',)).start()
Thread(target=eat2,args=('nick',)).start()
Thread(target=eat1,args=('tank',)).start()
Thread(target=eat2,args=('egon',)).start()
'''
alex拿到面条了
alex拿到叉子了
alex吃面
nick拿到叉子了
tank拿到面条了
'''
三、递归锁
死锁的解决方法就是递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:
from threading import Thread,RLock
import time
fork_lock = noodle_lock = RLock()
def eat1(name):
noodle_lock.acquire()
print(f'{name}拿到面条了')
fork_lock.acquire()
print(f'{name}拿到叉子了')
print(f'{name}吃面')
fork_lock.release()
noodle_lock.release()
def eat2(name):
fork_lock.acquire()
print(f'{name}拿到叉子了')
time.sleep(1)
noodle_lock.acquire()
print(f'{name}拿到面条了')
print(f'{name}吃面')
noodle_lock.release()
fork_lock.release()
Thread(target=eat1,args=('alex',)).start()
Thread(target=eat2,args=('nick',)).start()
Thread(target=eat1,args=('tank',)).start()
Thread(target=eat2,args=('egon',)).start()
'''
alex拿到面条了
alex拿到叉子了
alex吃面
nick拿到叉子了
nick拿到面条了
nick吃面
tank拿到面条了
tank拿到叉子了
tank吃面
egon拿到叉子了
egon拿到面条了
egon吃面
'''
四、全局解释器锁GIL
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
在多线程环境中,Python 虚拟机按以下方式执行:
a、设置 GIL;
b、切换到一个线程去运行;
c、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
d、把线程设置为睡眠状态;
e、解锁 GIL;
d、再次重复以上所有步骤。
在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。
GIL 就是当线程访问cpu对数据进行修改时介于两者之间的一把锁,其实就是Cpython的解释器起作用。
他控制了同一时刻只能有一个线程来访问数据,将此线程设置为睡眠状态
当线程对数据修改完毕了,GIL就会释放该线程
之后才允许下一个线程来访问数据
GIL锁的是线程
GIL导致的不能并发问题是Cpython的特性,目前还不能得以解决!