线程操作之锁的使用
一、线程锁
1.多个线程抢占资源的情况:
锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁:
案例一:
from threading import Thread,Lock
import time
K = Lock()
def func():
global n
K.acquire() # 加锁
team = n
time.sleep(0.1) # 分析:第一个线程刚拿到team = n = 100,就遇到阻塞,马上切换到下一个线程,下一个线程还是跟上一个线程一样的情况,
# 由于线程切换速度很快,一直拿到的team都是99,除非你的线程数量足够大。
# 解决方法: 给处理数据部分进行加锁处理。
n = team-1
K.release() # 释放锁
if __name__ == '__main__':
n = 100
l = []
for i in range(100):
t = Thread(target=func)
l.append(t)
t.start()
for t in l:
t.join()
print(n)
案例二:
from threading import Thread,Lock
x = 0
K = Lock()
def func():
global x
K.acquire()
for i in range(60000):
x = x+1
# t1 的x刚拿到0 保存状态 就被切换了
# t2 的x拿到0 进行+1操作 x = 1
# t1 又获得了运行 ,返回被切之前的保存状态,x=0+1 x= 1
# 其实运算出来的数字应该是+2,实际上只+1
# 这就产生了数据安全问题
K.release()
if __name__ == '__main__':
t1 = Thread(target=func)
t2 = Thread(target=func)
t3 = Thread(target=func)
t1.start()
t2.start()
t3.start()
t1.join()
t2.join()
t3.join()
print(x)
二、死锁问题
死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
from threading import Lock as Lock
import time
mutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()
解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:
from threading import Thread,RLock,currentThread
import time
k1 = k2 = RLock() # 一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
class Myt(Thread):
def run(self):
self.task1()
self.task2()
def task1(self):
k1.acquire()
print(f'{self.name} 抢到了 锁1')
k2.acquire()
print(f'{self.name} 抢到了 锁2')
k2.release()
print(f'{self.name} 释放了 锁2')
k1.release()
print(f'{self.name} 释放了 锁1')
def task2(self):
k2.acquire()
print(f'{self.name} 抢到了 锁2')
time.sleep(1) # 当线程1 执行到这一步的时候,遇到阻塞,马上切换到线程2,此时,线程1已经拿到了锁2,但是线程2也要拿,所以形成了死锁。
k1.acquire() # 解决方法: 加入递归锁RLock
print(f'{self.name} 抢到了 锁1')
k1.release()
print(f'{self.name} 释放了 锁1')
k2.release()
print(f'{self.name} 释放了 锁2')
for i in range(3):
t = Myt()
t.start()
三、 信号量Semaphore
同进程的一样
Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):
from threading import Thread,currentThread,Semaphore
import time
def func():
sm.acquire()
print(f'{currentThread().name} 正在执行')
time.sleep(3)
sm.release()
sm = Semaphore(5)
for i in range(15):
t= Thread(target=func)
t.start()
四、GIL锁:
GIL:全局解释器锁。每个线程在执行的过程都需要先获取GIL,保证同一时刻只有一个线程可以执行代码。
GIL 与Lock是两把锁,本质上也是一把互斥锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock
过程分析:所有线程抢的是GIL锁,或者说所有线程抢的是执行权限
线程1抢到GIL锁,拿到执行权限,开始执行,然后加了一把Lock,还没有执行完毕,即线程1还未释放Lock,有可能线程2抢到GIL锁,开始执行,执行过程中发现Lock还没有被线程1释放,于是线程2进入阻塞,被夺走执行权限,有可能线程1拿到GIL,然后正常执行到释放Lock。。。这就导致了串行运行的效果
既然是串行,那我们执行
t1.start()
t1.join
t2.start()
t2.join()
这也是串行执行啊,为何还要加Lock呢,需知join是等待t1所有的代码执行完,相当于锁住了t1的所有代码,而Lock只是锁住一部分操作共享数据的代码。
五、计算密集型
在 处理像科学计算 这类需要持续使用cpu的类型。
from multiprocessing import Process
from threading import Thread
import os,time
res = 0
def func():
global res
for i in range(10000000):
res += i
if __name__ == '__main__':
print(os.cpu_count()) # CPU核心数
start = time.time()
l = []
for i in range(4):
# t = Process(target=func)
t =Thread(target=func)
l.append(t)
t.start()
for t in l :
t.join()
end = time.time()
print(end-start) # 多进程耗时:3.4384214878082275 多线程耗时:4.417709827423096
由上看出:在处理持续计算长时间使用CPU的代码,多进程处理的速度比多线程快。推荐使用多进程进行运算。
六、IO密集型
指在处理像IO操作等可能引起阻塞的类型
from multiprocessing import Process
from threading import Thread
import time
def func():
time.sleep(5)
if __name__ == '__main__':
l = []
start = time.time()
for i in range(4):
# t = Process(target=func) # 多进程
t = Thread(target=func) # 多线程
l.append(t)
t.start()
for t in l:
t.join()
end = time.time()
print(end-start) # 多进程:5.8953258991241455 多线程:5.002920150756836
由上可以看出:在执行IO操作引起阻塞这种任务的时候,多线程的速度明显比多进程要快。
由上面两个案例,得出结论:
python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
多线程用于IO密集型,如socket,爬虫,web
多进程用于计算密集型,如金融分析