关于python中GIL的介绍

关于python中GIL的介绍

原文:What is the Python Global Interpreter Lock (GIL)?

原文链接:https://realpython.com/python-gil/

本文为原文的个人翻译+摘选+改动版本,加入些许补充和解释,与原文内容不完全一致。

衷心地感谢RealPython给我们带来这么棒的教程,祝他们越来越好!请大家支持RealPython!

其他参考:

https://www.zhihu.com/question/23474039 DarrenChan的回答,感谢!

引言

GIL全称Python Global Interpreter Lock, 简单来说就是一个编译器锁,它只允许一个线程拥有python解释器的控制权,执行一定数量字节码的代码之后,释放控制权,重新给线程们按照请求分配控制权,不断重复这个过程。也就是说,python程序任何时刻只有一个线程能够处于运行态。这让GIL在多线程架构和CPU密集型程序的圈子里声名狼藉。本文将介绍GIL对程序的性能影响,以及如何减轻这种影响。

不是所有的python编译器都实现了GIL,但下面我们所说的都基于Cpython,它是有GIL的。

GIL是什么&有什么用

python在内存管理中采用“引用计数\(^{[1]}\)”的方式。当多个线程同时使用同一个对象时,引用计数非常容易出错,极有可能会导致内存泄漏\(^{[2]}\)或者内存提前释放\(^{[3]}\),从而让程序出现奇怪的bug。

可以通过对所有线程间共享的数据结构进行加锁的方式,来保证计数变量的安全性(每个时刻只有一个线程能访问某一个资源),但这种方式会引入大量的锁,多锁就可能导致死锁,而且也会由于申请锁和释放锁带来大量额外的时间开销。

于是就有了GIL。python给自己的解释器加上一个锁,每当有代码要执行,必须请求该锁。全场只有一个锁,不仅预防了死锁,还不怎么增加开销,不过也限制了所有CPU密集型的程序只能是单线程的,但单线程的性能是很好的\(^{[4]}\)

不过GIL其实并不是唯一能解决线程安全问题的方法,比如,有的语言干脆避开引用计数而采用垃圾回收机制来管理内存,从而也就不会产生它带来的线程安全问题,不过这种机制自然是比GIL复杂得多,开销也大,从而需要引入其他机制来提升性能,追赶GIL高效的单线程能力,例如JIT编译器等。

那为什么偏偏要使用GIL呢?借用Larry Hastings的一句话来说,GIL可能是python目前如此火热的功臣之一。1. 在线程的概念出现以前python就已经出现了,它的设计理念旨在让开发者更快上手。2.很多python扩展是用C语言写的,为了防止这些扩展的变动导致前后版本之间相冲突,C扩展需要GIL提供的线程安全的内存管理机制,并且那些原本线程不安全的扩展也可以在GIL的保护下更容易地集成使用。3.GIL机制简单,能提供很好的单线程性能。

对多线程程序的影响

CPU密集型程序

在此类程序中,GIL会限制多个线程的并发执行,再加上线程的创建/切换/销毁时间,采用多线程可能并不会减少运行时间。尤其是对于纯计算型程序,甚至还可能增加时间。

IO密集型程序

在此类程序中,合理使用python多线程是能够正常的减少时间开销的(像其他语言一样),因为在等待IO的过程中,GIL是共享的\(^{[5]}\)

为什么GIL没被移除/弃用

有过这样的尝试,但几乎所有尝试都会遇到如下两个问题:C扩展们失效和单线程程序(以及多线程IO密集程序)的性能下降。

如何解决GIL带来的问题

如果GIL真的给你的程序带来了问题,可以参考下面几种方法:

  1. 使用multiprocessing多进程代替多线程:多进程会给每个进程创建独立的解释器的内存空间,这样一来GIL的问题就没有了。不过要注意,进程的创建/转换/销毁要比线程繁重得多。
  2. 选用其他的python解释器:只有Cpython中才存在GIL,其他比较流行的一些解释器中不存在GIL,如 Jython, IronPython 和 PyPy。

一般情况下,只有在写C扩展和CPU密集型的多线程程序时,才会遇到GIL的问题,不必太担心。

[1] 当一个对象被创建,会有一个计数变量记录它被引用的次数。每次对象被引用,其引用计数就会加一,当引用的周期结束时(例如函数返回),引用计数就会减一。当引用计数减少到0时,该对象占用的内存将被释放。

[2] 指内存无法得到释放。个人理解是,如x当前引用为2,两个线程同时各解除1个引用,但由于同时性,两个线程都是读取2减少1,最终x引用数量还是1.这样一来,x的引用计数就始终无法清零,导致其对应的内存总得不到释放,造成内存泄漏,最终累积成内存溢出。

[3] 类似[2]中同时解除的情况,这里两个线程同时给x各增加1个引用,但结果只增加了1个,在之后的解除中,由于少加了一个而导致引用提前清零,x的内存被提前释放,之后如果再想引用x就会得到错误的值。

[4] 性能好的原因就是前面那几句:全局只需要管理一个锁,策略简单,时空开销小,不会出现死锁。

[5] 这里笔者才疏学浅,没有完全弄懂。还不知道这里“共享”是不是指共享锁的意思。如果是,笔者不清楚读写对象;如果不是,笔者也不知道应该如何理解。望各位看官不吝赐教。

posted @ 2020-10-02 12:43  Zehao1998  阅读(587)  评论(0)    收藏  举报