GIL全局变量锁

一、GIL介绍

GIL (Global Interperter Lock) 称作全局解释器锁;在解析多线程时,保证每个时刻是由一个线程占用CPU,即使是多核CPU,即“并发”而非“并行”。

它不是Python的语言特性,是Cpython解释器的一个概念,GIL只在Cpython存在;

在CPython中,每一个Python线程执行前都需要去获得GIL锁 ,获得该锁的线程才可以执行,没有获得的只能等待 ,当具有GIL锁的线程运行完成后,其他等待的线程就会去争夺GIL锁。

 

二、为什么需要GIL

 同一个进程中的线程可以共享内存空间,引出了线程不安全的因素,容易导致程序逻辑错误,常用的方式就是使用锁 或者 信号量等机制来限制公共机制的使用。

在Cpython中,使用引用计数来管理内存,在Python中,一切都是对象,引用计数就是指向对象的指针数,当这个数字变成0,则会进行垃圾回收,自动释放内存。

举例:如果有两个线程,同时引用a,这样就有可能a的引用计数只增加了一次,这就会导致内存被污染了,因为当第一个线程结束的时候,a的引用计数减去1,而如果这时候a的引用计数刚好为0的时候,a所引用的列表就会被释放,这时候另一个线程去访问a的时候,就找不到有效的内存了.

 

三、GIL的历史背景

Guido van Rossum(吉多·范罗苏姆)创建python时就只考虑到单核cpu,解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁, 于是有了GIL这把超级大锁。

由于大量的程序开发者接收了这套机制,现在代码量越来越多,已经不容易通过修改c代码去解决这个问题。而且每次从 CPython 中去除 GIL 的尝试会耗费单线程程序太多性能,尽管去除 GIL 会带来多线程程序性能的提升,但仍是不值得的。(前者是Guido最为关切的, 也是不去除 GIL 最重要的原因, 一个简单的尝试是在1999年, 最终的结果是导致单线程的程序速度下降了几乎2倍.)

 

四、GIL对程序的影响

1. 因为GIL的存在,在Python中同一时刻有且只有一个线程会执行,线程是CPU调度和分派的基本单位,所以无法利用多核 CPU;

2. GIL对于IO密集型的任务影响很小(网络数据收发,文件读写等),多线程适合用来做IO密集型的程序,Python解释在程序执行IO等待时,会释放GIL锁,让其它线程执行,提高Python程序的执行效率;

3. GIL对计算密集型的任务影响较大,因为线程切换的过程中需要保存和调取上下文数据 也需要消耗资源,多线程执行甚至可能比单线程执行耗时更长。

 

五、如何绕过GIL

1. 更换Jpython或者Pypy解释器。但是开发者少,支持模块少,开发效率低,不建议这个方法;

2. 用多进程 代替 多线程,多进程可以运行在CPU的不同核上,实现真正的并行;

3. 使用C/C++来实现这部分代码,并生成对应的so或dll文件,再通过Python的ctypes将其调用起来 ,Python中很多对计算性能有较高要求的库都采用了这种方式,如Numpy、Pandas等等;

 

六、python2和python3中的GIL区别

Python3.2中对GIL进行了改进,改进后的GIL相比旧GIL(Python2.x)会让线程对GIL的竞争更加平稳,主要是check_interval机制的改进。

python2中,基于ticker来决定是否释放GIL,默认100个ticker,大约对应1000个bytecodes(每个bytecode是一个代码原子操作);并且释放完后,释放的线程依旧会参与GIL争夺,这就使得某线程一释放GIL就立刻去获得它,而其他CPU核下的线程相当于白白被唤醒,没有抢到GIL后,继续挂起等待,这就造成了资源的浪费。

python3.2以后,改为使用时间,默认为5毫秒,此外虽然说新GIL使用了时间,但决定线程是否释放GIL并不取决于时间,而是取决于gildroprequest这一全局变量,如果gildroprequest=0,则线程会在解释器中一直运行,直到gildroprequest=1,此时线程才会释放GIL,具体细节见 https://zhuanlan.zhihu.com/p/77674796

 

七、GIL何时被线程让出

1. I/O阻塞时;

2. python解释器的check_interval抢占机制,每隔一段时间强制释放GIL;

 

八、线程安全不能依赖GIL

GIL 仅允许一个 Python 线程执行,但Python的check interval抢占机制仍然可能导致线程安全问题。

原因是:类似n+=1这样的代码是非原子的,实际上是由四行bytecode组成,如下:

>>> import dis
>>> dis.dis(foo)
LOAD_GLOBAL              0 (n)
LOAD_CONST               1 (1)
INPLACE_ADD
STORE_GLOBAL             0 (n)

而这四行 bytecode 中间都是有可能被打断的。如果在中间被打断,运行到另一个线程的代码中,会导致运行结果不确定。 (ps:sort()函数是原子的)

所以python多线程还是要加把互斥锁,threading模块的lock()方法

 

九、GIL和互斥锁Mutex lock的关系和区别

相同点:

  • 互斥锁的作用相似,是为了解决解释器中多个线程资源竞争的问题。在使用互斥锁解决代码中的资源竞争问题时,当一个线程执行时,会将全局共享的资源上锁,当线程执行完成后,将锁解开,释放资源,其他线程才能够使用。

区别:

  • 线程互斥锁是Python代码层面的锁,解决我们自己写的Python程序中多线程共享资源的问题。GIL是Python解释器层面的锁,它让多个线程按照一定的顺序并发执行,同时保证同一时刻只有一个线程能使用到CPU。
  • (Python解释器也是一个应用程序。只是说这个应用程序不是我们实现的,我们自己的python程序都要运行在解释器之上,这个应用程序被用来帮我们运行我们自己的程序

举例:

 首先假设只有一个进程,这个进程中有两个线程 Thread1,Thread2, 要修改共享的数据date, 并且有互斥锁
 执行以下步骤:
 (1) 多线程运行,假设Thread1获得GIL可以使用cpu,这时Thread1获得 互斥锁lock,Thread1可以改date数据(但并
 没有开始修改数据)
 (2) Thread1线程在修改date数据前发生了 i/o操作 或者 ticks计数满100 (注意就是没有运行到修改data数据),这个
 时候 Thread1 让出了Gil,Gil锁可以被竞争
 (3) Thread1 和 Thread2 开始竞争 Gil (注意:如果Thread1是因为 i/o 阻塞 让出的Gil Thread2必定拿到Gil,如果
 Thread1是因为ticks计数满100让出Gil 这个时候 Thread1 和 Thread2 公平竞争)
 (4) 假设 Thread2正好获得了GIL, 运行代码去修改共享数据date,由于Thread1有互斥锁lock,所以Thread2无法更改共享数据
 date,这时Thread2让出Gil锁 , GIL锁再次发生竞争 
 (5) 假设Thread1又抢到GIL,由于其有互斥锁Lock所以其可以继续修改共享数据data,当Thread1修改完数据释放互斥锁lock,
 Thread2在获得GIL与lock后才可对data进行修改
 以上描述了 互斥锁和Gil锁的 一个关系
 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值