Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the wp-pagenavi domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /var/www/blog.zhujinhui.net/wp-includes/functions.php on line 6114

Notice: 函数 _load_textdomain_just_in_time 的调用方法不正确twentyseventeen 域的翻译加载触发过早。这通常表示插件或主题中的某些代码运行过早。翻译应在 init 操作或之后加载。 请查阅调试 WordPress来获取更多信息。 (这个消息是在 6.7.0 版本添加的。) in /var/www/blog.zhujinhui.net/wp-includes/functions.php on line 6114
Python面试必问5:什么是GIL? – 煅魂-JeffreyChu的修炼屋

Python面试必问5:什么是GIL?

GIL是Python中一个非常重要的概念,几乎所有的python开发面试都会问到这个问题,今天我们来细细盘点下。

概念(重点)

GIL(Global Interpreter Lock,全局解释器锁) 是Python解释器(特别是CPython实现)中的一个机制。它是一种互斥锁,用于保护Python解释器内部的全局状态,确保同时只有一个线程执行Python字节码。换句话说,GIL限制了多线程程序中多个线程的并行(注意不是并发)执行。

GIL产生原因

  • GIL简化了Python解释器的内存管理保护(多)线程安全。Python使用引用计数机制来管理内存,GIL确保了引用计数操作的原子性,避免了多线程环境下引用计数出错的问题。如果没有GIL,多线程同时操作引用计数时,可能会导致内存泄漏或崩溃
  • CPython的历史遗留问题,CPython是Python的最早实现,设计时并没有充分考虑多线程并发的支持。
  • 提高单线程性能
  • 第三方C扩展的兼容性需要进行大改才能确保线程安全。

工作原理

GIL的持有和释放过程可以分为以下几个步骤:

获取GIL
  • 当一个线程要执行Python字节码时,它首先必须获取GIL。
  • 获取GIL的过程是互斥的,即同一时刻只有一个线程能成功获取GIL。
  • 如果GIL已经被其他线程持有,当前线程将进入等待状态,直到GIL被释放。
执行Python代码
  • 一旦线程获取了GIL,它便可以开始执行Python字节码。
  • 线程执行过程中,每经过一定数量的字节码指令(如100个字节码指令),解释器会检查是否需要释放GIL,以便让其他线程有机会执行。
释放GIL
  • 当线程完成了一定数量的字节码指令或进入I/O(如文件读取、网络请求等)操作时,它会释放GIL。
  • 释放GIL后,其他等待的线程可以尝试获取GIL并开始执行。
再度获取GIL

如果一个线程在释放GIL后仍需继续执行,它必须重新尝试获取GIL。这意味着,即使是同一个线程,也无法保证连续长时间持有GIL。
以下是一个简化的示例,描述了GIL的获取和释放:

import threading

def thread_function():
    while True:
        # 尝试获取GIL
        with gil:
            # 执行Python字节码
            execute_bytecode()
            # 每执行一定数量的字节码指令后,检查是否需要释放GIL
            if bytecode_count >= threshold:
                bytecode_count = 0
                # 释放GIL
                release_gil()

# 创建多个线程
threads = [threading.Thread(target=thread_function) for _ in range(10)]
for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

在这个简化的示例中,线程在执行过程中会周期性地释放GIL,让其他线程有机会执行。这种机制虽然限制了多线程的并行性,但也确保了Python解释器的稳定和内存管理的简单性。

影响

CPU密集型任务

GIL限制了多线程程序的并行执行,使得CPU密集型任务无法充分利用多核CPU的计算能力,导致性能提升有限甚至下降。

I/O密集型任务

在I/O密集型任务中,由于I/O操作会释放GIL,其他线程可以在等待I/O操作时执行,从而实现较好的并发性,受GIL的影响较小。因此,多线程在I/O密集型任务中仍然能够提供显著的性能提升。

如何绕过GIL

多进程方案

使用多进程(multiprocessing模块)绕过GIL:

  • 原理:多进程通过创建多个独立的进程来绕过GIL,每个进程都有自己的Python解释器和内存空间,因此它们不会争夺GIL,可以实现真正的并行执行。
  • 实现方法: Python的multiprocessing模块提供了方便的接口来创建和管理多个进程。
  • 优点:可以充分利用多核CPU,真正并行执行,提升性能。
  • 缺点:进程间通信开销较大,内存使用量较多。
原生线程库

使用C扩展或其他语言(如Cython)绕过GIL:

  • 原理:通过使用C语言编写Python扩展模块或使用Cython编写部分代码,可以在扩展模块中释放GIL,让计算密集型任务并行执行。
  • 实现方法:
  • C扩展: 在C代码中使用Python提供的API(如Py_BEGIN_ALLOW_THREADS和Py_END_ALLOW_THREADS)释放和重新获取GIL。
  • Cython: 在Cython代码中,通过声明nogil块来释放GIL。
  • 优点:可以在特定的代码段中实现并行计算,提升性能。
  • 缺点:需要额外的开发工作和学习成本,复杂度增加。
异步编程

Python中的异步编程(asyncio):

  • 原理:异步编程通过事件循环和协程的方式处理并发任务,适用于I/O密集型任务。与多线程不同,异步编程不需要创建多个线程,而是通过单线程处理多个I/O操作,避免了GIL的限制。
  • 实现方法:使用asyncio模块,通过async和await关键字定义和执行异步任务。
  • 优点:高效处理I/O密集型任务,资源占用少,代码相对简洁。
  • 缺点:不适用于CPU密集型任务,需要对异步编程模式有一定的理解。

未来

Python社区一直在探索替代GIL的方案,以提升多线程性能。未来,可能会引入细粒度锁或其他并发模型,但这需要确保向后兼容和性能稳定。PyPy等替代解释器也在积极尝试绕过GIL的限制。

不过目前看来,任重而道远啊!

总结

GIL是Python解释器中的全局锁,限制了多线程并行执行,尤其影响CPU密集型任务,但对I/O密集型任务影响较小。尽管它简化了内存管理,提升了单线程性能,但限制了多核利用。多进程、C扩展和异步编程是绕过GIL的有效方案。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注