本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理

做 Python 开发时,想必你肯定听过 GIL,它经常被 Python 程序员吐槽,说 Python 的多线程非常鸡肋,因为 GIL 的存在,Python 无法利用多线程提高性能。

但事实真的如此吗?

这篇文章,我们就来看一下 Python 的 GIL 到底是什么?以及它的存在,究竟对我们的程序有哪些影响。

GIL是什么?

查阅官方文档,GIL 全称 Global Interpreter Lock,即全局解释器锁,它的官方解释如下:

In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple 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.)

翻译成中文就是:

在 CPython 解释器中,全局解释锁 GIL 是在于执行 Python 字节码时,为了保护访问 Python 对象而阻止多个线程执行的一把互斥锁。这把锁的存在主要是因为 CPython 解释器的内存管理不是线程安全的。然而直到今天 GIL 依旧存在,现在的很多功能已经习惯于依赖它作为执行的保证。

我们从这个定义中,可以看到几个重点:

  1. GIL 是存在于 CPython 解释器中的,属于解释器层级,而并非属于 Python 的语言特性。也就是说,如果你自己有能力实现一个 Python 解释器,完全可以不使用 GIL
  2. GIL 是为了让解释器在执行 Python 代码时,同一时刻只有一个线程在运行,以此保证内存管理是安全的
  3. 历史原因,现在很多 Python 项目已经习惯依赖 GIL(开发者认为 Python 就是线程安全的,写代码时对共享资源的访问不会加锁)

在这里我想强调的是,因为 Python 默认的解释器是 CPython,GIL 是存在于 CPython 解释器中的,我们平时说到 GIL 就认为它是 Python 语言的问题,其实这个表述是不准确的。

其实除了 CPython 解释器,常见的 Python 解释器还有如下几种:

  • CPython:C 语言开发的解释器,官方默认使用,目前使用也最为广泛,存在 GIL
  • IPython:基于 CPython 开发的交互式解释器,只是增强了交互功能,执行过程与 CPython 完全一样
  • PyPy:目标是加快执行速度,采用 JIT 技术,对 Python 代码进行动态编译(不是解释),可以显著提高代码的执行速度,但执行结果可能与 CPython 不同,存在 GIL
  • Jython:运行在 Java 平台的 Python 解释器,可以把 Python 代码编译成 Java 字节码,依赖 Java 平台,不存在 GIL
  • IronPython:和 Jython 类似,运行在微软的 .Net 平台下的 Python 解释器,可以把 Python 代码编译成 .Net 字节码,不存在 GIL

虽然有这么多 Python 解释器,但使用最广泛的依旧是官方提供的 CPython,它默认是有 GIL 的。

那么 GIL 会带来什么问题呢?为什么开发者总是抱怨 Python 多线程无法提高程序效率?

GIL带来的问题

想要了解 GIL 对 Python 多线程带来的影响,我们来看一个例子。

import threading

def loop():
count = 0
while count <= 1000000000:
count += 1 # 2个线程执行loop方法
t1 = threading.Thread(target=loop)
t2 = threading.Thread(target=loop) t1.start()
t2.start()
t1.join()
t2.join()
复制代码

在这个例子中,虽然我们开启了 2 个线程去执行 loop,但我们观察 CPU 的使用情况,发现这个程序只能跑满一个 CPU 核心,没有利用到多核。

这就是 GIL 带来的问题。

其原因在于,一个 Python 线程想要执行一段代码,必须先拿到 GIL 锁后才被允许执行,也就是说,即使我们使用了多线程,但同一时刻却只有一个线程在执行。

但我们进一步思考一下,就算有 GIL 的存在,理论来说,如果 GIL 释放的够快,多线程怎么也要比单线程执行效率高吧?

但现实的结果是:多线程比我们想象的更糟糕。

我们再来看一个例子,还是运行一个 CPU 密集型的任务程序,我们来看单线程执行 2 次和 2 个线程同时执行,哪个效率更高?

单线程执行 2 次 CPU 密集型任务:

import time
import threading def loop():
count = 0
while count <= 1000000000:
count += 1 # 单线程执行 2 次 CPU 密集型任务
start = time.time()
loop()
loop()
end = time.time()
print("execution time: %s" % (end - start))
# execution time: 89.63111019134521
复制代码

从结果来看,执行耗时 89秒。

再来看 2 个线程同时执行 CPU 密集型任务:

import time
import threading def loop():
count = 0
while count <= 1000000000:
count += 1 # 2个线程同时执行CPU密集型任务
start = time.time() t1 = threading.Thread(target=loop)
t2 = threading.Thread(target=loop)
t1.start()
t2.start()
t1.join()
t2.join() end = time.time()
print("execution time: %s" % (end - start))
# execution time: 92.29994678497314
复制代码

执行耗时却达到了 92 秒。

从执行结果来看,多线程的效率还不如单线程的执行效率高!

为什么会导致这种情况?我们来看一下 GIL 究竟是怎么回事。

GIL原理

其实,由于 Python 的线程就是 C 语言的 pthread,它是通过操作系统调度算法调度执行的。

Python 2.x 的代码执行是基于 opcode 数量的调度方式,简单来说就是每执行一定数量的字节码,或遇到系统 IO 时,会强制释放 GIL,然后触发一次操作系统的线程调度。

虽然在 Python 3.x 进行了优化,基于固定时间的调度方式,就是每执行固定时间的字节码,或遇到系统 IO 时,强制释放 GIL,触发系统的线程调度。

但这种线程的调度方式,都会导致同一时刻只有一个线程在运行。

而线程在调度时,又依赖系统的 CPU 环境,也就是在单核 CPU 或多核 CPU 下,多线程在调度切换时的成本是不同的。

如果是在单核 CPU 环境下,多线程在执行时,线程 A 释放了 GIL 锁,那么被唤醒的线程 B 能够立即拿到 GIL 锁,线程 B 可以无缝接力继续执行,执行流程如下图:

而如果在在多核 CPU 环境下,当多线程执行时,线程 A 在 CPU0 执行完之后释放 GIL 锁,其他 CPU 上的线程都会进行竞争。

但 CPU0 上的线程 B 可能又马上获取到了 GIL,这就导致其他 CPU 上被唤醒的线程,只能眼巴巴地看着 CPU0 上的线程愉快地执行着,而自己只能等待,直到又被切换到待调度的状态,这就会产生多核 CPU 频繁进行线程切换,消耗资源,这种情况也被叫做「CPU颠簸」。整个执行流程如下图:

图中绿色部分是线程获得了 GIL 并进行有效的 CPU 运算,红色部分是被唤醒的线程由于没有争夺到 GIL,只能无效等待,无法充分利用 CPU 的并行运算能力。

这就是多线程在多核 CPU 下,执行效率还不如单线程或单核 CPU 效率高的原因。

到此,我们可以得出一个结论:如果使用多线程运行一个 CPU 密集型任务,那么 Python 多线程是无法提高运行效率的。

别急,你以为事情就这样结束了吗?

我们还需要考虑另一种场景:如果多线程运行的不是一个 CPU 密集型任务,而是一个 IO 密集型的任务,结果又会如何呢?

答案是,多线程可以显著提高运行效率!

其实原因也很简单,因为 IO 密集型的任务,大部分时间都花在等待 IO 上,并没有一直占用 CPU 的资源,所以并不会像上面的程序那样,进行无效的线程切换。

例如,如果我们想要下载 2 个网页的数据,也就是发起 2 个网络请求,如果使用单线程的方式运行,只能是依次串行执行,其中等待的总耗时是 2 个网络请求的时间之和。

而如果采用 2 个线程的方式同时处理,这 2 个网络请求会同时发送,然后同时等待数据返回(IO等待),最终等待的时间取决于耗时最久的线程时间,这会比串行执行效率要高得多。

所以,如果需要运行 IO 密集型任务,Python 多线程是可以提高运行效率的。

为什么会有GIL?

我们已经了解到,GIL 对于处理 CPU 密集型任务的场景,多线程是无法提高运行效率的。

既然 GIL 的影响这么大,那为什么 Python 解释器 CPython 在设计时要采用这种方式呢?

这就需要追溯历史原因了。

在 2000 年以前,各个 CPU 厂商为了提高计算机的性能,其努力方向都在提升单个 CPU 的运行频率上,但在之后的几年遇到了天花板,单个 CPU 性能已经无法再得到大幅度提升,所以在 2000 年以后,提升计算机性能的方向便改为向多 CPU 核心方向发展。

为了更有效的利用多核心 CPU,很多编程语言就出现了多线程的编程方式,但也正是有了多线程的存在,随之带来的问题就是多线程之间对于维护数据和状态一致性的困难。

Python 设计者在设计解释器时,可能没有想到 CPU 的性能提升会这么快转为多核心方向发展,所以在当时的场景下,设计一个全局锁是那个时代保护多线程资源一致性最简单经济的设计方案。

而随着多核心时代来临,当大家试图去拆分和去除 GIL 的时候,发现大量库的代码和开发者已经重度依赖 GIL(默认认为 Pythonn 内部对象是线程安全的,无需在开发时额外加锁),所以这个去除 GIL 的任务变得复杂且难以实现。

所以,GIL 的存在更多的是历史原因,在 Python 3 的版本,虽然对 GIL 做了优化,但依旧没有去除掉,Python 设计者的解释是,在去除 GIL 时,会破坏现有的 C 扩展模块,因为这些扩展模块都严重依赖于 GIL,去除 GIL 有可能会导致运行速度会比 Python 2 更慢。

Python 走到现在,已经有太多的历史包袱,所以现在只能背负着它们前行,如果一切推倒重来,想必 Python 设计者会设计得更加优雅一些。

解决方案

既然 GIL 的存在会导致这么多问题,那我们在开发时,需要注意哪些地方,避免受到 GIL 的影响呢?

我总结了以下几个方案:

  1. IO 密集型任务场景,可以使用多线程可以提高运行效率
  2. CPU 密集型任务场景,不使用多线程,推荐使用多进程方式部署运行
  3. 更换没有 GIL 的 Python 解释器,但需要提前评估运行结果是否与 CPython 一致
  4. 编写 Python 的 C 扩展模块,把 CPU 密集型任务交给 C 模块处理,但缺点是编码较为复杂
  5. 更换其他语言 :)

总结

这篇文章我们主要讲了 Python GIL 相关的问题。

首先,我们了解到 GIL 属于 Python 解释器层面的,它并不是 Python 语言的特性,这一点我们一定不要搞混了。GIL 的存在会让 Python 在执行代码时,只允许同一时刻只有一个线程在执行,其目的是为了保证在执行过程中内存管理的安全性。

之后我们通过一个例子,观察到 Python 在多线程运行 CPU 密集型任务时,执行效率比单线程还要低,其原因是因为在多核 CPU 环境下,GIL 的存在会导致多线程切换时无效的资源消耗,因此会降低程序运行的效率。

但如果使用多线程运行 IO 密集型的任务,由于线程更多地是在等待 IO,所以并不会消耗 CPU 资源,这种情况下,使用多线程是可以提高程序运行效率的。

最后,我们分析了 GIL 存在的原因,更多是因为历史问题导致,也正因为 GIL 的存在,很多 Python 开发者默认 Python 是线程安全的,这也间接增加了去除 GIL 的困难性。

基于这些前提,我们平时在部署 Python 程序时,一般更倾向于使用多进程的方式去部署,就是为了避免 GIL 的影响。

任何一种编程语言,都有其优势和劣势,我们需要理解它的实现机制,发挥其长处,才能更好地服务于我们的需求。

想要获取更多Python学习资料可以加
QQ:2955637827私聊
或加Q群630390733
大家一起来学习讨论吧!

Python进阶——为什么GIL让多线程变得如此鸡肋?的更多相关文章

  1. python进阶(9)多线程

    什么是线程? 线程也叫轻量级进程,是操作系统能够进行运算调度的最小单位,它被包涵在进程之中,是进程中的实际运作单位.线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其 ...

  2. python进阶(15)多线程与多进程效率测试

    前言 在Python中,计算密集型任务适用于多进程,IO密集型任务适用于多线程   正常来讲,多线程要比多进程效率更高,因为进程间的切换需要的资源和开销更大,而线程相对更小,但是我们使用的Python ...

  3. Python进阶篇:Socket多线程

    1. 初识Socket server和clinet之间的交互方式 2. 客户端和服务端的单次交互 ==================================== 服务端 import soc ...

  4. 【Python进阶】无论API怎么变,SDK都可以根据URL实现完全动态的调用

    现在很多网站都搞REST API,比如新浪微博.豆瓣啥的,调用API的URL类似: http://api.server/user/friends http://api.server/user/time ...

  5. Python进阶基础学习(多线程)

    Python进阶学习笔记(一) threading模块 threading.thread(target = (函数)) 负责定义子线程对象 threading.enumerate() 负责查看子线程对 ...

  6. python 并发编程 多线程 GIL与多线程

    GIL与多线程 有了GIL的存在,同一时刻同一进程中只有一个线程被执行 多进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势 1.cpu到底是用来做计算的,还是用来做I/ ...

  7. Python中的GIL

    •start 线程准备就绪,等待CPU调度 •setName 为线程设置名称 •getName 获取线程名称 •setDaemon 设置为后台线程或前台线程(默认) 如果是后台线程,主线程执行过程中, ...

  8. python中的GIL详解

    GIL是什么 首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念.就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可 ...

  9. 聊一下Python的线程 & GIL

    再来聊一下Python的线程 参考这篇文章 https://www.zhihu.com/question/23474039/answer/24695447 简单地说就是作为可能是仅有的支持多线程的解释 ...

随机推荐

  1. L - Deque 题解(区间dp)

    题目链接 题目大意 给你一个双端队列里面有n个数组元素(n<=3000) 有两个人,每次一个人都可以选择队列里的首元素或者尾元素删除,轮流进行,删除后那个人即可获得这个元素的值 第一个人的总权值 ...

  2. C语言讲义——头文件

    头文件.h Dev C++可以建C项目,也可以建C++项目,下面分C和C++两种情况讨论. c.h C语言中,头文件往往不是必须的,只是描述性的文件. 因此,C项目中可以没有.h文件. cpp.h 下 ...

  3. C语言讲义——“编译、链接”

    HelloWorld 最简HelloWorld include <stdio.h> 指令:标准输入输出头文件. main函数 C语言程序的唯一入口. #include <stdio. ...

  4. MindSpore手写数字识别初体验,深度学习也没那么神秘嘛

    摘要:想了解深度学习却又无从下手,不如从手写数字识别模型训练开始吧! 深度学习作为机器学习分支之一,应用日益广泛.语音识别.自动机器翻译.即时视觉翻译.刷脸支付.人脸考勤--不知不觉,深度学习已经渗入 ...

  5. Guava中EventBus分析

    EventBus 1. 什么是EventBus 总线(Bus)一般指计算机各种功能部件之间传送信息的公共通信干线,而EventBus则是事件源(publisher)向订阅方(subscriber)发送 ...

  6. Django匆匆一眼却解答了多年疑惑

    Django 是 Python 的 一款 Web 开发框架,另外还有 Tornado,Flask,Twisted.为什么我要选择学 Django?原因很简单,上家公司来了个网易的测开,就是用 Djan ...

  7. Cys_Control(三) MTextBox

    一.查看TextBox原样式 通过Blend查看TextBox原有样式 <Window.Resources> <SolidColorBrush x:Key="TextBox ...

  8. 基础篇:JAVA.Stream函数,优雅的数据流操作

    前言 平时操作集合数据,我们一般都是for或者iterator去遍历,不是很好看.java提供了Stream的概念,它可以让我们把集合数据当做一个个元素在处理,并且提供多线程模式 流的创建 流的各种数 ...

  9. 第十一章、Designer中主窗口QMainWindow类

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 一.概述 主窗口对象是在新建窗口对象时,选择main window类型的模板时创建的窗口对象,如图: ...

  10. Xray高级版白嫖破解指南

    啊,阿Sir,免费的还想白嫖?? 好啦好啦不开玩笑 Xray是从长亭洞鉴核心引擎中提取出的社区版漏洞扫描神器,支持主动.被动多种扫描方式,自备盲打平台.可以灵活定义 POC,功能丰富,调用简单,支持 ...