01-并发基础概念
Python 3.11+
概念铺垫
并发与并行
实际场景
一个 Web 应用需要同时处理 100 个并发请求。如果逐个处理,用户等待时间会非常长。如何让这些请求"同时"进行?
问题:如何理解并发和并行的区别?
并发(Concurrency) 和 并行(Parallelism) 是两个容易混淆的概念。
┌─────────────────────────────────────────────────────────────┐
│ 并发 vs 并行 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 并发(Concurrency): │
│ • 多个任务交替执行,看起来同时运行 │
│ • 单核 CPU 也能实现 │
│ • 重点:任务调度,提高响应性 │
│ │
│ 并行(Parallelism): │
│ • 多个任务真正同时执行 │
│ • 需要多核 CPU │
│ • 重点:计算能力,提高吞吐量 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 单核 CPU(并发) │ │
│ │ 时间 ────────────────────────────────────────────► │ │
│ │ 任务 A: ████────████────████ │ │
│ │ 任务 B: ────████────████────████ │ │
│ │ (交替执行) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 多核 CPU(并行) │ │
│ │ 核心 1: ████████████████████████████████ │ │
│ │ 核心 2: ████████████████████████████████ │ │
│ │ (同时执行) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘生活比喻
┌─────────────────────────────────────────────────────────────┐
│ 生活比喻 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 并发:一个人同时处理多件事 │
│ • 厨师一边煮汤,一边切菜,一边看火 │
│ • 实际上是交替做,但看起来同时进行 │
│ │
│ 并行:多个人同时处理多件事 │
│ • 三个厨师分别煮汤、切菜、看火 │
│ • 真正同时进行 │
│ │
└─────────────────────────────────────────────────────────────┘Python GIL
什么是 GIL
GIL(Global Interpreter Lock,全局解释器锁)是 CPython 的一个机制,确保同一时刻只有一个线程执行 Python 字节码。
┌─────────────────────────────────────────────────────────────┐
│ GIL 的作用 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Python 进程 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────┐ │ │
│ │ │ GIL │ ← 同一时刻只有一个线程能持有 │ │
│ │ └────┬────┘ │ │
│ │ │ │ │
│ │ ┌────┴────────────────────────┐ │ │
│ │ │ 线程池 │ │ │
│ │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │
│ │ │ │线程1│ │线程2│ │线程3│ │ │ │
│ │ │ └─────┘ └─────┘ └─────┘ │ │ │
│ │ └───────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 结果:多线程无法利用多核 CPU 进行计算 │
│ │
└─────────────────────────────────────────────────────────────┘I/O 密集型 vs CPU 密集型
┌─────────────────────────────────────────────────────────────┐
│ 任务类型对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ I/O 密集型: │
│ • 大部分时间在等待 I/O │
│ • CPU 空闲,适合多线程 │
│ • 示例:网络请求、文件读写、数据库查询 │
│ │
│ CPU 密集型: │
│ • 大部分时间在计算 │
│ • CPU 繁忙,受 GIL 限制 │
│ • 示例:数值计算、图像处理、加密解密 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ I/O 密集型任务时间线 │ │
│ │ ████████░░░░░░░░░░░░████████░░░░░░░░░░░░████████ │ │
│ │ │ 计算 │ 等待 I/O │ 计算 │ 等待 I/O │ 计算 │ │ │
│ │ └──────┴───────────┴──────┴───────────┴──────┘ │ │
│ │ ↑ 大部分时间在等待 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ CPU 密集型任务时间线 │ │
│ │ ████████████████████████████████████████████████ │ │
│ │ │ 计算 │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ │ ↑ 大部分时间在计算 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘分层学习
L1 理解层:会用
GIL 的影响 — 代码演示
python
# gil_impact_demo.py
from __future__ import annotations
import threading
import time
def cpu_bound_task(n: int) -> int:
"""CPU 密集型任务"""
count: int = 0
for i in range(n):
count += i
return count
# 单线程
start: float = time.time()
cpu_bound_task(10_000_000)
cpu_bound_task(10_000_000)
print(f"单线程: {time.time() - start:.2f}s")
# 多线程(受 GIL 限制,不会更快)
start: float = time.time()
t1: threading.Thread = threading.Thread(target=cpu_bound_task, args=(10_000_000,))
t2: threading.Thread = threading.Thread(target=cpu_bound_task, args=(10_000_000,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f"多线程: {time.time() - start:.2f}s")
# 结果:多线程可能更慢(线程切换开销)GIL 什么时候释放
┌─────────────────────────────────────────────────────────────┐
│ GIL 释放时机 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. I/O 操作时 │
│ • 文件读写 │
│ • 网络请求 │
│ • 用户输入 │
│ → 多线程对 I/O 密集型任务有效 │
│ │
│ 2. 时间片到期 │
│ • Python 3.2+:每 5ms 切换一次 │
│ • 线程自愿释放 GIL │
│ │
│ 3. 执行 C 扩展代码时 │
│ • NumPy、Pandas 等库 │
│ • 可以绕过 GIL │
│ │
└─────────────────────────────────────────────────────────────┘选择并发模型
| 任务类型 | 推荐模型 | 原因 |
|---|---|---|
| I/O 密集型 | 多线程 / asyncio | GIL 在 I/O 时释放,可并发执行 |
| CPU 密集型 | 多进程 | 绕过 GIL,利用多核 |
| 混合型 | 多进程 + 多线程 | 根据任务特点组合使用 |
L2 实践层:用好
推荐做法表
| 推荐做法 | 说明 | 为什么 |
|---|---|---|
| 先判断任务类型再选并发模型 | I/O 型用线程/协程,CPU 型用进程 | 错误的模型会导致性能更差 |
| 先写串行版本再优化 | 正确性优先于性能 | 并发代码调试困难,串行版本可作为参考基准 |
用 time.perf_counter() 测量 | 避免 time.time() 受系统时钟调整影响 | 性能分析需要高精度计时 |
| 临界并发数 = CPU 核数 | CPU 密集型场景 | 超过核数会产生上下文切换开销,反而降低吞吐 |
反模式对比
| ❌ 反模式 | ✅ 正确做法 | 说明 |
|---|---|---|
| 混淆并发和并行 | 先判断任务类型:I/O 还是 CPU | 以为多线程就能加速所有任务 |
| 忽略 GIL 影响 | CPU 密集型用多进程 | 用多线程做 CPU 计算,反而更慢 |
| 过早优化 | 先写正确的串行版本,再优化 | 一开始就写并发代码增加复杂度 |
| 无限制创建线程 | 使用线程池 ThreadPoolExecutor | 线程有内存和调度开销,超量反而降低性能 |
适用场景表
| 场景 | 是否推荐并发 | 原因 |
|---|---|---|
| 批量下载网页 | ✅ 推荐 | I/O 密集型,加速明显 |
| 批量计算素数 | ❌ 不推荐用线程 | 受 GIL 限制,改用多进程 |
| 处理单个小文件 | ❌ 不推荐 | 并发开销 > 收益 |
| Web 服务器响应请求 | ✅ 推荐 | 高并发 I/O 场景 |
L3 专家层:深入
GIL 的 C 语言实现
GIL 本质上是 CPython 解释器中的一个 互斥锁(mutex),位于 _PyRuntime.gil。
┌─────────────────────────────────────────────────────────────┐
│ GIL 内部实现机制 │
├─────────────────────────────────────────────────────────────┤
│ │
│ CPython 中的 GIL 结构(简化): │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ _PyRuntime.gil │ │
│ │ ├── mutex ← 互斥锁 │ │
│ │ ├── cond ← 条件变量(线程等待用) │ │
│ │ ├── locked ← 是否被持有 │ │
│ │ ├── switch_interval ← 切换间隔(默认 5ms) │ │
│ │ └── last_holder ← 最后持有者(调试用) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 获取 GIL 流程: │
│ 1. 尝试 acquire mutex │
│ 2. 如果 locked → wait on cond(挂起) │
│ 3. 获取成功后 set locked = True │
│ 4. 执行字节码 │
│ 5. 达到 switch_interval → release │
│ │
└─────────────────────────────────────────────────────────────┘GIL 释放机制详解
python
# gil_release_demo.py
"""演示 GIL 在 I/O 操作时释放"""
import threading
import time
gil_released: bool = False
def io_bound_task() -> None:
global gil_released
with open("/dev/null", "r") as f:
gil_released = True
time.sleep(0.1) # time.sleep 也释放 GIL
def cpu_bound_task() -> None:
"""此任务持有 GIL 直到切换间隔"""
total: int = 0
for i in range(10_000_000):
total += i
t1 = threading.Thread(target=io_bound_task)
t2 = threading.Thread(target=cpu_bound_task)
t1.start()
t2.start()
t1.join()
t2.join()GIL 释放操作对照表:
| 操作 | GIL 状态 | 其他线程能运行吗? |
|---|---|---|
| Python 字节码执行 | 持有 | ❌ 不能 |
time.sleep() | 释放 | ✅ 能 |
file.read() | 释放 | ✅ 能 |
socket.recv() | 释放 | ✅ 能 |
| NumPy 矩阵运算 | 释放(C 扩展) | ✅ 能 |
python
# gil_check_interval_demo.py
"""
检查 GIL 切换间隔(Python 3.2+ 默认 5ms)
sys.getswitchinterval() 返回线程切换间隔(秒)。
间隔越小,线程切换越频繁,但上下文切换开销也越大。
"""
import sys
print(f"线程切换间隔: {sys.getswitchinterval()}s") # 默认 0.005 (5ms)
sys.setswitchinterval(0.001) # 改为 1ms(不建议)性能考量表
| 操作 | 单线程耗时 | 多线程耗时 | 说明 |
|---|---|---|---|
| CPU 计算 2×10⁷次加法 | ~0.5s | ~0.7s | 多线程更慢(GIL+切换开销) |
| I/O 等待 10×0.5s | ~5.0s | ~0.5s | 多线程加速 10 倍 |
| 混合任务 | ~3.0s | ~1.5s | 部分加速(I/O 部分释放 GIL) |
性能测试代码:
python
# gil_performance_test.py
import time
import threading
from concurrent.futures import ThreadPoolExecutor
def cpu_work() -> int:
return sum(i * i for i in range(1_000_000))
def io_work() -> None:
time.sleep(0.5)
# CPU 密集型:多线程更慢
start = time.time()
for _ in range(4):
cpu_work()
single_cpu = time.time() - start
start = time.time()
with ThreadPoolExecutor(max_workers=4) as executor:
list(executor.map(cpu_work, range(4)))
multi_cpu = time.time() - start
print(f"CPU 单线程: {single_cpu:.2f}s")
print(f"CPU 多线程: {multi_cpu:.2f}s")
# 预期:multi_cpu >= single_cpu
# I/O 密集型:多线程显著加速
start = time.time()
for _ in range(4):
io_work()
single_io = time.time() - start
start = time.time()
with ThreadPoolExecutor(max_workers=4) as executor:
list(executor.map(lambda _: io_work(), range(4)))
multi_io = time.time() - start
print(f"I/O 单线程: {single_io:.2f}s")
print(f"I/O 多线程: {multi_io:.2f}s")
# 预期:multi_io ≈ single_io / 4Python 3.13+ 无 GIL 模式(前瞻性)
引入版本:Python 3.13(实验性) 启动方式:
python -X free-threading
Python 3.13 引入了 PEP 703(Making the Global Interpreter Lock Optional in CPython),允许在编译时禁用 GIL。
| 特性 | 说明 |
|---|---|
| 状态 | 实验性,Python 3.13 默认关闭 |
| 启用方式 | 编译时配置 --disable-gil |
| 影响 | 内存开销增加 ~10%,单线程性能下降 ~5% |
| 适用场景 | 多线程 CPU 密集型任务可真正并行 |
设计动机
Python 为什么设计 GIL?
| 设计选择 | 原因 | 替代方案对比 |
|---|---|---|
| 保留 GIL | 简化 C 扩展开发,保证线程安全 | Java 无 GIL,但 C API 更复杂 |
| I/O 时释放 | 让 I/O 密集型任务受益 | 所有 I/O 调用都经过 C 层 |
| 不取消 GIL(历史原因) | 大量 C 扩展依赖 GIL 保证安全 | 3.13 开始探索可选 GIL |
知识关联图
并发知识关联:
┌───────────────────┐
│ 并发 vs 并行 │ ← 概念基础
└────────┬──────────┘
│
▼
┌───────────────────┐ ┌───────────────────┐
│ GIL │────→│ 线程调度机制 │
│ (全局解释器锁) │ │ 5ms 切换间隔 │
└────────┬──────────┘ └───────────────────┘
│
┌────┴────┐
▼ ▼
┌───────┐ ┌───────────┐
│ I/O型 │ │ CPU 型 │
│多线程 │ │ 多进程 │
└───┬───┘ └─────┬─────┘
│ │
▼ ▼
┌───────┐ ┌───────────┐
│asyncio│ │进程间通信 │
│协程 │ │Queue/Pipe │
└───────┘ └───────────┘本章小结
┌─────────────────────────────────────────────────────────────┐
│ 并发基础概念 知识要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 并发 vs 并行: │
│ ✓ 并发:交替执行,单核也能实现 │
│ ✓ 并行:同时执行,需要多核 │
│ │
│ GIL: │
│ ✓ CPython 的全局解释器锁 │
│ ✓ 同一时刻只有一个线程执行字节码 │
│ ✓ I/O 操作时释放 │
│ ✓ Python 3.13+ 开始实验无 GIL 模式 │
│ │
│ 任务类型: │
│ ✓ I/O 密集型:等待为主,适合多线程/asyncio │
│ ✓ CPU 密集型:计算为主,适合多进程 │
│ │
│ 选择模型: │
│ ✓ I/O 密集型 → 多线程 / asyncio │
│ ✓ CPU 密集型 → 多进程 │
│ │
└─────────────────────────────────────────────────────────────┘