[Python/多线程/并发] Python底层运行机制:全局解释器锁(GIL)

0 序

1 概述:Python全局解释器锁(GIL)

GIL的定义

image

image

image

  • Python全局解释器锁GIL, Global Interpreter Lock)是一个全局级的互斥锁,它确保在任何时候只有一个线程可以控制Python解释器。这意味着:即使在多核处理器的【多线程架构】中,也只有一个线程可以在任何时候执行

image

对于执行【单线程程序】的开发者来说,GIL的影响并不明显,但它可能成为【CPU密集型任务】和【多线程代码任务】的性能瓶颈

image

GIL的工作原理

  • GIL的存在是为了防止多线程同时增加或减少对象的引用计数,这可能会导致内存泄漏或错误释放内存,从而引发崩溃或其他异常错误

GIL通过在【解释器级别】添加一个【规则】来解决这个问题,即:执行任何Python字节码都需要获取【解释器锁】
这避免了【死锁】(因为只有一个锁),并且没有引入太多的【性能开销】。
然而,这实际上使得任何CPU密集型的Python程序都变成了【单线程】。

GIL的影响:不利于CPU密集型程序(多线程程序)

  • GIL对【多线程Python程序】的影响取决于程序是【CPU密集型】还是【I/O密集型】
  • 对于【CPU密集型程序】,GIL阻止了线程在多核处理器上并行执行
  • 对于【I/O密集型程序】,由于线程在等待I/O时【会释放GIL】,因此GIL的【影响相对较小】。

GIL的存在使得在多核CPU上的Python【多线程程序】无法充分利用【多核性能】。

如何处理GIL的限制?

  • 面对GIL的限制,开发者可以采取以下几种方法:
  • 使用多进程:通过使用multiprocessing模块,可以创建多个【进程】来充分利用【多核CPU】。每个进程都有自己的【Python解释器】和【GIL】——因此,能够避免GIL对【多线程程序性能的影响】。

参见本文档: 多进程Python Demo 章节。

  • 使用C扩展:编写关键部分的代码为C扩展,因为C语言中不存在GIL的概念。使用C编写Python的关键性能部分(如CPU密集型计算),可以规避GIL的限制。
  • 异步编程:采用【异步编程模型】,如asyncio库,可以在【I/O密集型任务】中充分利用【事件循环】和【协程】来避免GIL的影响。【异步编程】避免了【线程的阻塞等待】,使得【单个线程】能够处理多个任务。
  • 使用其他Python解释器:一些Python的替代解释器,如JythonIronPython等,不同程度上【规避】了GIL的存在,这些解释器可能更适合某些特定的应用场景
  • 使用线程池:使用concurrent.futures模块中的【线程池】可以减少【线程创建】和【销毁的开销】,【有效管理线程】,并能部分规避GIL的限制。

Python多进程机制(multiprocessing)

注意:Windows 下使用multiprocessing【进程入口代码】必须放在 if __name__ == '__main__' 保护块里面,否则会无限递归创建【子进程】

  1. Windows 必须 if __name__ == '__main__',Linux/macOS 可以不用,但建议统一写上,保证跨平台兼容。
  2. start()启动进程;join()阻塞等待进程结束。
  3. 进程之间【内存隔离】,【全局变量】无法【直接共享】,通信使用 Queue/Pipe/Manager
  4. 【CPU 密集场景】适合多进程;【IO 密集】优先考虑多线程threading或者 asyncio
  5. Pool.map会把【任务序列】分发给进程池,返回结果列表。

Demo1:基础多进程

import multiprocessing
import time

def worker(name: str, delay: int):
    """子进程执行的任务"""
    print(f"子进程 {name} 启动, pid={multiprocessing.current_process().pid}")
    time.sleep(delay)
    print(f"子进程 {name} 执行完毕")


if __name__ == '__main__':
    print(f"主进程 pid={multiprocessing.current_process().pid}")

    # 创建2个子进程
    p1 = multiprocessing.Process(target=worker, args=("进程A", 2))
    p2 = multiprocessing.Process(target=worker, args=("进程B", 1))

    # 启动进程
    p1.start()
    p2.start()

    # 等待子进程结束,主进程再往下执行
    p1.join()
    p2.join()

    print("所有子进程执行完成,主进程退出")
  • 输出示例
主进程 pid=1234
子进程 进程A 启动, pid=5678
子进程 进程B 启动, pid=9012
子进程 进程B 执行完毕
子进程 进程A 执行完毕
所有子进程执行完成,主进程退出

Demo2:进程池 Pool(批量任务,更常用)

  • 适合大量任务,控制并发数量
import multiprocessing
import time


def task(x):
    print(f"处理任务 {x}, pid={multiprocessing.current_process().pid}")
    time.sleep(1)
    return x * x


if __name__ == '__main__':
    # 进程池最大并发数3
    with multiprocessing.Pool(processes=3) as pool:
        # map: 自动分配任务
        results = pool.map(task, range(5))

    print(f"任务结果:{results}")

Demo3:进程之间通信 Queue

  • 多进程不能直接共享变量,用 Queue 传递数据
import multiprocessing


def producer(q: multiprocessing.Queue):
    """生产者:往队列放数据"""
    for i in range(3):
        q.put(i)
        print(f"生产:{i}")


def consumer(q: multiprocessing.Queue):
    """消费者:从队列取数据"""
    while not q.empty():
        val = q.get()
        print(f"消费:{val}")


if __name__ == '__main__':
    queue = multiprocessing.Queue()
    p_pro = multiprocessing.Process(target=producer, args=(queue,))
    p_con = multiprocessing.Process(target=consumer, args=(queue,))

    p_pro.start()
    p_pro.join()

    p_con.start()
    p_con.join()

结论

  • 虽然GIL是Python的一个挑战,但通过合理的技术选择和编程实践,我们能够最大程度地发挥Python的优势,并充分利用多核CPU,提高程序的效率和性能。

了解GIL的工作原理和其对多线程程序的影响,有助于我们更好地理解Python的并发模型,并选择合适的解决方案来优化程序性能。
随着技术的发展和Python生态系统的进步,我们相信对于GIL的限制也会有更多的解决方案和改进,为Python程序的并发性能提供更多可能性。

Y 推荐文献

X 参考文献

posted @ 2026-08-25 15:04  千千寰宇  阅读(6)  评论(0)    收藏  举报