📡 I/O 操作与 GIL 释放详解

线程在 I/O 等待时如何释放 GIL 并实现切换

🎯 场景设定
假设有 Thread-A(主线程)Thread-B(工作线程)
Thread-A 需要执行计算任务,Thread-B 需要发起网络请求
requests.get(url)
file.read()
socket.recv()
time.sleep()
🔒 持有 GIL,执行中
🔓 释放 GIL
📡 I/O 等待
⏳ 等待 GIL
⭐ Thread-A
🔹 Thread-B
T0 T1 T2 T3 T4 T5 T6
T1
T2
T3
T4
T5
执行计算
CPU 任务
等待
让出 CPU
继续执行
接过 GIL
就绪
等待调度
开始执行
获取 GIL
发起 I/O
准备释放
等待响应
不需要 CPU
处理结果
重新获取
GIL
1
T0 → T1:Thread-A 先获得 GIL 开始执行
操作系统调度 Thread-A 获得 CPU 时间片,Thread-A 调用 threading.Lock() 获取 GIL, 开始执行自己的 Python 代码(计算任务)
2
T1 → T2:Thread-B 被调度,获得 GIL
时间片轮转到 Thread-B,Thread-B 获取 GIL 后开始执行自己的代码
此时 Thread-A 还没执行完,但没关系! Thread-A 虽然失去 CPU,但它的 GIL 也被释放了,它处于"就绪/等待"状态
3
T2 → T3:Thread-B 发起 I/O 请求,释放 GIL ⭐
这是关键!Thread-B 执行到 I/O 调用时:
# Python 内部检测到这是阻塞式 I/O 调用 response = requests.get("https://api.example.com/data") # ↑ # 调用 C 层面的 socket 操作 # ↓ # Python 检测到需要等待外部响应 # ↓ # 自动释放 GIL!!!
⭐ 核心原理:Python 的 I/O 函数(如 socket.readfile.read) 底层调用 C 库,Python 解释器知道"这个操作会阻塞",于是主动释放 GIL
4
T3 → T4:Thread-B 等待 I/O 响应,Thread-A 获得 GIL
Thread-B 进入 I/O 等待状态,不消耗 CPU:
# Thread-B 的状态变化 Thread-B.state = "BLOCKED" # 被 OS 标记为阻塞 Thread-B.gil = null # 不持有 GIL # Thread-A 检测到 GIL 可用 Thread-A.state = "RUNNING" # 被 OS 调度 Thread-A.gil = acquired # 重新获取 GIL,继续执行
这就是多线程的价值!Thread-B 在等待网络响应时,完全不占用 CPU, Thread-A 可以充分利用这段时间执行自己的任务
5
T4:I/O 响应返回,Thread-B 重新就绪
当网络响应到达时:
# 操作系统通知:I/O 完成! Thread-B.state = "READY" # 变为就绪状态 # 等待 OS 下一次调度,获取 GIL 后继续执行
6
T5 → T6:Thread-A 和 Thread-B 继续交替执行
循环往复,两个线程通过 OS 调度 + GIL 机制实现"伪并发"
💡 关键结论
I/O 操作时 GIL 释放的本质:

1. Python 的 I/O 函数(socketfilerequests 等) 底层调用 C 库,Python 解释器能识别"这是一个需要等待的操作"

2. 识别后,Python 会主动释放 GIL,而不是让线程空等

3. 其他线程检测到 GIL 可用 + 被 OS 调度后,就能获取 GIL 继续执行

4. 结果:多个线程在 I/O 等待期间可以交替执行,充分利用 CPU 资源

这就是为什么多线程对 I/O 密集型任务有帮助: 等待的时候不占 CPU,别人就可以用!