深入理解Python多线程与GIL机制

陆遥
陆遥
发布时间:2026-10-11 | 2人浏览过
原创

本文深入探讨Python多线程与全局解释器锁(GIL)的工作机制。通过8核CentOS系统的实测数据,对比单线程、多线程及限制CPU核心数下的执行耗时,揭示GIL对CPU密集型任务的限制。文章解析了PyThreadState结构、线程创建流程及GIL的获取与释放逻辑,帮助开发者理解Python并发编程的本质与局限。

Python多线程性能实测

为了直观理解GIL对多线程性能的影响,我们在8核CentOS系统上进行了基准测试。测试对象为一个简单的CPU密集型函数,通过递减计数器模拟计算负载。

单线程基准测试

首先,我们运行单线程版本的CPU密集型程序。代码逻辑如下:

import time
def fun(n):#CPU密集型的程序
  while(n>0):
    n -= 1

start_time = time.time()
fun(10000000)
print('{} s'.format(time.time() - start_time))#测量程序执行时间

在8核全开状态下,单线程执行该程序的平均耗时约为 0.968370994 秒。这反映了单个CPU核心处理该任务的基准性能。

多线程并行测试

接着,我们创建两个线程并行执行相同的函数。代码实现如下:

import time
import threading

def fun(n):
  while(n>0):
    n -= 1

start_time = time.time()
t1 = threading.Thread( target=fun, args=(10000000,) )
t1.start()
t2 = threading.Thread( target=fun, args=(10000000,) )
t2.start()

t1.join()
t2.join()
print('{} s'.format(time.time() - start_time))

测试结果显示,两个线程并行执行的平均耗时约为 2.150056044 秒。如果理想情况下两个线程能完全并行利用两个核心,耗时应接近单线程的 0.968 秒左右。然而实际耗时几乎是单线程的两倍,这表明多线程并未带来预期的加速效果,反而因上下文切换等开销略有增加。

限制CPU核心数的测试

为了排除其他核心干扰,我们禁用了7个CPU核心,仅保留一个核心工作。在Linux系统中,可以通过写入 /sys/devices/system/cpu/cpuX/online 文件来禁用核心。操作示例如下:

[xxx]# echo 0 > /sys/devices/system/cpu/cpu1/online
[xxx]# echo 0 > /sys/devices/system/cpu/cpu2/online
[xxx]# echo 0 > /sys/devices/system/cpu/cpu3/online
[xxx]# echo 0 > /sys/devices/system/cpu/cpu4/online
[xxx]# echo 0 > /sys/devices/system/cpu/cpu5/online
[xxx]# echo 0 > /sys/devices/system/cpu/cpu6/online
[xxx]# echo 0 > /sys/devices/system/cpu/cpu7/online

在仅有一个核心可用的情况下,多线程程序的平均耗时约为 2.533491453 秒。这一结果进一步证实,在CPU密集型任务中,Python的多线程无法实现真正的并行计算,其性能甚至低于理想化的单线程执行。

GIL机制与线程实现原理

Python多线程使用的是C语言实现的真实系统线程,由操作系统内核进行调度。然而,Python解释器引入了全局解释器锁(GIL),确保同一时刻只有一个线程在执行Python字节码。这是导致上述性能测试现象的根本原因。

线程创建与PyThreadState

在Python中创建一个新线程,主要包含以下步骤:创建 PyThreadState 结构体、调用底层 pthread 创建系统线程、并执行线程函数。PyThreadState 结构体用于保存当前线程的执行状态,其定义大致如下:

typedef struct _ts {
  struct _ts *next; # 链表指正
  PyInterpreterState *interp; # 解释器状态
  struct _frame *frame; # 当前的stack frame
  int recursion_depth; # 当前的递归深度
  int tracing;
  int use_tracing;
  Py_tracefunc c_profilefunc;
  Py_tracefunc c_tracefunc;
  PyObject *c_profileobj;
  PyObject *c_traceobj;
  PyObject *curexc_type;
  PyObject *curexc_value;
  PyObject *curexc_traceback;
  PyObject *exc_type;
  PyObject *exc_value;
  PyObject *exc_traceback;
  PyObject *dict;
  int tick_counter;
  int gilstate_counter;
  PyObject *async_exc;
  long thread_id; # 线程ID
} PyThreadState;

该结构体保存了当前栈帧、递归深度、线程ID以及Hooks等关键信息。在Python 3.1及之前的版本中,存在一个全局变量 _PyThreadState_Current 指向当前正在执行的线程状态。

GIL的控制与释放

GIL通过信号(signal)机制实现,而非简单的锁。线程在获取GIL前会检查其状态,若未被释放,则进入睡眠等待信号。当线程释放GIL时,会发送信号通知其他线程。

对于CPU密集型任务,由于不涉及I/O阻塞,线程不会主动释放GIL。此时,解释器会周期性地进行检查(check),以执行线程调度,避免某个线程独占CPU。这个检查周期默认为 100 个tick,每个tick大致相当于一汇编指令的执行时间。解释器的check行为包括复位tick计数器、检查主线程信号以及释放GIL。

信号处理与调度

值得注意的是,只有主线程负责处理信号,子线程不直接处理信号。此外,Python多线程编程中没有线程优先级的概念,线程调度完全依赖于操作系统。GIL通过signal机制协调线程执行,每一时刻只允许一个线程持有GIL并执行代码。当线程遇到I/O阻塞时,会自动释放GIL,允许其他线程获取锁并执行,这也是Python多线程在I/O密集型任务中表现较好的原因。

总结

通过实测与源码分析,我们可以清晰地看到Python多线程在CPU密集型任务中的局限性。GIL的存在使得Python无法利用多核CPU进行真正的并行计算。对于CPU密集型场景,建议使用多进程(multiprocessing)而非多线程;而对于I/O密集型场景,多线程依然是一个有效的并发模型。理解GIL的工作原理,有助于开发者根据具体业务场景选择合适的并发策略。

以上就是深入学习python多线程与GIL的详细内容,更多关于Python并发编程、GIL机制及性能优化的资料请关注本站其它相关文章!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表