本章中的关键主题,建议结合示例反复练习。
并发编程概述
约 2192 字大约 7 分钟
2026-05-10
并发编程研究的是:如何让程序在同一段时间内处理多个任务。
- 并发与并行
- 同步与异步
- 阻塞与非阻塞
- CPU 密集型与 IO 密集型
建议先建立整体地图,再按章节逐步深入。
本章中的关键主题,建议结合示例反复练习。
本章中的关键主题,建议结合示例反复练习。
本章中的关键主题,建议结合示例反复练习。
本章中的关键主题,建议结合示例反复练习。
并发编程研究的是:如何让程序在同一段时间内处理多个任务。
在现实项目中,程序经常需要等待网络、磁盘、数据库、用户输入等外部资源。如果程序只能按顺序等待,就会浪费大量时间。并发编程的目标,就是在一个任务等待时,让程序可以继续推进其他任务。
并发与并行
并发(Concurrency) 指的是多个任务在同一段时间内交替推进。它强调“任务管理能力”。
并行(Parallelism) 指的是多个任务在同一时刻真正同时执行。它强调“执行能力”。
举个例子:
- 一个人同时煮饭、洗菜、烧水,是并发:他在多个任务之间切换。
- 三个人分别煮饭、洗菜、烧水,是并行:多个任务真的同时进行。
在计算机中:
- 单核 CPU 可以并发,但不能真正并行执行 Python 字节码。
- 多核 CPU 可以并行,但 Python 是否能充分利用多核,还要看使用的是线程、进程还是其他机制。
同步与异步
同步(Synchronous) 指的是调用一个操作后,必须等待它完成,才能继续向下执行。
result = download(url)
print(result)异步(Asynchronous) 指的是发起一个操作后,不必一直等待它完成,可以先去做别的事,等结果准备好后再处理。
task = asyncio.create_task(download(url))
print('先做别的事')
result = await task同步和异步描述的是任务发起后,程序如何等待结果。
阻塞与非阻塞
阻塞(Blocking) 指的是当前执行流被卡住,直到操作完成。
非阻塞(Non-blocking) 指的是操作无法立即完成时,不会卡住当前执行流,而是立刻返回某种状态。
阻塞/非阻塞更偏底层 IO 行为;同步/异步更偏程序组织方式。它们经常一起出现,但不是同一个概念。
CPU 密集型与 IO 密集型
选择并发方案前,最重要的是判断任务类型。
CPU 密集型
CPU 密集型任务主要消耗 CPU 计算能力,例如:
- 大量数学计算
- 图片处理
- 视频编码
- 加密解密
- 数据压缩
这类任务想提速,通常需要真正的并行计算。Python 中更适合使用多进程或把关键计算交给 C 扩展、NumPy 等释放 GIL 的库。
IO 密集型
IO 密集型任务主要在等待外部资源,例如:
- 网络请求
- 文件读写
- 数据库查询
- 调用第三方 API
- 等待用户输入
这类任务等待时间多,CPU 常常是空闲的。Python 的多线程和 asyncio 都适合处理 IO 密集型任务。
Python 的主要并发方式
Python 常见并发方式有四类:
| 方式 | 代表模块 | 适合场景 |
|---|---|---|
| 多线程 | threading | 简单 IO 并发、后台任务 |
| 多进程 | multiprocessing | CPU 密集型、多核并行 |
| 线程池 / 进程池 | concurrent.futures | 批量任务并发执行 |
| 协程 | asyncio | 高并发网络 IO |
如何选择
可以先按下面的规则判断:
- 任务主要在等网络、文件、数据库:优先线程池或 asyncio。
- 任务主要在做 CPU 计算:优先进程池。
- 只是想同时跑几个简单任务:优先
concurrent.futures。 - 要处理大量网络连接:优先 asyncio。
- 需要和同步库配合:线程池通常更简单。
- 不确定怎么选:先写同步版本,再根据瓶颈选择并发方案。
并发编程的核心问题
并发带来性能提升的同时,也会带来复杂性:
- 多个任务如何启动?
- 如何等待任务结束?
- 如何传递结果?
- 如何处理异常?
- 如何取消任务?
- 如何避免共享数据被同时修改?
- 如何优雅退出?
后续章节会分别介绍线程、锁、队列、进程、线程池、进程池、asyncio、GIL 和实践建议。
学习路线
建议按这个顺序学习:
- 先理解并发、并行、同步、异步、阻塞、非阻塞。
- 学习
threading,理解线程的启动和等待。 - 学习锁和队列,理解共享状态的风险。
- 学习
multiprocessing,理解进程隔离和多核并行。 - 学习
concurrent.futures,掌握线程池和进程池。 - 学习
asyncio,理解事件循环和协程。 - 学习 GIL,理解为什么 Python 多线程不能直接加速 CPU 密集型任务。
- 最后用最佳实践总结如何选择方案。
先用生活场景理解并发
并发不是“让代码看起来高级”,而是让程序在等待时不要闲着。
想象你在厨房做饭:
烧水需要 5 分钟
煮面需要 3 分钟
切菜需要 2 分钟如果你严格按顺序做:先盯着水烧开,再煮面,再切菜,就会浪费很多等待时间。更自然的做法是:水在烧的时候去切菜,水开后再煮面。
程序也是一样。网络请求、读写文件、等待数据库返回时,CPU 经常在“等”。并发就是让程序在等待 A 的时候,先去做 B。
并发代码先问三个问题
写并发前,先别急着选线程、进程还是 asyncio。先问:
- 任务主要是在等外部结果,还是在消耗 CPU?
- 每个任务之间是否需要共享数据?
- 失败一个任务时,其他任务应该继续还是一起停止?
这三个问题比背 API 更重要。
| 问题 | 如果答案是 | 常见选择 |
|---|---|---|
| 大部分时间在等网络/文件 | IO 密集型 | 线程、asyncio |
| 大部分时间在算东西 | CPU 密集型 | 多进程 |
| 任务数量很多但每个很轻 | 高并发 IO | asyncio / 线程池 |
| 需要复用现有同步库 | 同步 API | 线程池 |
| 任务之间共享状态很多 | 共享数据复杂 | 先简化设计,少共享 |
初学者不要一上来追求最快
并发会带来额外复杂度:
- 代码执行顺序不再那么直观;
- 报错可能藏在线程、进程或任务里;
- 共享变量可能出现竞态条件;
- 程序退出时要考虑任务有没有收尾;
- 并发太高反而会拖垮自己或对方服务。
所以学习顺序建议是:
先写出正确的同步版本
↓
确认瓶颈确实在等待或计算
↓
选择一种最简单的并发方式
↓
加上超时、异常处理和并发数量限制这比一开始就堆很多并发技巧更稳。
一个简单对比
同步写法:
import time
def download(name):
print('开始', name)
time.sleep(1)
print('完成', name)
for i in range(3):
download(i)线程写法:
import threading
import time
def download(name):
print('开始', name)
time.sleep(1)
print('完成', name)
threads = []
for i in range(3):
t = threading.Thread(target=download, args=(i,))
t.start()
threads.append(t)
for t in threads:
t.join()如果任务主要是在等待,第二种写法通常会更快完成。但如果任务是纯计算,线程不一定能带来明显提升,这就和 GIL 有关。
总结
并发编程不是越多越好,也不是越复杂越高级。它的本质是根据任务特点,选择合适的执行模型,让程序更高效地利用等待时间或硬件资源。
- 并发的核心是让程序在等待时不要闲着,不是盲目追求复杂写法。
- 先判断任务是 IO 密集还是 CPU 密集,再选择线程、进程或 asyncio。
- 初学者应先写正确的同步版本,再逐步加入并发、超时、异常处理和并发限制。
版权所有
版权归属:Shuo Liu
