『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 变量.变量的命名 ...
随机推荐
- 【前端】【探究】HTML - input类型为file时如何实现自定义文本以更好的美化
想到英语四级考了两次都没过,我觉得要多使用英文,所以本文使用英文书写. 本文讲述了遇到的问题,解决的思路,并讲述了解决方案,也许对你会有帮助. 目录 Problem description Solut ...
- 解决File "<input>", line 1 pip install XXXX ^ SyntaxError: invalid syntax
首先退出python exit() 打开cmd里直接输入(不要进python) pip install XXX
- 波折重重:Linux实时系统Xenomai宕机问题的深度定位
目录 一 前言 二 背景 三 原因分析及措施 硬件原因 应用软件 操作系统 四 分析定位 转机 拨云见雾 irq计数 Schedstat coreclk 现象结论 五 原因一 六 原因二 七 解决 八 ...
- Qt编写安防视频监控系统33-onvif云台控制
一.前言 云台控制也是onvif功能中最常用的,最常用的功能排第一的是拿到视频流地址,排第二的就是云台控制了,云台控制的含义就是对带云台的摄像机进行上下左右的移动,一般云台摄像机都是带有一个小电机,一 ...
- DataTable 循环取值
//list是datatable类型 for (int i = 0; i < list.Rows.Count; i++) { var A = list.Rows[i]["列名" ...
- Windows安全加固(二)
三.本地安全策略用户权限分配 1. 使用windows+R打开运行,输入"secpol.msc"打开本地安全策略->本地策略->用户权限分配->找到"拒 ...
- 第十二章 ArrayList&LinkedList源码解析
一.对于ArrayList需要掌握的七点内容 ArrayList的创建:即构造器 往ArrayList中添加对象:即add(E)方法 获取ArrayList中的单个对象:即get(int index) ...
- RPC框架的实现原理,及RPC架构组件详解
RPC的由来 随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进. 单一应用架构 当网站流量很小时, ...
- vue3.5保证你看得明明白白
子组件中设置默认属性 <template> <div class="child-page"> <h1>我是子组件</h1> < ...
- [ARC 188A] ABC Symmetry
solution by XiangXunYi 思路推导 step 1 首先题目中操作二同时删掉 A,B,C 的条件相当于同时将三者数量减一,操作一删掉两个相同字符等同于将某一字符的数量减二,那么我们可 ...