你是否真的了解全局解析锁(GIL)

关于我
一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。
Github:https://github.com/hylinux1024
微信公众号:终身开发者(angrycode)

0x00 什么是全局解析锁(GIL)

A global interpreter lock (GIL) is a mechanism used in computer-language interpreters to synchronize the execution of threads so that only one native thread can execute at a time. --引用自wikipedia

从上面的定义可以看出,GIL是计算机语言解析器用于同步线程执行的一种同步锁机制。很多编程语言都有GIL,例如PythonRuby

0x01 为什么会有GIL

Python作为一种面向对象的动态类型编程语言,开发者编写的代码是通过解析器顺序解析执行的。
大多数人目前使用的Python解析器是CPython提供的,而CPython的解析器是使用引用计数来进行内存管理,为了对多线程安全的支持,引用了global intepreter lock,只有获取到GIL的线程才能执行。如果没有这个锁,在多线程编码中即使是简单的操作也会引起共享变量被多个线程同时修改的问题。例如有两个线程同时对同一个对象进行引用时,这两个线程都会将变量的引用计数从0增加为1,明显这是不正确的。
可以通过sys模块获取一个变量的引用计数

>>> import sys
>>> a = []
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3

sys.getrefcount()方法中的参数对a的引用也会引起计数的增加。

是否可以对每个变量都分别使用锁来同步呢?

如果有多个锁的话,线程同步时就容易出现死锁,而且编程的复杂度也会上升。当全局只有一个锁时,所有线程都在竞争一把锁,就不会出现相互等待对方锁的情况,编码的实现也更简单。此外只有一把锁时对单线程的影响其实并不是很大。

0x02 可以移除GIL吗?

Python核心开发团队以及Python社区的技术专家对移除GIL也做过多次尝试,然而最后都没有令各方满意的方案。

内存管理技术除了引用计数外,一些编程语言为了避免引用全局解析锁,内存管理就使用垃圾回收机制。

当然这也意味着这些使用垃圾回收机制的语言就必须提升其它方面的性能(例如JIT编译),来弥补单线程程序的执行性能的损失。
对于Python的来说,选择了引用计数作为内存管理。一方面保证了单线程程序执行的性能,另一方面GIL使得编码也更容易实现。
Python中很多特性是通过C库来实现的,而在C库中要保证线程安全的话也是依赖于GIL

所以当有人成功移除了GIL之后,Python的程序并没有变得更快,因为大多数人使用的都是单线程场景。

0x03 对多线程程序的影响

首先来GILIO密集型程序和CPU密集型程序的的区别。
像文件读写、网络请求、数据库访问等操作都是IO密集型的,它们的特点需要等待IO操作的时间,然后才进行下一步操作;而像数学计算、图片处理、矩阵运算等操作则是CPU密集型的,它们的特点是需要大量CPU算力来支持

对于IO密集型操作,当前拥有锁的线程会先释放锁,然后执行IO操作,最后再获取锁。线程在释放锁时会把当前线程状态存在一个全局变量PThreadState的数据结构中,当线程获取到锁之后恢复之前的线程状态

用文字描述执行流程

保存当前线程的状态到一个全局变量中
释放GIL
... 执行IO操作 ...
获取GIL
从全局变量中恢复之前的线程状态

下面这段代码是测试单线程执行500万次消耗的时间

import time

COUNT = 50000000

def countdown(n):
    while n > 0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds -', end - start)

# 执行结果
# Time taken in seconds - 2.44541597366333

在我的8核的macbook上跑大约是2.4秒,然后再看一个多线程版本

import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n > 0:
        n -= 1

t1 = Thread(target=countdown, args=(COUNT // 2,))
t2 = Thread(target=countdown, args=(COUNT // 2,))

start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()

print('Time taken in seconds -', end - start)

# 执行结果
# Time taken in seconds - 2.4634649753570557

上文代码每个线程都执行250万次,如果线程是并发的,执行时间应该是上面单线程版本的一半时间左右,然而在我电脑中执行时间大约为2.5秒!
多线程不但没有更高效率,反而还更耗时了。这个例子就说明Python中的线程是顺序执行的,只有获取到锁的线程可以获取解析器的执行时间。多线程执行多出来的那点时间就是获取锁和释放锁消耗的时间。

那如何实现高并发呢?

答案是使用多进程。前面的文章有介绍多进程的使用

from multiprocessing import Pool
import time

COUNT = 50000000

def countdown(n):
    while n > 0:
        n -= 1

if __name__ == '__main__':
    pool = Pool(processes=2)
    start = time.time()
    r1 = pool.apply_async(countdown, [COUNT // 2])
    r2 = pool.apply_async(countdown, [COUNT // 2])
    pool.close()
    pool.join()
    end = time.time()
    print('Time taken in seconds -', end - start)

# 执行结果
# Time taken in seconds - 1.2389559745788574

使用多进程,每个进程运行250万次,大约消耗1.2秒的时间。差不多是上面线程版本的一半时间。

当然还可以使用其它Python解析器,例如JythonIronPythonPyPy

既然每个线程执行前都要获取锁,那么有一个线程获取到锁一直占用不释放,怎么办?

IO密集型的程序会主动释放锁,但对于CPU密集型的程序或IO密集型和CPU混合的程序,解析器将会如何工作呢?
早期的做法是Python会执行100条指令后就强制线程释放GIL让其它线程有可执行的机会。
可以通过以下获取到这个配置

>>> import sys
>>> sys.getcheckinterval()
100

在我的电脑中还打印了下面的输出警告

Warning (from warnings module):
  File "__main__", line 1
DeprecationWarning: sys.getcheckinterval() and sys.setcheckinterval() are deprecated.  Use sys.getswitchinterval() instead.

意思是sys.getcheckinterval()方法已经废弃,应该使用sys.getswitchinterval()方法。
因为传统的实现中每解析100指令的就强制线程释放锁的做法,会导致CPU密集型的线程会一直占用GILIO密集型的线程会一直得不到解析的问题。于是新的线程切换方案就被提出来了

>>> sys.getswitchinterval()
0.005

这个方法返回0.05秒,意思是每个线程执行0.05秒后就释放GIL,用于线程的切换。

0x04 总结

CPython解析器的实现由于global interpreter lock(全局解释锁)的存在,任何时刻都只有一个线程能执行Pythonbytecode(字节码)。
常见的内存管理方案有引用计数和垃圾回收,Python选择了前者,这保证了单线程的执行效率,同时对编码实现也更加简单。想要移除GIL是不容易的,即使成功将GIL去除,对Python的来说是牺牲了单线程的执行效率。
PythonGILIO密集型程序可以较好的支持多线程并发,然而对CPU密集型程序来说就要使用多进程或使用其它不使用GIL的解析器。
目前最新的解析器实现中线程每执行0.05秒就会强制释放GIL,进行线程的切换。

0x05 为了看懂GIL我阅读了下面这些资料

原文地址:https://www.cnblogs.com/angrycode/p/11396808.html

时间: 2024-11-08 21:30:59

你是否真的了解全局解析锁(GIL)的相关文章

python 什么是全局解释器锁GIL

什么是全局解释器锁GIL Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行.对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行. 在多线程环境中,Python 虚拟机按以下方式执行: 1. 设置GIL2. 切换到一个线程去运行3. 运行:    a. 指定数量的字节码指令,或者 b.

python 全局解释锁GIL

Python的全局解释器锁GIL用于保护python解释器,使得任意时刻,只有一个线程在解释器中运行.从而保证线程安全 在多线程环境中,Python 虚拟机按以下方式执行: 1. 设置GIL2. 切换到一个线程去运行3. 运行:    a. 指定数量的字节码指令,或者 b. 线程主动让出控制(可以调用time.sleep(0))4. 把线程设置为睡眠状态5. 解锁GIL6. 再次重复以上所有步骤 由上可知,至少有两种情况python会解锁GIL,做线程切换:一是一但有IO操作时:线程连续执行了一

全局解释器锁--GIL

参考博客:https://www.cnblogs.com/mindsbook/archive/2009/10/15/thread-safety-and-GIL.html    https://www.cnblogs.com/MnCu8261/p/6357633.html http://python.jobbole.com/87743/ 一.前言 在多核cpu的背景下,基于多线程以充分利用硬件资源的编程方式也不断发展,也就是在同一时间,可以运行多个任务.但是Cpython中由于GIL的存在,导致同

Python 36 GIL全局解释器锁

一:GIL全局解释器锁介绍 在CPython中,全局解释器锁(或GIL)是一个互斥锁, 它阻止多个本机线程同时执行Python字节码.译文:之所以需要这个锁, 主要是因为CPython的内存管理不是线程安全的.(然而,由于GIL的存在, 其他特性已经变得依赖于它所执行的保证.) 1. 什么是GIL全局解释器锁GIL本质就是一把互斥锁,相当于执行权限,每个进程内都会存在一把GIL,同一进程内的多个线程必须抢到GIL之后才能使用Cpython解释器来执行自己的代码,即同一进程下的多个线程无法实现并行

117 GIL全局解释器锁

一.GIL全局解释器锁 cpython中自带的GIL全局解释器,GIL本身就是一把互斥锁 重点:因为有了GIL全局解释器锁,导致了在同一进程的同一时刻只有一个线程在执行,无法利用多核优势 其实就算我们在程序中写了一个线程的并行操作,实际上GIL会因为垃圾回收机制的问题,操作系统调度的问题,会把并行的线程还是变成了串行,这正是这个GIL全局解释器锁导致了同一进程的同一时刻只有一个线程在运行, Python代码的执行由Python虚拟机(也叫解释器主循环)来控制.Python在设计之初就考虑到要在主

GIL全局解释器锁及协程

GIL全局解释器锁 1.什么是GIL全局解释器锁 GIL本质是一把互斥锁,相当于执行权限,每个进程内都会存在一把GIL同一进程内的多线程,必须抢到GIL之后才能使用Cpython解释器来执行自己的代码,即同一进程下的多个线程无法实现并行,但可以实现并发 Cpython解释器下想实现并行可以开启多个进程 2.为何要有GIL 因为Cpython解释器的垃圾回收机制不是线程安全的,保证了数据的安全 3.GIL全局解释器的优缺点 优点:保证了数据安全 缺点:单个进程下开启多个线程只能实现并发不能实现并行

python开发线程:线程&守护线程&全局解释器锁

一 threading模块介绍 multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍 官网链接:https://docs.python.org/3/library/threading.html?highlight=threading# 二 开启线程的两种方式 #方式一 from threading import Thread import time def sayhi(name): time.sleep(2) print('%s

33、线程与全局解释器锁(GIL)

之前我们学了很多进程间的通信,多进程并发等等,今天我们来学习线程,线程和进程是什么关系,进程和线程有什么相同而又有什么不同今天就来揭晓这个答案. 一.线程概论 1.何为线程 每个进程有一个地址空间,而且默认就有一个控制线程.如果把一个进程比喻为一个车间的工作过程那么线程就是车间里的一个一个流水线. 进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位. 多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间(

GIL(全局解释器锁)

GIL(全局解释器锁) 我们知道多进程(mutilprocess) 和 多线程(threading)的目的是用来被多颗CPU进行访问, 提高程序的执行效率. 但是在python内部存在一种机制(GIL),在多线程 时同一时刻只允许一个线程来访问CPU. GIL 并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念.就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码.有名的编译器例如GCC,INTEL C++,Visual C++等.