『Python底层原理』--GIL对多线程的影响
在 Python 多线程编程中,全局解释器锁(Global Interpreter Lock,简称 GIL)是一个绕不开的话题。
GIL是CPython解释器的一个机制,它限制了同一时刻只有一个线程可以执行 Python 字节码。
尽管多线程在某些场景下可以显著提升程序性能,但 GIL 的存在却让 Python 多线程在很多情况下无法充分发挥其优势。
本文将探讨 GIL 的工作机制、它对 Python 多线程的影响,以及解决相关问题的方法和未来的发展方向。
1. Python的多线程
当我们运行一个 Python 可执行文件时,操作系统会启动一个主线程。
这个主线程负责执行 Python 程序的初始化操作,包括加载模块、编译代码以及执行字节码等。
在多线程环境中,Python 线程由操作系统线程(OS 线程)和 Python 线程状态组成,
操作系统线程负责调度线程的执行,而 Python 线程状态则包含了线程的局部变量、堆栈信息等。
比如:
import threading
def worker():
print(f"Thread {threading.current_thread().name} is running")
# 创建并启动两个线程
thread1 = threading.Thread(target=worker, name="Thread-1")
thread2 = threading.Thread(target=worker, name="Thread-2")
thread1.start()
thread2.start()
thread1.join()
thread2.join()
在上述代码中,我们创建了两个线程Thread-1和Thread-2。操作系统会为每个线程分配一个** OS 线程**,并在适当的时候切换它们的执行。
不过,Python中的多线程与其他语言不一样的地方在于,它有一个GIL的机制。
GIL是Python解释器的一个重要机制,一个线程在进入运行之前,必须先获得 GIL。
如果 GIL 已被其他线程占用,那么当前线程将等待,直到 GIL 被释放。
GIL 的释放规则如下:
- 线程执行一定时间后,会主动释放
GIL,以便其他线程可以获取它 - 线程在执行
I/O操作时,会释放GIL,因为I/O操作通常会阻塞线程,释放GIL可以让其他线程有机会运行。
比如:
import time
def cpu_bound_task():
# 模拟 CPU 密集型任务
result = 0
for i in range(10000000):
result += i
def io_bound_task():
# 模拟 I/O 密集型任务
time.sleep(2)
# 创建两个线程分别执行 CPU 密集型和 I/O 密集型任务
thread_cpu = threading.Thread(target=cpu_bound_task)
thread_io = threading.Thread(target=io_bound_task)
thread_cpu.start()
thread_io.start()
thread_cpu.join()
thread_io.join()
在上述代码中,cpu_bound_task是一个 CPU 密集型任务,它会一直占用 GIL,直到任务完成。
而io_bound_task是一个 I/O 密集型任务,它在执行时会释放 GIL,让其他线程有机会运行。
2. GIL的影响
2.1. 对CPU密集型任务的影响
GIL对 CPU 密集型任务的影响巨大,使得Python的多线程在CPU密集型任务中几乎无法发挥优势。
因为即使有多个线程,同一时刻也只有一个线程可以执行 Python 字节码。
而且,线程之间的上下文切换还会增加额外的开销,导致程序性能下降。
import time
import threading
def cpu_bound_task():
result = 0
for i in range(10000000):
result += i
def single_thread():
start_time = time.time()
cpu_bound_task()
cpu_bound_task()
print(f"Single-thread time: {time.time() - start_time:.2f} seconds")
def multi_thread():
start_time = time.time()
thread1 = threading.Thread(target=cpu_bound_task)
thread2 = threading.Thread(target=cpu_bound_task)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f"Multi-thread time: {time.time() - start_time:.2f} seconds")
single_thread()
multi_thread()

运行上述代码,我们会发现多线程版本的执行时间比单线程版本还要长,这正是因为 GIL 的存在导致了线程之间的上下文切换开销。
2.2. 对I/O密集型任务的影响
与 CPU 密集型任务不同,多线程在 I/O密集型任务中可以显著提升性能。
因为当一个线程在执行 I/O 操作时,它会释放 GIL,其他线程可以利用这段时间执行其他任务。
import time
import threading
def io_bound_task():
time.sleep(2)
def single_thread():
start_time = time.time()
io_bound_task()
io_bound_task()
print(f"Single-thread time: {time.time() - start_time:.2f} seconds")
def multi_thread():
start_time = time.time()
thread1 = threading.Thread(target=io_bound_task)
thread2 = threading.Thread(target=io_bound_task)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f"Multi-thread time: {time.time() - start_time:.2f} seconds")
single_thread()
multi_thread()

运行上述代码,我们会发现多线程版本的执行时间比单线程版本缩短了一半,这说明多线程在 I/O 密集型任务中可以有效提升性能。
2.3. 护航效应(Convoy Effect)
当 CPU 密集型线程和 I/O 密集型线程混合运行时,会出现一种称为“护航效应”的现象。
CPU 密集型线程会一直占用 GIL,导致 I/O 密集型线程无法及时获取 GIL,从而大幅降低 I/O 密集型线程的性能。
比如:
import time
import threading
def cpu_bound_task():
result = 0
for i in range(10000000):
result += i
def io_bound_task():
time.sleep(2)
def mixed_thread():
start_time = time.time()
thread_cpu = threading.Thread(target=cpu_bound_task)
thread_io = threading.Thread(target=io_bound_task)
thread_cpu.start()
thread_io.start()
thread_cpu.join()
thread_io.join()
print(f"Mixed-thread time: {time.time() - start_time:.2f} seconds")
mixed_thread()
在上述代码中,cpu_bound_task会一直占用GIL,导致io_bound_task 无法及时运行,从而延长了整个程序的执行时间。
3. GIL存在的原因
GIL给并发性能带来了很多的问题,为什么Python解释器中会有GIL这个方案呢?
因为Python历史悠久,当初Python流行的时候,针对多核的并发编程并不是主流,当时采用GIL主要是为了保证线程安全。
GIL涵盖了以下几个方面:
- 引用计数:
Python使用引用计数来管理内存。如果多个线程同时修改引用计数,可能会导致内存泄漏或崩溃 - 数据结构:许多
Python内置数据结构(如列表、字典等)需要线程安全的访问 - 全局数据:解释器的全局状态需要保护,以防止多线程访问时出现数据竞争
- C 扩展:许多 C 扩展模块依赖于
GIL来保证线程安全。
目前,尽管GIL带来了诸多限制,但移除它并非易事。主要困难包括:
- 垃圾回收机制:
Python的垃圾回收机制依赖于引用计数,移除GIL后需要重新设计垃圾回收机制 - C 扩展兼容性:许多现有的 C 扩展模块依赖于
GIL来保证线程安全。移除GIL后,这些扩展模块可能需要重新编写
例如,Gilectomy项目尝试移除 GIL,但最终因性能问题和兼容性问题而失败。
虽然移除了 GIL,但单线程性能大幅下降,且许多 C 扩展模块无法正常工作。
GIL的实现细节可以通过阅读CPython源代码来进一步了解。
关键文件包括Python/ceval.c和Python/thread.c,其中定义了GIL的获取和释放机制。
4. GIL的未来
GIL是一定要解决的问题,毕竟多核才是当前主流的发展方向。
目前,有些项目为了解决GIL对并发性能的影响,正在努力发展中,包括:
4.1. 子解释器计划
Python 的子解释器计划(PEP 554)试图通过引入多个独立的解释器(每个解释器拥有自己的 GIL)来实现多解释器并行。
这种方法可以在一定程度上绕过 GIL 的限制,但目前仍存在一些限制,例如跨解释器通信的开销较大。
4.2. Faster CPython 项目
Faster CPython 项目专注于提升 Python 的单线程性能。
虽然它可能会进一步优化 GIL 的实现,但其主要目标是减少解释器的开销,而不是直接解决 GIL 问题。
这可能会使 GIL 问题在短期内受到较少的关注。
4.3. Sam Gross 的 CPython fork
Sam Gross 的 CPython fork 是一个值得关注的尝试,他成功移除了 GIL,并且在单线程性能上取得了显著提升。
他的工作为解决 GIL 问题带来了新的方向,但目前尚未被合并到主线 CPython 中。
『Python底层原理』--GIL对多线程的影响的更多相关文章
- 『Python基础-9』元祖 (tuple)
『Python基础-9』元祖 (tuple) 目录: 元祖的基本概念 创建元祖 将列表转化为元组 查询元组 更新元组 删除元组 1. 元祖的基本概念 元祖可以理解为,不可变的列表 元祖使用小括号括起所 ...
- 『Python基础-12』各种推导式(列表推导式、字典推导式、集合推导式)
# 『Python基础-12』各种推导式(列表推导式.字典推导式.集合推导式) 推导式comprehensions(又称解析式),是Python的一种独有特性.推导式是可以从一个数据序列构建另一个新的 ...
- 『Python基础-11』集合 (set)
# 『Python基础-11』集合 (set) 目录: 集合的基本知识 集合的创建 访问集合里的值 向集合set增加元素 移除集合中的元素 集合set的运算 1. 集合的基本知识 集合(set)是一个 ...
- 『Python基础-10』字典
# 『Python基础-10』字典 目录: 1.字典基本概念 2.字典键(key)的特性 3.字典的创建 4-7.字典的增删改查 8.遍历字典 1. 字典的基本概念 字典一种key - value 的 ...
- 『Python基础-8』列表
『Python基础-8』列表 1. 列表的基本概念 列表让你能够在一个地方存储成组的信息,其中可以只包含几个 元素,也可以包含数百万个元素. 列表由一系列按特定顺序排列的元素组成.你可以创建包含字母表 ...
- 『Python基础-7』for循环 & while循环
『Python基础-7』for循环 & while循环 目录: 循环语句 for循环 while循环 循环的控制语句: break,continue,pass for...else 和 whi ...
- 『Python基础-6』if语句, if-else语句
# 『Python基础-6』if语句, if-else语句 目录: 条件测试 if语句 if-else语句 1. 条件测试 每条if语句的核心都是一个值为True或False的表达式,这种表达式被称为 ...
- 『Python基础-5』数字,运算,转换
『Python基础-5』数字,运算,转换 目录 基本的数字类型 二进制,八进制,十六进制 数字类型间的转换 数字运算 1. 数字类型 Python 数字数据类型用于存储数学上的值,比如整数.浮点数.复 ...
- 『Python基础-4』字符串
# 『Python基础-4』字符串 目录 1.什么是字符串 2.修改字符串 2.1 修改字符串大小 2.2 合并(拼接)字符串 2.3 使用乘号'*'来实现字符串的叠加效果. 2.4 在字符串中添加空 ...
- 『Python基础-3』变量、定义变量、变量类型、关键字Python基础-3』变量、定义变量、变量类型、关键字
『Python基础-3』变量.定义变量.变量类型.关键字 目录: 1.Python变量.变量的命名 2.变量的类型(Python数据类型) 3.Python关键字 1. Python 变量.变量的命名 ...
随机推荐
- CompilerGenerated与GeneratedCode区别
前言 最近在捣鼓代码生成器,基于 Roslyn,我们可以让生成器项目生成我们的目标 C# 代码,这个也是MVVM Toolkit的实现方式,在查看生成代码的过程中,我们经常会遇到一些特殊的特性,如 G ...
- 干掉EasyExcel!FastExcel初体验
我们知道 EasyExcel 在作者从阿里离职之后就停止维护了,但在前两周 EasyExcel 原作者推出了他的升级版框架 FastExcel.以下是 FastExcel 的上手实战过程,带大家一起提 ...
- Qt/C++路径轨迹回放/回放每个点信号/回放结束信号/拿到移动的坐标点经纬度
一.前言说明 在使用百度地图的路书功能中,并没有提供移动的信号以及移动结束的信号,但是很多时候都期望拿到移动的哪里了以及移动结束的信号,以便做出对应的处理,比如结束后需要触发一些对应的操作.经过搜索发 ...
- [转]C#的二进制文件操作及关于Encoding类与汉字编码转换的问题
1.数值应保存在二进制文件 首先列举文本.二进制文件的操作(读写)方法: 方式1: //文本文件操作:创建/读取/拷贝/删除 using System; using System.IO; class ...
- 【开源】C#上位机必备高效数据转换助手
一.前言 大家好!我是付工. 我们在进行上位机开发时,从设备端获取到的数据之后,需要进行一定的数据处理及转换,才能生成我们需要用的数据. 这其中就涉及到了各种数据类型之间的相关转换,很多非科班出身的电 ...
- CDS标准视图:催款范围描述 I_DunningAreaText
视图名称:催款范围描述 I_DunningAreaText 视图类型: 视图代码: 点击查看代码 @EndUserText.label: 'Dunning Area - Text' @Analytic ...
- Ellyn-Golang调用级覆盖率&方法调用链插桩采集方案
词语解释 Ellyn要解决什么问题? 在应用程序并行执行的情况下,精确获取单个用例.流量.单元测试走过的方法链(有向图).出入参数.行覆盖等运行时数据,经过一定的加工之后,应用在覆盖率.影响面评估.流 ...
- css笔记详解
css讲解 首先在我们学习css之前先来思考一个问题,为什么html标签上不直接改变样式,而要将文档结构和样式分离,分别用html和css来表示呢? 其实我个人认为这样分离带来的好处明显,我总结了几 ...
- HashMap的put方法的扩容流程
final Node<K,V>[] resize() { // [1,2,3,4,5,6,7,8,9,10,11,,,,] Node<K,V>[] oldTab = table ...
- el-table关于选择行的三个常用事件
变量声明 data(){ return{ selectList: [], } } 事件绑定 <el-table @select-all="selectAllChange" @ ...