如果 Python 编程中使用了多线程,估计都听说过 GIL,全局解释器锁,Global Interpreter Lock

很多人一听这玩意儿,第一反应就是:这不就是 Python 最大的缺陷设计嘛!其实 GIL 并不是毫无用处,它有历史背景,也有优缺点。

今天我就来带你从零开始聊聊它,保证讲得接地气。

GIL 介绍

通俗涞水,GIL 就是一把 “全局的锁”,在任何时刻,只允许一个线程真正执行 Python 字节码

是不是感觉很离谱?多线程居然不能并行?

那为啥要设计这么个东西呢?

原因其实挺简单:Python(准确说是 CPython 解释器)在管理内存的时候,不是线程安全的。

如果没有锁,不同线程同时修改内存数据,很容易造成崩溃。所以 GIL 相当于一个“安全保护罩”,保证同一时刻只有一个线程在操作解释器内部的数据。

一句话总结:GIL 是 Python 为了简单省事搞出来的妥协方案。

特点

GIL 有几个特别容易被误解的特点,具体如下:

1、多线程不是真并行(CPU 密集型任务下)

比如:你开 10 个线程跑计算任务,结果可能还没单线程快,因为线程之间可能还要 “争夺”这把锁

2、I/O 密集型任务友好

读写文件、网络请求这种,线程会经常被阻塞,Python 会把 GIL 释放给别的线程。

所以这种场景,多线程还是能带来提升。

3、GIL 和多进程没关系

多进程每个进程都有自己的解释器和 GIL,互不干扰。针对 CPU 密集型任务,用多进程才是正解

实战

我们来写一个小例子:用多线程跑计算密集型任务(比如循环计算),再对比单线程。

import threading
import time

def cpu_task():
    count = 0
    for i in range(10**7):
        count += i
    return count

def run_threads():
    threads = []
    for _ in range(4):  # 开 4 个线程
        t = threading.Thread(target=cpu_task)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()

if __name__ == "__main__":
    start = time.time()
    run_threads()
    end = time.time()
    print("多线程耗时:", end - start)

    start = time.time()
    for _ inrange(4):
        cpu_task()
    end = time.time()
    print("单线程耗时:", end - start)

运行后,我们发现:多线程并没有比单线程快,甚至更慢

原因就是所有线程都在抢 GIL,互相切换反而增加了开销。

如果换成 I/O 任务,比如:requests.get() 这种,多线程才会明显快一些

GIL 的应对策略

既然 GIL 有限制,那我们实际开发中怎么应对呢?

针对 I/O 密集型任务,使用用多线程

比如:爬虫、网络请求这类,使用 Python 多线程就很香

而针对 CPU 密集型任务,更建议用多进程

用 multiprocessing,或者干脆把耗时计算扔给 C 扩展、NumPy 这种底层释放 GIL 的库

总结

GIL 的本质是 为了让 CPython 简化内存管理,它让 Python 多线程在 CPU 密集型任务下“失色”,但在 I/O 密集型场景仍然有优势。而遇到 CPU 密集型任务时,不要硬刚多线程,直接上多进程或者调用 C 扩展库才是王道。

所以,下次再有人说 “Python 慢,是因为 GIL”,你完全可以这样回复:它仅是限制了 CPU 多线程,其他方面不会有影响!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐