目录

1.引言

2.GIL存在的背景

3.GIL主要工作原理

4.Python 线程安全问题

5.可以如何绕过 GIL?

6.延伸阅读


1.引言

我们来看下Python 多线程另一个很重要的话题——GIL(Global Interpreter Lock,即全局解释器锁),这个概念可能大多数人听过,但是真正理解的人可能不多。

我们先从一个简单的 cpu-bound (计算密集型)代码看起,如下:

import timedef CountDown(n):while n > 0:n -= 1start_time = time.time()n = 100000000
CountDown(n)end_time = time.time()
print("time cost: {} seconds".format(end_time - start_time))

在我的四核MacBook Pro机器上面运行结果耗时5秒左右,如下:

time cost: 5.597759962081909 seconds

嗯,太慢了,此时我们考虑用多线程来加速,如下代码所示,我们使用了4个线程:

import time
from threading import Threadn = 100000000def CountDown(n):while n > 0:n -= 1start_time = time.time()t1 = Thread(target=CountDown, args=[n // 2])
t2 = Thread(target=CountDown, args=[n // 2])
t3 = Thread(target=CountDown, args=[n // 2])
t4 = Thread(target=CountDown, args=[n // 2])
t1.start()
t2.start()
t3.start()
t4.start()
t1.join()
t2.join()
t3.join()
t4.join()end_time = time.time()
print("time cost: {} seconds".format(end_time - start_time))

在我的四核MacBook Pro机器上面运行结果耗时11秒左右,如下:

time cost: 11.078598022460938 seconds

什么,使用多线程反而比单线程处理慢,难道Python 的线程是假的线程?

Python 的线程,的的确确封装了底层的操作系统线程,在 Linux 系统里是 Pthread(全称为 POSIX Thread),而在 Windows 系统里是 Windows Thread。另外,Python 的线程,也完全受操作系统管理,比如协调何时执行、管理内存资源、管理中断等等。所以,虽然 Python 的线程和 C++ 的线程本质上是不同的抽象,但它们的底层并没有什么不同。

2.GIL存在的背景

通过上述的示例引入我们今天的主角,也就是 GIL,导致了 Python 线程的性能并不像我们期望的那样。

GIL,是最流行的 Python 解释器 CPython 中的一个技术术语。它的意思是全局解释器锁,本质上是类似操作系统的 Mutex(互斥锁)。每一个 Python 线程,在 CPython 解释器中执行时,都会先锁住自己的线程,阻止别的线程执行。

当然,CPython 会做一些小把戏,轮流执行 Python 线程。这样一来,用户看到的就是“伪并行”——Python 线程在交错执行,来模拟真正并行的线程。

为什么 CPython 需要 GIL 呢?这其实和 CPython 的实现有关。CPython 使用引用计数来管理内存,所有 Python 脚本中创建的实例,都会有一个引用计数,来记录有多少个指针指向它。当引用计数只有 0 时,则会自动释放内存。

我们来看下面这个例子:

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

这个例子中,a 的引用计数是 3,因为有 a、b 和作为参数传递的 getrefcount 这三个地方,都引用了一个空列表。

这样一来,如果有两个 Python 线程同时引用了 a,就会造成引用计数的race condition,也就是竞争条件,引用计数可能最终只增加 1,这样就会造成内存被污染。因为第一个线程结束时,会把引用计数减少 1,这时可能达到条件释放内存,当第二个线程再试图访问 a 时,就找不到有效的内存了。

条件竞争漏洞(Race condition)官方概念是“发生在多个线程同时访问同一个共享代码、变量、文件等没有进行锁操作或者同步操作的场景中。”

所以说,CPython 引进 GIL 其实主要就是这么两个原因:

  • 一是设计者为了规避类似于内存管理这样的复杂的竞争风险问题(race condition);
  • 二是因为 CPython 大量使用 C 语言库,但大部分 C 语言库都不是原生线程安全的(线程安全会降低性能和增加复杂度)。

3.GIL主要工作原理

下面这张图,就是一个 GIL 在 Python 程序的工作示例。其中,Thread 1、2、3 轮流执行,每一个线程在开始执行时,都会锁住 GIL,以阻止别的线程执行;同样的,每一个线程执行完一段后,会释放 GIL,以允许别的线程开始利用资源。

为什么 Python 线程会去主动释放 GIL 呢?毕竟,如果仅仅是要求 Python 线程在开始执行时锁住 GIL,而永远不去释放 GIL,那别的线程就都没有了运行的机会。

没错,CPython 中还有另一个机制,叫做 check_interval,意思是 CPython 解释器会去轮询检查线程 GIL 的锁住情况。每隔一段时间,Python 解释器就会强制当前线程去释放 GIL,这样别的线程才能有执行的机会。

不同版本的 Python 中,check interval 的实现方式并不一样(不用过多的纠结,先知道有这么回事就行了)。早期的 Python 是 100 个 ticks,大致对应了 1000 个 bytecodes;而 Python 3 以后,interval 是 15 毫秒。当然,我们不必细究具体多久会强制释放 GIL,这不应该成为我们程序设计的依赖条件,我们只需明白,CPython 解释器会在一个“合理”的时间范围内释放 GIL 就可以了。

4.Python 线程安全问题

不过,有了 GIL,并不意味着我们 Python 编程者就不用去考虑线程安全了。即使我们知道,GIL 仅允许一个 Python 线程执行,但前面我也讲到了,Python 还有 check interval 这样的抢占机制。我们来考虑这样一段代码:

import threadingn = 0def foo():global nn += 1threads = []
for i in range(100):t = threading.Thread(target=foo)threads.append(t)for t in threads:t.start()for t in threads:t.join()print(n)

如果你执行的话,就会发现,尽管大部分时候它能够打印 100,但有时侯也会打印 99 或者 98。这其实就是因为,n+=1这一句代码让线程并不安全。如果你去翻译 foo 这个函数的 bytecode,就会发现,它实际上由下面四行 bytecode 组成:

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

而这四行 bytecode 中间都是有可能被打断的

所以,千万别想着,有了 GIL 你的程序就可以高枕无忧了,我们仍然需要去注意线程安全。正如我开头所说,GIL 的设计,主要是为了方便 CPython 解释器层面的编写者,而不是 Python 应用层面的程序员。

作为 Python 的使用者,我们还是需要 lock 等工具,来确保线程安全。比如我下面的这个例子:

n = 0
lock = threading.Lock()def foo():global nwith lock:n += 1

5.可以如何绕过 GIL?

Python因为存在GIL的问题,感觉不太行 ?其实不需要太沮丧。

Python 的 GIL,是通过 CPython 的解释器加的限制。如果你的代码并不需要 CPython 解释器来执行,就不再受 GIL 的限制。事实上,很多高性能应用场景都已经有大量的 C 实现的 Python 库,例如 NumPy 的矩阵运算,就都是通过 C 来实现的,并不受 GIL 影响。

所以,大部分应用情况下,你并不需要过多考虑 GIL。因为如果多线程计算成为性能瓶颈,往往已经有 Python 库来解决这个问题了。

换句话说,如果你的应用真的对性能有超级严格的要求,比如 100us 就对你的应用有很大影响,那我必须要说,Python 可能不是你的最优选择。

当然,可以理解的是,我们难以避免的有时候就是想临时给自己松松绑,摆脱 GIL,比如在深度学习应用里,大部分代码就都是 Python 的。在实际工作中,如果我们想实现一个自定义的微分算子,或者是一个特定硬件的加速器,那我们就不得不把这些关键性能(performance-critical)代码在 C++ 中实现(不再受 GIL 所限),然后再提供 Python 的调用接口。

总的来说,你只需要重点记住,绕过 GIL 的大致思路有这么两种就够了:

  • 绕过 CPython,使用 JPython(Java 实现的 Python 解释器)等别的实现;
  • 把关键性能代码,放到别的语言(一般是 C++)中实现。

6.延伸阅读

  • Python有可能删除GIL吗?
  • Python 字节码bytecode

Python全局解释器锁(GIL)相关推荐

  1. Python培训教程:什么是Python全局解释器锁(GIL)?

    本期Python培训教程小编为大家带来的是关于"什么是Python全局解释器锁(GIL)?"的问题,全局解释器锁是计算机程序设计语言解释器用于同步线程的工具,使得在同一进程内任何时 ...

  2. Python全局解释器锁GIL与多线程

    Python中如果是 I/O密集型的操作,用多线程(协程Asyncio.线程Threading),如果I/O操作很慢,需要很多任务/线程协同操作,用Asyncio,如果需要有限数量的任务/线程,那么使 ...

  3. python 全局解释器锁_python全局解释器锁(GIL)

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

  4. Python进阶并发基础--线程,全局解释器锁GIL由来,如何更好的利用Python线程,

    全局解释器锁GIL 官方对于线程的介绍: 在 CPython 中,由于存在全局解释器锁,同一时刻只有一个线程可以执行 Python代码(虽然某些性能导向的库可能会去除此限制).如果你想让你的应用更好地 ...

  5. python gil全局锁_什么是Python全局解释器锁(GIL)?

    python gil全局锁 The Python Global Interpreter Lock or GIL, in simple words, is a mutex (or a lock) tha ...

  6. 【Python核心】全局解释器锁GIL

    Python多线程另一个很重要的话题--GIL(Global Interpreter Lock,即全局解释器锁)鲜有人知,甚至连很多Python老司机都觉得GIL就是一个谜 一.一个不解之谜 耳听为虚 ...

  7. python全局解释器锁 tensorflow_想轻松复现深度强化学习论文?看这篇经验之谈

    近期深度强化学习领域日新月异,其中最酷的一件事情莫过于 OpenAI和 DeepMind 训练智能体接收人类的反馈而不是传统的奖励信号.本文作者认为复现论文是提升机器学习技能的最好方式之一,所以选择了 ...

  8. 问:为什么python中有了全局解释器锁GIL,还要有互斥锁?

    首先我们在进行对比之前,我们要知道什么是全局解释器锁,和什么是互斥锁,他们分别是用来做什么的才能解决这个问题. 首先介绍全局解释解释器锁GIL,Python代码的执行由Python 虚拟机(也叫解释器 ...

  9. python全局解释器锁 tensorflow_Python即将出局?Julia和Swift能取而代之吗?

    上世纪90年代,Python作为一种通用编程语言诞生了.尽管语句简洁,Python在最初十年的使用情况却并不乐观,它也没能真正成为开发者使用的编程语言.当时,Perl是编程首选,Java已在面向对象编 ...

最新文章

  1. python 读取csv文件生成散点图
  2. elasticsearch 集群在线水平扩容收缩
  3. php $_post 报错,关于php输入$_post[‘’]报错的原因
  4. reactjs render props向组件内部动态传入带内容的标签
  5. linux 两个序列比对,如何用COBALT构建本地的多序列比对(Linux系统)
  6. Win32ASM学习[17]:条件跳转
  7. overlay 如何实现跨主机通信?- 每天5分钟玩转 Docker 容器技术(52)
  8. CentOS 7上快速安装saltstack
  9. e值的MonteCalo法估计
  10. [转]vmlinuz
  11. 微计算机与单片机原理及应用答案,单片机原理及应用(张毅刚)完整答案[一].pdf...
  12. FreeSwitch 相关资料
  13. python实现逻辑回归算法
  14. Java实现基于Cookie的单点登录看这篇文章就够了
  15. 方波峰峰值和有效值_峰峰值,峰值,平均值,有效值的关系
  16. 滴滴的大数据可视化效果
  17. Twitterrific for Mac(Twitter客户端)
  18. ThingsBoard RPC control
  19. MySQL~Java的数据库编程:JDBC(JDBC的环境配置以及使用)
  20. TCP/IP高频考点之一个数据包的流浪日记

热门文章

  1. 【其他】什么是Blog? 什么是博客? 什么是Blogger?重要的是内容!!!
  2. 【财富空间】大润发创始人挥泪离场:我战胜了所有对手,却输给了时代!
  3. 人性的弱点 --卡耐基
  4. android /system/lib/ so库解析
  5. spring cloud读取配置文件
  6. shit 环信 IM SDK IM SDK web
  7. 地方政府争夺试点,互联网巨头参与测试,央行数字货币指日可待
  8. LINUX自学第一课
  9. 《阿凡达》的元宇宙帝国多久抵达现实?
  10. 怎么设置uboot从u盘启动linux,RT5350使用uboot从U盘启动linux顺利