赞
踩
GIL锁的英文全称: Global Interpreter Lock, 中文意思是: 全局解释器锁.
在 CPython 解释器中, GIL 是一把互斥锁, 用来阻止同一个进程下多个线程的同时执行.
* GIL 不是 Python 的特点, 而是 CPython 解释器的特点.
由于全局解释器锁的存在, 在同一时间内, python解释器只能运行一个线程的代码,
每个线程在调用 cpython 解释器之前, 需要先抢到 GIL 锁, 然后才能运行.
这大大影响了python多线程的性能, 而这个解释器锁由于历史原因, 现在几乎无法消除.
* 1. 设计者为了规避类似内存管理这样的复杂的竞争风险问题(race condition).
* 2. CPython大量使用C语言库,但大部分C语言库都不是原生"线程安全"的(线程安全会降低性能和增加复杂度)。
CPython中, 使用了引用计数, 当引用计数为0时,CPython解释器会自动释放内存.
如果有多个线程同时引用了一个变量,就会造成引用计数的竞争条件.
如果发生了这种情况,可能会导致泄露的内存永远不会被释放,
更严重的是对象的引用仍然存在的情况下错误地释放内存,导致Python程序崩溃或带来各种诡异的问题.
因此引用计数变量需要在多个线程同时增加或减少时从竞争条件中得到保护.
竞争条件(race condition): 多个进程并发访问和操作同一数据, 出现竞争的情况,
执行结果与访问的特定顺序有关.
假设1: 两个进程P1和P2共享了变量a.
在某一执行时刻, P1更新a为1, 在另一时刻,
P2更新a为2. 因此两个任务竞争地写变量a.
竞争的“失败者”(最后更新的进程)决定了变量a的最终值.
假设2: 两个进程P1和P2共享了变量a.
在某一执行时刻, P1更新a的计数引用为0,开始回收变量, 释放内存.
P2使用这个变量, 这个变量没有了, 报错.
可以看一看下面这张图, 就是一个GIL在Python程序中的工作示例.
其中, 线程1、2、3轮流执行, 每一个线程在执行, 都会先获取GIL锁, 以阻止别的线程执行.
CPython 中还有另一个机制, 叫做 check_interval, 意思是 CPython 解释器会去轮询检查线程 GIL 的锁住情况.
每隔一段时间,Python 解释器就会强制当前线程去释放 GIL,这样别的线程才能有执行的机会。
释放GIL锁的情况:
* 1. 任务没有执行完成.
* 2. 程执行遇到I/O操作, 会释放GIL, 以允许别的线程开始利用资源.
* 3. 给定时间(python3 15毫秒)没没有执行完或执行了1000 个 bytecodes(字节码, 早期100) 会释放GIL.
主动释放锁的疑问?
线程1没有执行完, 释放锁给线程2执行, 那么现在共有变量的问题好像又出现了...
GIL的设计, 主要是为了方便 CPython 解释器层面的编写者,而不是 Python 应用层面的程序员.
作为 Python 的使用者,我们还是需要 lock 等工具,来确保线程安全.
Gil锁 :保证同一时刻只有一个线程能使用到cpu
互斥锁 : 多线程时,保证修改共享数据时进行有序的修改,不会产生数据修改混乱。
GIL的设计,主要是为了方便 CPython 解释器层面的编写者,而不是 Python 应用层面的程序员。
作为 Python 的使用者,我们还是需要 lock 等工具,来确保线程安全。
启动 10 个线程 去修改同一个变量的值.
在线程运行的函数中加入 time.sleep(0.1).
from threading import Thread import time # 定义一个全局 number = 10 def func(): # number为全局变量 global number # 获取number的值 tem = number # 延时会让当前进程进入到阻塞态 time.sleep(0.1) # 修改number的值 number = tem - 1 # 定义一个列表 thread_list = [] # 获取时间戳 now_time = time.time() for i in range(10): # 创建线程对象 thread = Thread(target=func) # 启动线程 thread.start() thread_list.append(thread) # 让所有线程结束在执行主进程 for i in thread_list: i.join() print(f'程序运行使用时间:{time.time() - now_time}秒') print(number)
终端显示:
程序运行使用时间:0.11956381797790527秒
9
不是说同一时间只有一个显示在执行吗, 为什么是9, 而不是0?
第一个线程获取到获得的 GIL 锁运行线程绑定的函数, 其他的线程等待GIL锁的释放才能运行...
函数中获取number的值为10,
time.sleep() 会让线程进入阻塞状态, 这个时候会将GIL释放. (关键点)
其他的线程就会抢锁执行, 都会在延时操作时释放掉锁,
那么所有的的线程拿number的值为10.
延时到期后, 第一个线程再次获取GIL锁, 将number的值设置为9, 程序结束, 释放GIL锁.
其他的线程相继如此, 最后所有线程的中number值都设置为9.
在线程运行的函数中 删除 time.sleep(0.1).
from threading import Thread import time # 定义一个全局 number = 10 def func(): # number为全局变量 global number # 获取number的值 tem = number # 修改number的值 number = tem - 1 # 定义一个列表 thread_list = [] # 获取时间戳 now_time = time.time() for i in range(10): # 创建线程对象 thread = Thread(target=func) # 启动线程 thread.start() thread_list.append(thread) # 让所有线程结束在执行主进程 for i in thread_list: i.join() print(f'程序运行使用时间:{time.time() - now_time}秒') print(number)
终端显示:
程序运行使用时间:0.001999378204345703秒
0
同一时间只有一个线程在执行(线程执行函数速度快, 直接执行完毕,给定时间与字节码没有超出, GIL锁没有中途释放).
得到的number是0, 结果是我们想要的.
添加time.sleep(0.1), 再加入的互斥锁.
from threading import Thread, Lock import time # 定义一个全局 number = 10 def func(mutex): # number为全局变量 global number # 上锁 mutex.acquire() # 获取number的值 tem = number # 延时会让当前进程进入到阻塞态 time.sleep(0.1) # 修改number的值 number = tem - 1 # 释放锁 mutex.release() # 定义一个列表 thread_list = [] # 获取时间戳 now_time = time.time() # 生成互斥锁对象 mutex = Lock() for i in range(10): # 创建线程对象 thread = Thread(target=func, args=(mutex, )) # 启动线程 thread.start() thread_list.append(thread) # 让所有线程结束在执行主进程 for i in thread_list: i.join() print(f'程序运行使用时间:{time.time() - now_time}秒') print(number)
终端显示:
程序运行使用时间:1.094970703125秒
0
第一个线程在运行时, 先获取 GIL 锁, 然后获取到互斥锁,
运行到 time.sleep(0.1) 线程变成"阻塞态", 释放 GIL 锁,
第二个线程拿到了GIL锁执行到mutex.acquire(), 没有锁, 线程进入阻塞态, 释放 GIL 锁,
其他的线程依旧如此, 在同一个位置等待互斥锁的释放.
当第一个线程 time.sleep(0.1) 运行结束之后(IO 操作结束), 重新进入就绪态, 获取到GIL 锁,
执行完后释放掉互斥锁, 在释放掉GIL锁.
程序由并发变成了串行, 才能保证数据的安全.
由于 GIL 的存在, 即使是多个线程处理任务, 但是最终也只有一个线程在工作, 那么是不是多线程真的一点用处都没有呢?
对于需要执行的任务来说, 分为两种: 计算密集型、IO 密集型
计算密集型(CPU-Intensive)
1、特点:要进行大量的计算,消耗CPU资源。比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。
2、计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,
CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
3、计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。
Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
假如一个 计算密集型 的任务需要 10s 的执行时间, 总共有 4 个这样的任务
在 4核及以上 的情况下:
多进程: 需要开启 4 个进程, 但是 4 个 CPU 并行, 最终只需要消耗 10s 多一点的时间(Python的效果会差一些)
多线程: 只需要开1 个进程, 这个进程开启 4 个线程, 开启线程所消耗的资源很少,
但是由于最终执行是只有一个 CPU 可以工作, 所以最终消耗 40s 多的时间
import time
def foo():
res = 1.1
# 模拟计算密集型
for i in range(1, 100000000):
res *= i
start_time = time.time()
foo()
print(time.time() - start_time) # 2.395080089569092
# 计算密集型使用多进程 from multiprocessing import Process import time def foo(): res = 1.1 # 模拟计算密集型 for i in range(1, 100000000): res *= i if __name__ == '__main__': # 定义一个列表存放进程对象 l = [] # 获取当前时间戳 start_time = time.time() # 开始14个子进程, for i in range(12): p = Process(target=foo) p.start() l.append(p) # 主进程等待子进程结束 for p in l: p.join() # 查看运行时间 print(time.time() - start_time) # 4.787352561950684 每开一个子进程, 时间就久一点 """ 自己电脑是 6大核加8小核 14个核心 开启的子进程数 花费的时间 14 7.39286470413208 12 6.670255661010742 10 5.984007358551025 ... 6 4.481975317001343 5 3.92647385597229 4 3.625216245651245 3 3.1252694129943848 2 2.837176561355591 1 2.513249158859253 """
# 计算密集型使用多线程 from threading import Thread import time def func(): res = 1.1 # 模拟计算密集型 for i in range(1, 100000000): res *= i # 定义一个列表存放进程对象 l = [] # 获取当前时间戳 start_time = time.time() # 开始6个子进程, for i in range(6): t = Thread(target=func) t.start() l.append(t) # 主进程等待子进程结束 for t in l: t.join() # 查看运行时间 print(time.time() - start_time) # 14.323595523834229
IO密集型(IO-Intensive)
1、涉及到网络、磁盘IO的任务都是IO密集型任务。
2、特点:CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。
3、对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
4、IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,
因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。
对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
假如是多个 IO密集型的任务–CPU 大多数时间是处于闲置状态, 频繁的切换.
多进程: 进程进行切换需要消耗大量资源.
多线程: 线程进行切换并不需要消耗大量资源.
# io密集型使用多进程 from multiprocessing import Process import time def func(): # io操作 time.sleep(2) if __name__ == '__main__': l = [] start_time = time.time() for i in range(1000): p = Process(target=func) p.start() l.append(p) for p in l: p.join() print(time.time() - start_time) # 10.228676080703735
# io密集型使用多线程 from threading import Thread import time # 定义线程调用的函数 def func(): # 模拟io time.sleep(2) t_list = [] start_time = time.time() # 开启一千个子线程 for i in range(1000): t = Thread(target=func) t.start() t_list.append(t) # 等待所有线程结束 for p in t_list: p.join() print(time.time() - start_time) # 2.0857489109039307
对于多进程、多线程其实都有其各自的应用场景;
对于普通程序猿来说, 开发的软件大多是 IO 密集型(WEB开发), 所以即使存在 GIL 锁, 开启多线程也是有优势的
并且, 可以同时开启多进程与多线程, 同时兼并二者的优点, 至于在何时切换成线程还是进程, 则有专门的模块.
文章的段落全是代码块包裹的, 留言0是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言1是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言2是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言3是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言4是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言5是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言6是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言7是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言8是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言9是为了避免文章提示质量低.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。