【0918 | Day 33】线程锁/死锁/递归锁/GIL锁/多进程vs多线程
线程锁
例子(线程锁)
线程锁本质是一个互斥锁,保证数据安全
不加锁的情况
from threading import Thread, Lock
x = 0
mutex = Lock()
def task():
global x
for i in range(100000):
x += 1
'''
==》t1 的 x刚拿到0 保存状态 就被切了
==》t2 的 x拿到0 进行+1 1
==》t1 又获得运行了 x = 0 +1 1
思考:一共加了几次1? 加了两次1 真实运算出来的数字本来应该+2 实际只+1
因此产生数据安全的问题
'''
if __name__ == '__main__':
t1 = Thread(target=task)
t2 = Thread(target=task)
t3 = Thread(target=task)
t1.start()
t2.start()
t3.start()
t1.join()
t2.join()
t3.join()
print(x) #结果<=300000(注意只有当range数据较大时才会产生这个问题)
加锁的情况
from threading import Thread, Lock
x = 0
mutex = Lock()
def task():
global x
mutex.acquire()
for i in range(100000):
x += 1
mutex.release()
if __name__ == '__main__':
t1 = Thread(target=task)
t2 = Thread(target=task)
t3 = Thread(target=task)
t1.start()
t2.start()
t3.start()
t1.join()
t2.join()
t3.join()
print(x) #300000
死锁
介绍
死锁的这个概念在很多地方都存在,大概介绍下是怎么产生的
A拿了一个苹果
B拿了一个香蕉
A现在想再拿个香蕉,就在等待B释放这个香蕉
B同时想要再拿个苹果,这时候就等待A释放苹果
这样就是陷入了僵局,这就是生活中的死锁
python中在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。
例子(死锁)
import threading
import time
lock_apple = threading.Lock()
lock_banana = threading.Lock()
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
self.fun1()
self.fun2()
def fun1(self):
lock_apple.acquire() # 如果锁被占用,则阻塞在这里,等待锁的释放
print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime()))
lock_banana.acquire()
print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
lock_banana.release()
lock_apple.release()
def fun2(self):
lock_banana.acquire()
print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
time.sleep(0.1)
lock_apple.acquire()
print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime()))
lock_apple.release()
lock_banana.release()
if __name__ == "__main__":
for i in range(0, 10): #建立10个线程
my_thread = MyThread() #类继承法是python多线程的另外一种实现方式
my_thread.start()
-----------------------------------我是一条分割线---------------------------------
线程 Thread-1 , 想拿: 苹果--Sun Apr 28 12:21:06 2019
线程 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019
线程 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019
线程 Thread-2 , 想拿: 苹果--Sun Apr 28 12:21:06 2019
上面的代码其实就是描述了苹果和香蕉的故事。大家可以仔细看看过程。下面我们看看执行流程
fun1中,线程1先拿了苹果,然后拿了香蕉,然后释放香蕉和苹果,然后再在fun2中又拿了香蕉,sleep 0.1秒。
在线程1的执行过程中,线程2进入了,因为苹果被线程1释放了,线程2这时候获得了苹果,然后想拿香蕉
这时候就出现问题了,线程一拿完香蕉之后想拿苹果,返现苹果被线程2拿到了,线程2拿到苹果执行,想拿香蕉,发现香蕉被线程1持有了
双向等待,出现死锁,代码执行不下去了
上面就是大概的执行流程和死锁出现的原因。在这种情况下就是在同一线程中多次请求同一资源时候出现的问题。
递归锁(RLock)
介绍
为了支持在同一线程中多次请求同一资源,python提供了"递归锁":threading.RLock。
RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
例子(递归锁)
下面我们用递归锁RLock解决上面的死锁问题:
import threading
import time
lock = threading.RLock() #递归锁
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
self.fun1()
self.fun2()
def fun1(self):
lock.acquire() # 如果锁被占用,则阻塞在这里,等待锁的释放
print (f"线程{self.name}, 想拿:'苹果'--{time.ctime()}" )
lock.acquire()
print (f"线程{self.name}, 想拿: '香蕉'--{time.ctime()}" )
lock.release()
lock.release()
def fun2(self):
lock.acquire()
print (f"线程{self.name}, 想拿: '香蕉'--{time.ctime()}" )
time.sleep(0.1)
lock.acquire()
print (f"线程{self.name}, 想拿:'苹果'--{time.ctime()}" )
lock.release()
lock.release()
if __name__ == "__main__":
for i in range(0, 3): #建立10个线程
my_thread = MyThread() #类继承法是python多线程的另外一种实现方式
my_thread.start()
线程Thread-1, 想拿:'苹果'--Wed Sep 18 17:01:30 2019
线程Thread-1, 想拿: '香蕉'--Wed Sep 18 17:01:30 2019
线程Thread-1, 想拿: '香蕉'--Wed Sep 18 17:01:30 2019
线程Thread-1, 想拿:'苹果'--Wed Sep 18 17:01:30 2019
线程Thread-2, 想拿:'苹果'--Wed Sep 18 17:01:30 2019
线程Thread-2, 想拿: '香蕉'--Wed Sep 18 17:01:30 2019
线程Thread-2, 想拿: '香蕉'--Wed Sep 18 17:01:30 2019
线程Thread-2, 想拿:'苹果'--Wed Sep 18 17:01:30 2019
线程Thread-3, 想拿:'苹果'--Wed Sep 18 17:01:30 2019
线程Thread-3, 想拿: '香蕉'--Wed Sep 18 17:01:30 2019
线程Thread-3, 想拿: '香蕉'--Wed Sep 18 17:01:30 2019
线程Thread-3, 想拿:'苹果'--Wed Sep 18 17:01:30 2019
上面我们用一把递归锁,就解决了多个同步锁导致的死锁问题。大家可以把RLock理解为大锁中还有小锁,只有等到内部所有的小锁,都没有了,其他的线程才能进入这个公共资源。
思考
如果我们都加锁也就是单线程了,那我们还要开多线程有什么用呢?
这里解释下,在访问共享资源的时候,锁是一定要存在的。
但是我们的代码中,不总是在访问公共资源的,还有一些其他的逻辑可以使用多线程。
所以我们在代码里面加锁的时候,要注意在什么地方加,对性能的影响最小,这个就靠对逻辑的理解了。
信号量(Semphare)
介绍
它控制同一时刻多个线程访问同一个资源的线程数
- 实例化时,指定使用量。
- 其内置计数器,锁定时+1, 释放时-1,计数器为0则阻塞。
- acquire(blocking=True,timeout=None)
- release()释放锁
例子(信号量)
from threading import Thread,currentThread,Semaphore
import time
def task():
sm.acquire()
print(f'{currentThread().name} 在执行')
time.sleep(3)
sm.release()
sm = Semaphore(4)
for i in range(12):
t = Thread(target=task)
t.start()
Thread-1 在执行
Thread-2 在执行
Thread-3 在执行
Thread-4 在执行
Thread-5 在执行
Thread-6 在执行
Thread-7 在执行
Thread-8 在执行
Thread-9 在执行
Thread-10 在执行
Thread-11 在执行
Thread-12 在执行
GIL(全局解释器锁)
介绍
问题一:为什么python在多线程中为什么不能实现真正的并行操作呢(即在多CPU中执行不同的线程)?
这就要提到python中大名鼎鼎GIL,那什么是GIL?
GIL:全局解释器锁 无论你启多少个线程,你有多少个CPU, Python在执行的时候只会的在同一时刻只允许一个线程(线程之间有竞争)拿到GIL在一个CPU上运行。
当线程遇到IO等待或到达者轮询时间的时候,CPU会切换,把CPU的时间片让给其他线程执行.
CPU切换需要消耗时间和资源,所以计算密集型的功能(比如加减乘除)不适合多线程,因为CPU线程切换太多,IO密集型比较适合多线程。
问题二:为什么要有GIL锁?
因为cpython自带的垃圾回收机制不是线程安全的,一旦变量的引用计数为0,就会被回收。此时GIL锁就是与万恶的垃圾回收机制相抗衡,不让它这么块就过来抢我们暂时无家可归的小可爱(变量)!!!
不过呢,GIL锁也导致了同一个进程同一时间只能运行一个线程,无法利用到多核优势。
分析:我们有四个任务需要处理,处理方式肯定是要玩出并发的效果
解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程
例子(任务)
io密集型
'''采用多进程计时情况'''
def work1():
x = 1+1
time.sleep(5)
if __name__ == '__main__':
t_list = []
start = time.time()
for i in range(4):
t = Process(target=work1)
t_list.append(t)
t.start()
for t in t_list:
t.join()
end = time.time()
print('多进程',end-start)
多进程 5.499674558639526
'''采用多线程计时情况'''
def work1():
x = 1+1
time.sleep(5)
if __name__ == '__main__':
t_list = []
start = time.time()
for i in range(4):
t = Thread(target=work1)
# t = Process(target=work1)
t_list.append(t)
t.start()
for t in t_list:
t.join()
end = time.time()
print('多线程',end-start)
多线程 5.004202604293823
小结:你发现了嘛!!!多线程的时间更短,相差0.5秒意味着什么!!!你明白吗???奶茶都可以绕地球两百圈了!!
分析:多线程为什么更快?
- 因为你看,多进程那么多个人同时去做,意味着卡机的时候都得哭着等。
- 那线程就不一样了,我们可聪明了,谁要等你,我直接切切切,所以同一段当然更快咯,略略略~
计算密集型
'''采用多进程计时情况'''
def work1():
res=0
for i in range(100000000): #1+8个0
res*=i
if __name__ == '__main__':
t_list = []
start = time.time()
for i in range(4):
t = Process(target=work1)
t_list.append(t)
t.start()
for t in t_list:
t.join()
end = time.time()
print('多进程',end-start)
多进程 18.062480211257935
'''采用多线程计时情况'''
def work1():
res=0
for i in range(100000000):
res*=i
if __name__ == '__main__':
t_list = []
start = time.time()
for i in range(4):
t = Thread(target=work1)
# t = Process(target=work1)
t_list.append(t)
t.start()
for t in t_list:
t.join()
end = time.time()
print('多线程',end-start)
多线程 33.27059483528137
小结:这次就算周杰伦说好不哭也救不了,15秒可以说是天壤之别了。。。
分析:多进程为什么更快?
- 因为你看,这次不卡机了,所以多进程那么多个人同时去做一件事,意味着一个时间里只需要完成一件事就好啦!(一个任务时间)
- 那多线程就不一样了,计算工作量大又耗时,但这是必经之路,这跟卡机不一样,因为那只有一个数据在动,而计算整个过程牵一发而动全身,所以一个时刻一条线程不断地切换,耍小聪明既会丢数据又没用!(多个任务时间)
总结
- IO密集型
- 各个线程都会都各种的等待,多线程比较适合
- 也可以采用多进程+协程
- 计算密集型
- 线程在计算时没有等待,这时候去切换,就是无用的切换,python不太适合开发这类功能
- 推荐使用多进程