并发编程的核心可以归纳为三个问题:如何分工,如何协作,如何保护共享状态。语言和运行时提供线程、协程、事件循环、Actor、channel 等不同工具,但工具本身并不会自动带来正确性。
生产系统还必须处理任务生命周期、资源上限、超时、取消、背压与部分失败。只有把这些约束放进同一套设计中,并发才能从“同时运行多个任务”变成可推理、可观测、可控制的工程能力。
先把四组概念分开
并发不是并行
并发描述多个任务在时间上重叠、需要被协调;并行描述多个任务在同一时刻真正执行。单核事件循环可以并发但不能并行执行 Python 代码,多核上的多个工作线程则可能同时具备两者。
Amdahl 定律仍然成立:串行部分决定固定工作量下的加速上限。2026 年更应补充两个现实约束:协调本身有成本,扩容会把瓶颈推向锁、内存带宽、数据库连接或下游限额。增加线程从来不是免费的加速器。
同步/异步与阻塞/非阻塞不是一回事
同步和异步主要描述结果如何交付以及控制流如何组织;阻塞和非阻塞描述一次操作不能立即完成时,调用线程是否停住。它们是两个维度,不能简单把 BIO、NIO、AIO 填进一个永远正确的四象限。
同一个异步 API 也可能在内部阻塞工作线程;一个阻塞 API 放到 Java 虚拟线程上,代码仍然阻塞,但平台线程可以去执行其他任务。工程判断应关注:谁在等待、占用了什么资源、能否取消,而不是只看 API 名字里有没有 async。
进程、平台线程、轻量任务
进程拥有独立地址空间和故障边界;平台线程共享进程内存,由操作系统调度;goroutine、Java 虚拟线程和 Python asyncio Task 是由语言运行时管理的更轻量并发单元,但三者实现和语义并不相同。
协程会在挂起点交出执行权。多个任务对共享可变状态进行读-改-写时,仍会发生逻辑竞态。单线程事件循环只消除同一时刻的多核并行,不会自动保证跨 await 的不变量。因此,协程代码同样需要明确状态所有权和临界区。
内存模型:真正的问题是顺序关系
并发 Bug 通常来自三个维度:可见性、原子性和有序性。理解它们的关键不是推测 CPU 缓存的具体行为,而是依据语言内存模型判断操作之间是否建立了明确的 happens-before 关系。
在 Java 中,volatile 的意义来自 Java 内存模型:对 volatile 字段的写 happens-before 随后的读,从而建立可见性与顺序保证;它不使 count++ 这样的复合操作变成原子操作。synchronized、锁、线程启动与结束、并发容器也通过各自规则建立 happens-before。
Go 的结论类似但语法不同:如果多个 goroutine 并发访问同一位置且至少一个是写,必须使用 channel、sync 或 sync/atomic 建立同步。官方内存模型给出的最好建议非常朴素:不要写需要反复推演内存模型才能证明正确的“聪明代码”。
锁的粒度取决于不变量和竞争特征。现实系统可以使用锁分段、读写锁和乐观并发;核心要求是:每一项共享状态必须有明确、始终一致的同步协议。多把锁共同保护一个不变量时,必须固定获取顺序并证明不会死锁。
Java:虚拟线程改变的是成本,不是正确性
虚拟线程已经在 Java 21 正式交付。对高并发 I/O 服务,直接、可调试的阻塞式代码可以为每个任务分配一个虚拟线程,Thread-Per-Task 不再天然昂贵。
但虚拟线程不是“更快的线程”,也不会让 CPU 密集型代码突破核心数。更重要的是,不能用线程池大小去限制数据库或第三方 API 的并发,因为虚拟线程本来就应当充足。对稀缺资源应使用 Semaphore、连接池或速率限制器做显式准入控制。
var permits = new Semaphore(64);
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var future = executor.submit(() -> {
permits.acquire();
try {
return callDownstream();
} finally {
permits.release();
}
});
return future.get();
}
Java 25 中的 Structured Concurrency 仍是预览 API,不应写成已经永久定型的标准。但它代表了正确方向:父任务拥有子任务,退出作用域前等待它们完成;一个子任务失败时,兄弟任务可以被取消;超时和错误沿任务树传播,而不是留下无人管理的 Future。
Go:channel 是同步工具,不是可靠消息队列
Go 仍然擅长以 goroutine 和 channel 表达并发协作。发送与对应接收、关闭与观察到关闭之间可以建立 happens-before,这让 channel 不仅传值,也传递顺序。
但“通过通信共享内存”不是“所有共享状态都必须用 channel”。计数器、缓存和短临界区经常用 sync.Mutex 更清楚。选择标准是所有权:任务流和所有权移交适合 channel;同一对象的短时互斥适合锁;简单计数或状态位才考虑原子操作。
生产代码必须回答 channel 教程经常遗漏的问题:谁关闭 channel、缓冲区多大、消费者变慢时谁被阻塞、取消怎样传播、goroutine 怎样退出。context.Context 应沿调用链传递 deadline 与 cancellation;队列必须有界,否则“异步”只是把过载变成内存增长。
Python:GIL 不再是一句结论
对默认 CPython,I/O 密集任务仍适合 asyncio 或线程,CPU 密集的纯 Python 工作通常使用多进程、原生扩展或独立计算服务。但从 Python 3.13 开始,CPython 提供可选的 free-threaded 构建,可以关闭 GIL 并在多个核心上并行运行 Python 线程。它还不是“升级后自动变快”:部分扩展可能重新启用 GIL,线程安全与性能都需要重新测试。
在异步代码中,asyncio.TaskGroup 比散落的 create_task() 更适合表达一组同生共死的任务:离开作用域会等待所有子任务;首个非取消异常会取消其他任务并聚合异常。清理逻辑应放在 try/finally 中,收到 CancelledError 后通常应在清理完成后继续传播。
async def load_dashboard():
async with asyncio.TaskGroup() as group:
user = group.create_task(load_user())
projects = group.create_task(load_projects())
return user.result(), projects.result()
任何阻塞 I/O 都不应直接运行在事件循环里;可以使用原生异步驱动,或用 asyncio.to_thread() 隔离遗留阻塞调用。对 CPU 任务,则应根据运行时选择进程池、free-threaded 线程或外部执行器。
从并发模型走向结构化并发
Reactor、Future、Callback、Actor、CSP 仍然有价值,但它们回答的是不同问题:Reactor 管理就绪事件;Future 表示未来结果;Actor 把状态封装在消息接收者中;CSP 强调通过通信过程协作。
2026 年最重要的新共识是任务生命周期也必须被建模。一个并发任务不应比创建它的请求活得更久,除非它被明确移交给一个长期拥有者。结构化并发把任务组织成树,使完成、失败、取消和观测都有清晰父子关系。
生产级并发的八条检查线
- 先定义目标:是降低单请求延迟、提高吞吐,还是隔离故障?目标不同,模型不同。
- 区分任务类型:I/O 等待、CPU 计算、混合任务分别评估,不凭语言偏好选线程数。
- 限制在途数量:线程轻量不等于数据库连接、内存、文件描述符和 API 配额无限。
- 队列必须有界:容量、满载策略、优先级和丢弃语义都应显式设计。
- 传播 deadline 与取消:超时是预算,不是某一层随意设置的独立计时器。
- 明确状态所有权:优先不可变数据、单写者和消息传递;共享可变状态必须有同步协议。
- 让失败可观察:记录队列等待时间、在途任务、拒绝量、取消量、锁竞争和下游耗时,而不只看平均 QPS。
- 测试交错而非只测结果:配合 race detector、压力测试、故障注入和虚拟时间,覆盖取消、超时、重复执行与部分失败。
设计模式仍然有效,但要补上边界
Immutability 仍是降低共享复杂度的首选;Copy-On-Write 适合读多写极少的数据,不适合大型集合的频繁写入;ThreadLocal 必须考虑线程池复用与清理,在虚拟线程时代也不应拿来承载大型缓存;Worker Thread 的核心价值是资源隔离和容量控制,而不只是复用线程。
生产者-消费者模式最容易被误用。队列可以解耦生产和消费,却不能消灭速度差:当平均生产速度长期高于消费速度,任何有限队列都会满,任何无限队列都会耗尽内存。正确答案是背压、拒绝、降级、扩容或减少输入,而不是再加一层队列。
如何定义现代并发编程
并发编程不是“同时做更多事”,而是在有限资源和不确定失败下,管理多个任务的生命周期与状态关系。
线程、锁、channel 和执行模型只是实现手段。作用域、取消、背压、资源预算和可观测性同样属于并发设计。工具变得更轻,创建并发变得更容易,这也让约束并发成为更重要的工程能力。
真正成熟的系统,不是能启动一百万个任务,而是知道其中多少应该启动、何时停止、失败影响谁,以及系统过载时如何保持可控。