☰
Python基础之GIL锁是什么及其对爬虫的影响详析
2026/10/13 11:36:11 网站建设 项目流程

前言


先说一个措辞上的准确性问题。标题里的「GIL锁」其实是个叠词:GIL 全称 Global Interpreter Lock,本身就带着「Lock」。更重要的纠正是两点:


第一,GIL 是 CPython 的实现细节,不是 Python 语言的规定。你写的 Python 代码交给别的实现(比如 JPython)跑,就不一定有这把锁。所以精确的说法是「CPython 的 GIL」,而不是「Python 的 GIL」。


第二,GIL 和你在代码里import threading; lock = threading.Lock()用的那把锁是两回事。threading.Lock是你为了保护共享数据自己加的;GIL 是解释器内部为了自保加的。混为一谈是理解多线程时最常见的绊脚石。


本文讲清三件事:GIL 到底是什么、它为什么存在、以及它对爬虫这类程序到底有什么影响。示例以 CPython 3.8 及以上为基准,涉及 3.13 实验性自由线程构建的行为会单独标注。


一、GIL 到底是什么


一句话:GIL 让同一进程内、同一时刻只有一个线程在执行 Python 字节码。


拆开看这几个限定词都很关键:



  • 同一进程内:GIL 是每个解释器进程一把,不是全局唯一。开两个进程,就有两把 GIL,能真正并行。

  • 同一时刻:GIL 会在多个线程之间轮转,让每个线程都分到执行机会,只是任意瞬间只有一个在跑。

  • 执行 Python 字节码:注意,是「执行字节码」时才必须持有 GIL。当线程在等待 I/O、在sleep、在很多标准库的阻塞调用里时,它会释放 GIL,别的线程就能趁虚而入。


最后一点是理解「为什么多线程对爬虫有效」的全部关键。


二、GIL 为什么存在


GIL 不是设计失误,而是为了让 CPython 的实现变简单:



  • 引用计数(reference counting)是 CPython 的内存管理方式。每次对象被引用或取消引用,计数器都要加一减一。如果多个线程同时改同一个对象的计数器,就需要给每个对象都加锁,开销巨大,还极易写出竞态。用一把全局锁保护整个解释器状态,简单得多。

  • C 扩展的兼容性:大量第三方 C 扩展默认「有 GIL 保护就不会被并发打断」,因此写起来可以不管锁。这也是移除 GIL 极难的原因——牵扯整个生态。


一句话:GIL 用「牺牲多核并行」换来了「实现简单 + 扩展生态稳定」。


三、对两类任务,后果完全不同




任务类型典型例子多线程有用吗原因



CPU 密集大量数值计算、图像处理、压缩基本没用大家抢同一把 GIL,多核用不上

I/O 密集网络请求、读写文件、等数据库有用等待时会释放 GIL,别人能接着跑



官方文档在threading模块页说得很直接:如果想让程序更好地利用多核机器的计算资源,应当改用multiprocessing或concurrent.futures.ProcessPoolExecutor;而线程依然适合同时跑多个 I/O 密集任务。


这个区分对爬虫极其重要,因为爬虫几乎纯是 I/O 密集:程序 90% 以上的时间在等网络返回,CPU 只是在解析少量 HTML。等待期间线程释放 GIL,所以多线程爬虫是有效的。


四、GIL 对爬虫的具体影响


好消息:GIL 基本不阻碍多线程爬虫。因为爬虫的瓶颈在网络上,不在 CPU 上。


坏消息藏在细节里,主要有三点:


1. 解析/计算密集的那部分仍然不并行。如果你在拿到页面后做大量正则回溯、大规模 HTML 解析、或者跑机器学习模型,这些是 CPU 密集的,多线程跑不出来加速。这时把解析部分放到进程池,或换用带 C 扩展的解析库,更划算。


2. 「多线程能提升 N 倍」是错的。实际吞吐上限由目标站点响应速度、你的带宽、以及对方的限流策略共同决定。线程开到 200 个,多半只是让一部分线程在排队,还容易被对方识别为异常流量。不要相信任何没实测过的倍数说法。想比较就用time.perf_counter()自己量。


3. GIL 的切换会带来开销。线程过多时,解释器花在切换上的时间占比会上升。线程数应当和「并发等待的 I/O 数量」匹配,而不是越大越好。切换间隔由sys.setswitchinterval()控制,默认值约几毫秒——这是全局设置,不要在业务代码里随手改。


五、想绕过 GIL,有几条路




方案机制适用与代价



multiprocessing每个进程一个解释器,各有一把 GIL真并行;代价是数据要序列化、进程开销大

concurrent.futures.ProcessPoolExecutor同上,接口更统一适合把 CPU 密集的部分切出去

C 扩展 /numpy一类的库在原生代码里主动释放 GIL真并行;但本机没有编译工具链就没法自己写 C 扩展

asyncio单线程事件循环,靠await切换高并发 I/O 很省资源;但生态要适配异步

自由线程构建(3.13+)关掉 GIL,真正并行跑线程实验性、非默认、需单独安装



关于最后一条,务必说准:CPython 3.13 起提供了实验性的自由线程(free-threaded)构建,可以在运行时禁用 GIL,实现线程的真正并行执行。但官方文档明说这是实验特性、默认不启用,需要单独的可执行文件(安装时选 free-threaded 版本,或自行用--disable-gil编译)。所以不要说「Python 已经没有 GIL 了」——默认构建里它依然在。想检查当前解释器是否跑在自由线程模式下,可以用sys._is_gil_enabled()。


对爬虫而言,实际情况是:主流做法本来就是多线程(或 asyncio)跑 I/O + 必要时多进程做 CPU 密集的解析,这条路不依赖自由线程构建,今天就能用。


六、一个能观察 GIL 行为的实验


下面这段代码不需要任何第三方库。用两种方式各跑一次同样的「做饭」任务:一种是纯等待(模拟 I/O),一种是纯计算(模拟 CPU 密集)。它会打印出实际耗时,你可以亲眼看到多线程只在第一种情况下才有收益。


# 适用于 Python 3.8+
import threading
import time


def io_task():
time.sleep(0.5) # 模拟等待网络:会释放 GIL


def cpu_task():
total = 0
for i in range(3_000_000): # 模拟纯计算:全程要持有 GIL
total += i * i
return total


def run_all(fn, n=4):
threads = [threading.Thread(target=fn) for _ in range(n)]
start = time.perf_counter()
for t in threads:
t.start()
for t in threads:
t.join()
return time.perf_counter() - start


if __name__ == "__main__":
print(f"I/O 型,4 线程耗时:{run_all(io_task):.3f} 秒")

start = time.perf_counter()
for _ in range(4):
cpu_task()
serial = time.perf_counter() - start
print(f"CPU 型,串行 耗时:{serial:.3f} 秒")
print(f"CPU 型,4 线程耗时:{run_all(cpu_task):.3f} 秒")

预期现象:I/O 型的 4 线程耗时远小于串行相加的 2 秒;而 CPU 型的 4 线程耗时不会明显低于串行——因为计算全程都在抢 GIL。请自己在本机跑一遍看真实数字,本文不给具体倍数。


七、爬虫合规提醒


讲爬虫绕不开合规。无论线程开几个,下面几条是底线:



  • 先看目标站的robots.txt和使用条款,尊重抓取范围的声明;

  • 控制请求频率,用time.sleep或令牌桶限速,不要对单个站点打高并发;

  • 用合理的User-Agent表明身份,不要伪装、不要绕登录墙或付费墙;

  • 只抓公开数据,注意个人信息与版权边界。


反爬机制(检查 UA、频率、Cookie 校验)的成因值得了解,但那是识别异常流量的机制,不是用来研究怎么绕过的。合规抓取既能拿到数据,也不会给对方造成负担。


常见坑点


1. 把threading.Lock当成 GIL


❌ 以为代码里加了with lock:就等于「用好了 GIL」。 ✅ GIL 是解释器内部的锁,和你自己的业务锁无关;保护共享数据仍需自己加threading.Lock。


2. 用多线程做 CPU 密集任务


❌ 用 8 个线程跑大量数值计算,期待 8 倍速。 ✅ 受 GIL 限制没用;改用ProcessPoolExecutor或原生扩展库。


3. 说「Python 已经没有 GIL 了」


❌ 把 3.13 的实验性无 GIL 构建当成默认行为。 ✅ 它是实验性、非默认、要单独安装;默认构建里 GIL 仍在。


4. 线程数开得越大越好


❌ 爬虫动辄开几百个线程。 ✅ 线程数和并发 I/O 数量匹配,配合限速;过多线程增加切换开销且会被对方拦截。


5. 编造「提速 N 倍」


❌ 写「多线程爬虫比单线程快 10 倍」却没有依据。 ✅ 用time.perf_counter()实测,或只讲原理不给数字。


6. 在业务代码里改sys.setswitchinterval()


❌ 随手调小切换间隔以为能加速。 ✅ 它是全局设置,影响整个解释器;非必要不改。


7. 以为 GIL 能代替业务上的同步


❌ 在线程里对共享字典「随便改」,觉得 GIL 会保护自己。 ✅ GIL 只保证单条字节码不被中途抢占;+=、dict的读改删仍需显式同步。


8. 用random生成爬虫里的令牌/签名


❌random.random()生成会话标识。 ✅secrets模块才是密码学安全的随机源。


总结




问题结论



GIL 是什么CPython 内部锁,同一进程同一时刻只有一个线程跑字节码

是语言规范吗不是,是实现细节

对 CPU 密集基本没有加速效果

对 I/O 密集有效,因为等待时会释放 GIL

对爬虫爬虫是 I/O 密集,多线程有效;CPU 密集的解析交给进程

3.13 自由线程实验性、非默认、需单独安装

想真并行多进程、C 扩展、asyncio各有适用场景



把 GIL 记成一句话:它限制的是「同时算」,不限制「同时等」。爬虫干的正是「同时等」的活,所以多线程是合适的工具;而任何需要「同时算」的重活,才该请出多进程或原生扩展。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询