python进阶之 GIL

1.为什么会出现GIL

解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。于是有了GIL这把超级大锁,在python中关于gil锁的注释是:一个防止多线程并发的执行机器码的一个互斥锁
cpython中GIL就是为了在同一时间内同一个进程中的多线程有且只有一个线程能得到GIL锁,从而来使用CPU做计算

得到gil锁要做什么就能保证线程间数据安全了么??
  gil叫做全局解释器锁,在每个python进程中都存在一个gil,因为每个python进程都有gil锁的存在,所以cpython能在多核多进程下实现并行。
  获得了gil之后,会获得python解释器的使用权,解释器将py文件解释成字节码,存在PythonCodeObject内存中或者硬盘中的pyc文件中。

  然后才有Python虚拟机将字节码转成机器码,去一条一条的执行,完成python文件的动作。对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行

2.什么是GIL

首先gil是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全
需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。 就好比C
++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。 Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。 然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。 所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。
那么CPython实现中的GIL又是什么呢?GIL全称Global Interpreter Lock为了避免误导,我们还是来看一下官方给出的解释:一个防止多线程并发执行机器码的一个Mutex(互斥锁)!!!(防止多个线程同时读写某一块内存区域)
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing 
Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe.
(However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
python中的GIL源码
static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */  这一行代码摘自 ceval.c —— CPython 2.7 解释器的源代码

3.python中的GIL应用

复制代码
GIL的作用
  
同一时间内同一个进程中的多线程有且只有一个线程来获取GIL锁,从而来访问CPU做计算
GIL的影响

  从上文的介绍和官方的定义来看,GIL无疑就是一把全局排他锁(互斥锁)。毫无疑问全局锁的存在会对多线程的效率有不小影响。甚至就几乎等于Python是个单线程的程序。
GIL对线程的影响  
import threading
def dead_loop():
    while True:
        pass

# 新起一个死循环线程
t = threading.Thread(target=dead_loop)
t.start()
# 主线程也进入死循环
dead_loop()

t.join()
   主线程起一个死循环,子线程起一个死循环。那cpu的占有率是多少那?100%?那得是单核的cpu,如果是我的2核,这个死循环只会吃掉我一个核的工作负荷,也就是只占用 50% CPU
  那如何能让他占满我的cpu那?不好意思线程做不到!!!原因就在GIL这把超级大锁!!

   GIL 的全程为 Global Interpreter Lock ,意即全局解释器锁。在 Python 语言的主流实现 CPython 中,GIL 是一个货真价实的全局线程锁,在解释器解释执行任何 Python 代码时,都需要先获得这把锁才行。
在遇到 I/O 操作时会释放这把锁。如果是纯计算的程序,没有 I/O 操作,解释器会每执行了 1500行机器码或者10ns就释放这把锁,让别的线程有机会执行(这个次数可以通过 sys.setcheckinterval 来调整)。
  
所以虽然 CPython 的线程库直接封装操作系统的原生线程,但 CPython 进程做为一个整体,同一时间只会有一个获得了 GIL 的线程在跑,其它的线程都处于等待状态等着 GIL 的释放。
这也就解释了我们上面的实验结果:虽然有两个死循环的线程,而且有两个物理 CPU 内核,但因为 GIL 的限制,两个线程只是做着分时切换,总的 CPU 占用率还略低于 50%。
复制代码

4.多线程执行顺序

复制代码
多线程环境中,python虚拟机按照以下方式执行:
  1.设置GIL
  2.切换到一个线程去执行
  3.运行代码,这里有两种机制:
  4.指定数量的字节码指令(100个)
  5.固定时间15ms线程主动让出控制
  6.把线程设置为睡眠状态
  7.解锁GIL
  8.再次重复以上步骤 
复制代码

5.如何去除GIL限制

这个问题也可以说成gil锁带来了什么问题?
  1.由于gil锁的存在,导致单进程的多线程不能利用多核,浪费资源
  2.在单核处理三个io任务和多核处理三个io任务的时间基本一致,虽然三核心会快一点,但是进程见得切换花销会比线程的花销大,所以时间上的开销基本一致

虽然 CPython 的线程库封装了操作系统的原生线程,但却因为 GIL 的存在导致多线程不能利用多个 CPU 内核的计算能力。
好在现在 Python 有了multiprocessing模块, C 语言扩展机制 和 ctypes)

6.思考:有了GIL锁,线程中还需要锁么?

复制代码
有必要
理论上有了GIL锁会保证同一时间内只有一个线程执行cpu指令,但是运行多线程不加锁的话,会产生数据不安全的情况
有下列代码:
  当tt里面的第一个线程在执行的时候,假如说执行到count =count+1
  但是还没来得及把count放回去的时候,cpu就切换到下一个线程执行,此时的count并没有
  修改,还是0 ,于是就将coun加1,count=1,在切换到上一个线程继续执行,cpu寄存器里面
  存放的上一个线程的状态是要将count+1的结果存回给count,所以在两个线程运行的时候只加了一次
  造成count值不对,也就造成了数据不安全的问题。所以要加上锁
  这种情况主要是由于是cpu切换线程造成的
'''
coun = 0
def func():
count +=1
for i in range(2):
tt = Thread(target=func)
tt.start()
'''
线程中不安全现象:
  1.对全局变量进行修改
  2.对某个值进行+=,-=,*=,/+,%=等等操作
  产生的原因是cpu切换线程造成的

为什么某个值进行+=,-=,*=,/+,%=在cpu切换的时候造成数据不安全的现象?
  因为在count +=1等操作,在cpu的机器码中执行是可再分的,分成两步执行,count +1,count=count+1
  所以在 cpu切换线程的时候,有可能出现两个线程分别对一个数据进行修改,而返回的时候只是返回一个
  但是Python中常用的数据类型,list dict tuple set等修改或增加的方法都具有原子性(不是增加就是修改)
  
具有原子性也是在cpu底层机器码中体现出来的

注意:
  队列(queue)的原子性比以上的数据类型更强,因为队列的queue.get()获取不到数据的时候回阻塞住,而列表的list.pop()在没有数据时会报错
复制代码

 7gil锁和lock的区别

GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock
自己加锁要加到共享数据的地方,不宜范围太大,否则会影响性能,造成cpu的切换增加

上面代码既然有了lock,不sleep也可以,只是模仿阻塞!

 

 

 

 

 

 other

1.py在同一时刻只能跑一个线程,这样在跑多线程的情况下,只有当线程获取到全局解释器锁后才能运行,而全局解释器锁只有一个,
 因此即使在多核的情况下也只能发挥出单核的功能。(如果有4个cpu,可能会访问不同的cpu,但是同一时间只能访问一个)
2.GIL的特性,也就导致了python不能充分利用多核cpu。而 对面向I/O的(会调用内建操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其他线程在这个线程等待I/O的时候运行。
如果线程 并为使用很多I/O操作,它会在自己的时间片一直占用处理器和GIL。这也就是所说的:I/O密集型python程序比计算密集型的程序更能充分利用多线 程的好处。
  总之,不要使用python多线程,使用python多进程进行并发编程,就不会有GIL这种问题存在,并且也能充分利用多核cpu。

参考链接

    http://python.jobbole.com/87743/

 

posted @   thep0st  阅读(47)  评论(0编辑  收藏  举报
(评论功能已被禁用)
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· AI与.NET技术实操系列(六):基于图像分类模型对图像进行分类
点击右上角即可分享
微信分享提示