GIL全局解释器锁
定义
基于Cpython研究:
1.GIL本质上是一个互斥锁
2.GIL是为了阻止同一个进程内多个线程同执行(并行)
单个进程下的多个线程无法实现并行,但能实现并发
3.这把锁主要是因为Cpython的内存管理不是线程安全的
内存管理:垃圾回收机制
GIL的存在主要就是保住了线程的安全
'''遇到IO操作会进行阻塞,所以多线程时线程阻塞第二个线程就继续循环'''
from threading import Thread
import time
num = 100
def task():
global num
num2 = num
time.sleep(1) # 加上延时操作会使打印都为99
num = num2 -1
print(num)
for line in range(100):
t = Thread(target=task)
t.start()
多线程的作用
程序作用至计算及IO两种形式,任务分配情况
假设四个任务每个任务需要10s,在单核多核情况下代码情况.
1.计算密集型
单核
开启进程
4个进程共: 40s,消耗资源过大
开启线程
4个进程共: 40s,消耗资源远小于进程
多核
开启进程
并行执行,效率比较高,4个进程需要10s
开启线程
并发执行,效率比较低,4个进程40s
总结
计算密集型中,多核操作多进程比多线程效率高,因为线程不能实现并行,
而进程可以实现多进程操作大大节省时间.
2.IO密集型
单核
开启进程
4个进程共: 40s,消耗资源过大
开启线程
4个进程共: 40s,消耗资源远小于进程
多核
开启进程
并行执行,效率小于线程,因为遇到IO会立马切换CPU的执行权限
4个进程: 40s + 开启进程消耗的额外时间
开启线程
并发执行,4个进程 40s
总结
在IO密集型情况下,因为碰到IO操作会使CPU进行切换+保存的操作,而线程又比进程占用的资源切换的速度小,再加上开启进程消耗的时间,多核上多线程反而比多进程效率要高(IO密集型的情况下)
3.代码实现
计算密集型,(多进程明显比多线程要快)
from threading import Thread
from multiprocessing import Process
import time
# 计算密集型
def work1():
num = 0
for l in range(100000000):
num += 1
# IO密集型
def work2():
time.sleep(1)
if __name__ == '__main__':
# 开始时间
start_time = time.time()
list1 = []
for l in range(4):
# 测试计算密集型
# p = Process(target=work1) # 进程 程序执行时间为21.6076238155365
p = Thread(target=work1) # 线程 程序执行时间为39.6529426574707
list1.append(p)
p.start()
for p in list1:
p.join()
end_start = time.time()
print(f'程序执行时间为{end_start-start_time}')
# print(os.cpu_count()) # 查看cpu的个数 4
IO密集型 (多线程比多进程效率高)
from threading import Thread
from multiprocessing import Process
import time
# 计算密集型
def work1():
num = 0
for l in range(100000000):
num += 1
# IO密集型
def work2():
time.sleep(1)
if __name__ == '__main__':
# 开始时间
start_time = time.time()
list1 = []
for l in range(10):
# 测试计算密集型
p = Process(target=work2) # 进程 程序执行时间为6.449297666549683
# p = Thread(target=work2) # 线程 程序执行时间为1.00590920448303224707
list1.append(p)
p.start()
for p in list1:
p.join()
end_start = time.time()
print(f'程序执行时间为{end_start-start_time}')
4.总结
在计算密集型的情况下,使用多进程
在IO密集型的情况下,使用多线程
如果要高效执行多个IO密集型的程序: 使用多进程 + 多线程
死锁现象
进程锁的开锁与解锁进程因遇到IO操作切换进程,导致各自被占用,导致死锁.
可以通过给锁上锁来保证每次进入锁定状态的代码出去之后才能进行第二个进程,并发变串行.
from threading import Lock,Thread,current_thread
import time
mutex_a = Lock()
mutex_b = Lock()
class MyThread(Thread):
# 线程执行任务
def run(self):
self.func1()
self.func2()
def func1(self):
mutex_a.acquire() # a加锁
print(f'用户{current_thread().name}抢到了锁a')
# 与 print(f'用户{self.name}抢到了锁a') 一样
mutex_b.acquire() # b加锁
print(f'用户{current_thread().name}抢到了锁b')
mutex_b.release() # b 解锁
print(f'{current_thread().name}解放锁b')
mutex_a.release() # a 解锁
print(f'{current_thread().name}解放锁a')
def func2(self):
mutex_b.acquire() # b 加锁
print(f'用户{current_thread().name}抢到了锁b')
# IO操作
time.sleep(1)
mutex_a.acquire() # a 加锁
print(f'用户{current_thread().name}抢到了锁a')
mutex_a.release() # a 解锁
print(f'{current_thread().name}解放锁a')
mutex_b.release() # b 解锁
print(f'{current_thread().name}解放锁b')
for l in range(10):
t = MyThread()
t.start()
'''
用户Thread-1抢到了锁a
用户Thread-1抢到了锁b
Thread-1解放锁b
Thread-1解放锁a
用户Thread-1抢到了锁b
用户Thread-2抢到了锁a'''
用户Thread-1依次加锁解锁 a b,
运行func1结束进入func2:抢到了锁b,然后遇到IO暂停操作
用户Thread-2开始启动,func1 中抢到了锁a,
但锁b 被用户Thread-1所持有,
程序暂停
锁不能乱用
递归锁
解决死锁问题# RLocK
可以提供给多人使用,但是第一个使用时,会对该锁做引用计数,只用引用计数为0时,才会让另一个人继续使用
'''用于解决死锁现象'''
# RLocK
from threading import RLock,Thread,current_thread
import time
mutex_a = mutex_b = RLock()
class MyThread(Thread):
# 线程执行任务
def run(self):
self.func1()
self.func2()
def func1(self):
mutex_a.acquire() # a加锁
print(f'用户{current_thread().name}抢到了锁a')
# 与 print(f'用户{self.name}抢到了锁a') 一样
mutex_b.acquire() # b加锁
print(f'用户{current_thread().name}抢到了锁b')
mutex_b.release() # b 解锁
print(f'{current_thread().name}解放锁b')
mutex_a.release() # a 解锁
print(f'{current_thread().name}解放锁a')
def func2(self):
mutex_b.acquire() # b 加锁
print(f'用户{current_thread().name}抢到了锁b')
# IO操作
time.sleep(1)
mutex_a.acquire() # a 加锁
print(f'用户{current_thread().name}抢到了锁a')
mutex_a.release() # a 解锁
print(f'{current_thread().name}解放锁a')
mutex_b.release() # b 解锁
print(f'{current_thread().name}解放锁b')
for l in range(10):
t = MyThread()
t.start()
信号量
semaphore
是一个内置的计数器,用于控制进入指定数量的锁。
每当调用acquire()时,内置计数器-1
每当调用release()时,内置计数器+1
计数器不能小于0,当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
应用场景
比如说在读写文件的时候,一般只能只有一个线程在写,而读可以有多个线程同时进行,如果需要限制同时读文件的线程个数,这时候就可以用到信号量了(如果用互斥锁,就是限制同一时刻只能有一个线程读取文件)。
又比如在做爬虫的时候,有时候爬取速度太快了,会导致被网站禁止,所以这个时候就需要控制爬虫爬取网站的频率。
from threading import Semaphore
from threading import current_thread
from threading import Thread,Lock
import time
mutex = Lock()
sm = Semaphore(5) # 设置进入锁的数量
def task():
# mutex.acquire() # 一次进入1个(锁住)
sm.acquire() # 一次5个进入锁
print(f'{current_thread().name}抢到执行权限')
time.sleep(1) # 进行延时操作
sm.release()
# mutex.release() # 一次进入1个(解锁)
for l in range(20):
t = Thread(target=task)
t.start()
线程队列
1.基本使用
queue.Queue()
线程队列,线程之间使用,与进程的差不多
import queue
q = queue.Queue() # 默认创建多个线程
q.put(1)
q.put(2)
q.put(3)
print(q.get()) # 1
2.先进先出FIFO
普通的使用,默认就是先进先出
3.后进先出LIFO
queue.LifoQueue()
q = queue.LifoQueue()
q.put(1)
q.put(2)
q.put(3)
print(q.get()) # 3
4.优先级队列
根据参数内数字的大小进行分级,数字值越小,优先级越高
传参时将元组传入,以数字或ascll码进行排序
import queue
q = queue.PriorityQueue()
q.put((3,'abc'))
q.put((10,'js'))
print(q.get())
'''abc'''