『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 变量.变量的命名 ...
随机推荐
- 加入security+jwt安全策略
Pom中引入 <!-- security --> <dependency> <groupId>org.springframework.boot</groupI ...
- Qt编写安防视频监控系统27-GPU显示
一.前言 之前用ffmpeg解码的时候,已经做了硬解码的处理,比如支持qsv.dxva2.d3d11va等方式进行硬解码处理,但是当时解码出来以后,还是重新转成了QImage来绘制,这样就大打折扣了, ...
- Ubuntu系统查看文件夹目录
方法1: 进入文件夹里面我们可以使用 按下Ctrl + L 可以看到文件的路径了 然后复制即可. 方法2: 可以鼠标右键点击最下面的属性,然后复制位置里面的路径即可
- 如何通过C#修改Windows操作系统时间
C#的System.DateTime类提供了对日期时间的封装,用它进行时间的转换和处理很方便,但是我没有在其中找到任何可以用来修改系统时间的成员.用过VC.VB等的朋友可能知道,我们可以调用Win32 ...
- [转]WorldWind开发中WorldWindowGLCanvas .setPreferredSize()函数找不到
值高温假期,无意翻到了csdn中三维GIS开发的专栏,讲的是worldwind Java三维GIS系统开发的东西,十分感兴趣.恰巧要求的环境已经存在,直接耍起来.将最新的Worldwind和JOGL下 ...
- 即时通讯技术文集(第43期):直播技术合集(Part3) [共13篇]
为了更好地分类阅读 52im.net 总计1000多篇精编文章,我将在每周三推送新的一期技术文集,本次是第 43 期. [-1-] 直播系统聊天技术(一):百万在线的美拍直播弹幕系统的实时推送技术实践 ...
- 16. C++快速入门--模板和Concept
待修改 1 定义模板 1.1 模板形参 模板参数 模板可以有两种参数, 一种是类型参数, 一种是非类型参数 这两种参数可以同时存在, 非类型参数 的类型 可以是 模板类型形参 template < ...
- Python绘制土地利用和土地覆盖类型图详解
土地利用和土地覆盖是环境科学和城市规划中的重要概念,它们能够帮助本文理解人与自然的关系,促进可持续发展.随着城市化进程的加快,科学地监测和管理土地资源显得尤为重要.Python作为一种强大的编程语言, ...
- 探索Python @dataclass的内部原理
之前写过一篇介绍Python中dataclass的文章:<掌握python的dataclass,让你的代码更简洁优雅>. 那篇侧重于介绍dataclass的使用,今天想探索一下这个有趣的特 ...
- Spring Cloud Alibaba AI 入门与实践
一.概述 Spring AI 是 Spring 官方社区项目,旨在简化 Java AI 应用程序开发,让 Java 开发者像使用 Spring 开发普通应用一样开发 AI 应用. 可参考文章<S ...