Python中的GIL到底是什么,以及Python的多线程性能究竟如何,本文将为大家一探究竟!
一、GIL是什么
GIL(Global Interpreter Lock),全称为全局解释器锁,是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行,即便在多核处理器上,使用GIL的解释器也只允许同一时间执行一个线程。
然而,由于CPython是大部分环境下默认的Python解释器,所以在很多人的概念中CPython就是Python,也就理所当然的把GIL归结为Python语言的缺陷,但这里首先要明确一点,就是GIL并不是Python的特性,Python也完全可以不依赖于GIL,例如解释器JPython中就没有GIL,所以,GIL并不是Python独有的特性,而是解释型语言处理多线程问题的一种机制而非语言特性。
我们来一起看一下官方给出的解释:
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.)
通过这段解释,我们可以得出GIL的优点,即GIL可以保证我们在多线程编程时,无需考虑多线程之间数据的完整性和状态同步的问题,但其缺点也是显而易见的,即多线程程序执行起来是“并发”,而不是“并行”,因此执行效率会很低,甚至不如单线程的执行效率。
二、GIL产生的背景
由于物理上的限制,各CPU厂商在核心频率上的比赛已经被多核所取代。为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,而随之带来的问题就是线程间数据的一致性和状态同步的困难,即使在CPU内部的Cache也不例外,为了有效解决多份缓存之间的数据同步,各厂商花费了不少心思,也不可避免的带来了一定的性能损失。
Python当然也逃不开,为了利用多核,Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。于是有了GIL这把超级大锁,而当越来越多的代码库开发者接受了这种设定后,他们开始大量依赖这种特性。
慢慢的这种实现方式被发现是低效的。但当大家试图去拆分和去除GIL的时候,发现大量库代码开发者已经重度依赖GIL,而且非常难以去除了。到底有多难呢?做个类比,像MySQL这样的“小项目”为了把【Buffer Pool Mutex】这把大锁拆分成各个小锁也花了从5.5到5.6再到5.7多个大版为期近5年的时间,并且仍在继续。况且MySQL这个背后有公司支持且有固定开发团队的产品都走的如此艰难,更何况Python这样核心开发和代码贡献者高度社区化的团队呢?
所以简单的说,GIL的存在更多是历史原因。如果推倒重来,多线程的问题依然还是要面对,但至少会比目前GIL这种方式更优雅。
三、GIL的影响
从上文的介绍和官方的定义来看,GIL无疑就是一把全局排他锁。毫无疑问全局锁的存在会对多线程的效率有不小的影响,甚至几乎就等于Python是个单线程的程序。 那么大家就会说了,“全局锁只要释放的勤快,那效率也不会差啊,并且只要在进行耗时的I/O操作时,能释放GIL,这样也还是可以提升运行效率的,或者说再差也不会比单线程的效率差吧”,但是理论上是这样的,而实际上呢?Python比你想象的要更糟。
其实,这也是网上为什么很多人都提到过这样的疑问:”为什么Python多线程运行时比只有一个线程的时候还要慢?“显然,大家觉得一个具有两个线程的程序要比只有一个线程的快,而事实上。这个问题是确实存在的,原因在于GIL的存在使得Python多线程程序的执行效率甚至比不上单线程的执行效率。原因其实很简单,由于GIL的存在,使得同一时刻只有一个线程在运行程序,再加上切换线程和竞争GIL带来的开销,致使Python多线程的执行效率就比不上单线程的执行效率了。
四、如何规避GIL的影响
(1)使用多进程替代多线程(推荐)
【multiprocess】库的出现很大程度上是为了弥补【thread】库因为GIL而低效的缺陷。【multiprocess】库完整复制了一套【thread】库所提供的接口,方便迁移。唯一的不同,就是它使用了多进程而不是多线程。每个进程拥有自己独立的GIL,因此不会出现进程之间的GIL争抢。
当然【multiprocess】库也不是万能良药。它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于【thread】库来说,申明一个“global”变量,然后使用“thread.Lock”的“context”包裹住就搞定了。而【multiprocess】库由于进程之间无法共享数据,只能通过在主线程中申明一个“消息队列”,然后再通过“put、get“或者”share memory“的方法。这个额外的实现成本使得本来就非常痛苦的多线程编程,变得更加的痛苦了。
(2)使用其他解释器(不推荐)
前文也提到了,既然GIL是CPython的产物,那么使用其他的解释器是不是更好呢?没错,像JPython和IronPython这样的解释器由于实现语言的特性,他们不需要GIL的帮助。但是,由于它们使用了Java/C#用于解释器的实现,进而导致它们失去了利用社区众多C语言模块特性的机会。所以这些解释器也因此一直都比较小众,毕竟功能和性能大家在初期都会选择前者,即【Done is better than perfect】。
五、Python的多线程性能
最后,我们再来看一下Python的多线程性能究竟如何。下面,我们就来通过三段代码给大家对比一下,相信大家就会对Python的多线程性能有一个直观的了解。
首先,看一下多线程编程,由于我的计算机是4核,所以我开了4个线程,让我们一起看一下CPU资源占有率:
#coding=utf-8 from threading import Thread def loop(): while True: pass if __name__ == '__main__': for i in range(3): t = Thread(target=loop) t.start() while True: pass
通过上图我们发现CPU的利用率并没有占满,大致相当于单核的水平。如果我们将多线程改变成多进程呢?
#coding=utf-8 from multiprocessing import Process def loop(): while True: pass if __name__ == '__main__': for i in range(3): t = Process(target=loop) t.start() while True: pass
通过上图我们发现多线程换成多进程后CPU的利用率直接飙升到了100%,说明多进程是可以利用多核的。为了进一步验证这是Python中GIL带来的影响,我尝试使用Java编写相同的代码,并开启多线程。
package com.darrenchan.thread; public class TestThread { public static void main(String[] args) { for (int i = 0; i < 3; i++) { new Thread(new Runnable() { @Override public void run() { while (true) { } } }).start(); } while(true){ } } }
通过上图我们发现CPU的利用率同样达到了100%,由此可见,Java中的多线程是可以利用多核的,这才是真正的多线程!而Python中的多线程只能利用单核,是假的多线程!
综上所述,Python多线程相当于单核多线程,但是,单核多线程等于自断一臂。所以,在Python中可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务,因为多个Python进程有各自独立的GIL锁,互不影响。
还没有评论,来说两句吧...