concurrency
Backlinks (1)
1 · concurrency#
https://en.wikipedia.org/wiki/Concurrency_(computer_science)
S 一个程序需要同时处理多个任务: Web 服务同时处理多个请求,GUI 既要响应用户输入又要后台加载数据,数据库一边执行查询一边刷盘和复制。 C 现实世界里只有有限的 CPU 核、线程、锁、网络连接和内存带宽;多个执行流一旦共享状态,就会产生竞争、阻塞、死锁、饥饿、顺序不确定等问题。 Q 并发到底是什么?它和并行、异步有什么区别?写并发程序时真正要解决的核心问题是什么? A 并发是一种把多个独立任务组织为“可重叠推进”的程序结构;它不等于并行,也不等于异步。并发编程的核心是在有限资源下协调多个执行流,控制共享状态和时序,保证正确性、性能与可维护性。
1.1 · 1. 什么是并发#
并发 (concurrency) 关心的是:
- 系统里有多个任务要推进
- 这些任务的步骤可以交错执行
- 程序需要定义它们如何协作、通信、取消、等待和收尾
所以并发首先是一个结构问题,不是一个硬件问题。
一个简单判断:
- 如果你的程序里只有一条控制流,从头跑到尾,那通常不是并发
- 如果你的程序需要管理多条“逻辑上同时存在”的任务,那就是并发
1.2 · 2. 并发、并行、异步的区别#
1.2.1 · 并发 vs 并行#
- 并发: 多个任务在同一时间段内都在推进,步骤可以交错
- 并行: 多个任务在同一时刻真的在不同 CPU 核上同时执行
关系:
- 并发是程序结构
- 并行是运行时执行状态
- 并发程序可以运行在单核上,此时只有交错,没有真正同时执行
- 并行通常需要并发结构才能被有效利用
一句话:
并发回答“怎么组织多个任务”,并行回答“是不是同时真的在跑”。
1.2.2 · 并发 vs 异步#
- 同步/异步描述的是“怎么等结果”
- 并发描述的是“有没有多条任务一起推进”
例如:
- 单线程事件循环 +
async/await是异步,也通常是一种并发结构 - 多线程阻塞式服务器是并发,但未必使用 async/await
- 一个 Future/Promise API 可以是异步接口,但如果调用方立刻
await,整体未必形成有效并发
可以把它们分开理解:
- 并发: 任务之间的组织方式
- 异步: 控制流如何在等待时让出执行权
- 并行: 底层是否真有多个核心同时执行
1.3 · 3. 为什么并发难#
并发难点不在“同时做很多事”,而在“很多事交错时仍然正确”。
典型问题:
- 竞态条件 (race condition): 结果依赖于不可预测的执行顺序
- 数据竞争 (data race): 两个执行流并发访问同一内存位置,至少一个是写,且没有同步
- 原子性破坏:
x = x + 1不是一个不可分割的动作 - 可见性问题: 一个线程写了值,另一个线程未必立刻看见
- 顺序问题: 编译器、CPU、缓存系统都会重排
- 死锁: 多方互相等待,永远不前进
- 活锁: 大家都在动,但系统没有实质进展
- 饥饿: 某个任务长期抢不到资源
- 背压缺失: 生产速度大于消费速度,队列无限膨胀
- 取消与清理困难: 任务停不下来,资源回收不完整
并发 bug 的难点在于:
- 低概率
- 对时序敏感
- 难复现
- 测试覆盖不到所有交错顺序
1.4 · 4. 并发真正要管理的对象#
写并发程序,本质上是在管理下面几件事:
1.4.1 · 4.1 任务#
任务是逻辑上的工作单元,例如:
- 处理一个请求
- 执行一个后台同步
- 刷新缓存
- 计算一个结果
1.4.2 · 4.2 执行体#
执行体是承载任务的运行机制,例如:
- OS 线程
- goroutine
- coroutine / fiber
- 事件循环中的回调
不要把“任务”和“线程”绑定得太死。 一个好的并发模型通常允许:
- 任务数量远大于线程数量
- 任务在等待时让出执行体
1.4.3 · 4.3 共享资源#
多个任务常常会共享:
- 内存中的对象
- 文件句柄
- socket
- 数据库连接池
- CPU 时间
- 锁和队列
并发控制的核心就是规定:
- 谁能访问
- 什么时候访问
- 以什么顺序访问
- 访问失败或取消时如何收尾
1.5 · 5. 常见并发模型#
1.5.1 · 5.1 Shared-memory + lock#
多个线程共享同一地址空间,通过锁保护临界区。
优点:
- 直观,贴近操作系统线程模型
- 适合对共享内存做细粒度操作
缺点:
- 容易出现锁顺序问题、死锁、伪共享、锁竞争
- 正确性高度依赖程序员纪律
适用场景:
- 多核 CPU 密集任务
- 需要直接共享内存结构的低延迟组件
1.5.2 · 5.2 Message passing#
任务之间尽量不共享可变状态,而是通过消息通信。
代表思路:
- Go channel
- actor model
- CSP 风格
优点:
- 降低共享状态复杂度
- 让“所有权”和“边界”更清晰
缺点:
- 复制、序列化、排队会带来开销
- 不合理的 channel/queue 设计照样会阻塞或泄漏
适用场景:
- 服务内部流水线
- worker pool
- 状态封装明确的组件
1.5.3 · 5.3 Event loop#
一个或少量线程负责轮询 IO,就绪后分发任务继续执行。
优点:
- 少线程就能支撑大量 IO 连接
- 避免“一个连接一个线程”的成本
缺点:
- 不能让 CPU 密集任务长时间阻塞事件循环
- 调试调用栈和阻塞点通常更复杂
适用场景:
- 网络服务器
- GUI
- 高并发 IO 系统
1.5.4 · 5.4 Structured concurrency#
把并发任务纳入明确的父子生命周期中:
- 谁创建,谁负责等待
- 子任务失败时如何传播
- 取消时是否整体退出
这是现代并发设计里非常重要的一条原则。
它解决的是“任务能启动,但没人收尸”的问题。
关键词:
- scope
- cancellation
- join
- timeout
- error propagation
1.6 · 6. 正确性的核心: 同步原语#
为了让多个执行流在时序上达成一致,需要同步原语。
常见原语:
- mutex: 一次只允许一个执行流进入临界区
- rwlock: 多读单写
- semaphore: 限制并发数量
- condition variable: 等待某个条件成立
- barrier: 多个参与者都到达某点后再继续
- atomic: 对单个变量进行不可分割的读改写
- channel / queue: 用通信隐式表达同步
原则:
- 能缩小共享状态,就不要扩大锁范围
- 能通过所有权转移解决,就少用共享可变内存
- 能用高层抽象表达,就不要过早落到裸 atomic
1.7 · 7. 性能不是“线程越多越好”#
并发的目标不只是“多开几个线程”。
常见性能误区:
- 线程开太多,导致上下文切换变重
- 锁竞争严重,CPU 都耗在等待上
- 队列过深,延迟上升
- 细粒度任务过多,调度成本超过计算本身
- 多核程序被缓存一致性流量拖慢
所以并发设计通常要在几个目标之间平衡:
- 吞吐量: 单位时间能做多少工作
- 延迟: 单个任务多久完成
- 资源占用: 线程、内存、连接数
- 公平性: 是否有任务长期饿死
- 可预测性: 在高负载下是否退化平滑
1.8 · 8. 设计并发程序时先问什么#
在写代码前,先回答这几个问题:
- 任务边界是什么?
- 哪些数据是共享的?哪些可以变成私有?
- 正确性依赖哪些顺序保证?
- 失败和取消如何传播?
- 谁负责等待子任务结束?
- 是否需要背压,队列上限是多少?
- 这是 IO 密集还是 CPU 密集?
- 能否把并发限制在少数明确的组件里?
一个经验法则:
先设计所有权,再设计同步;先减少共享,再考虑怎么锁。
1.9 · 9. 工程实践#
1.9.1 · 9.1 优先避免共享可变状态#
最容易出 bug 的不是并发本身,而是共享的可变状态。
优先级通常是:
- 不共享
- 只读共享
- 所有权转移
- 不得不共享时再加同步
1.9.2 · 9.2 明确生命周期#
任何后台任务都应回答:
- 什么时候启动
- 什么时候退出
- 谁取消它
- 谁等待它结束
如果回答不清,这个并发设计大概率不完整。
1.9.3 · 9.3 限制并发度#
“能并发”不代表“应该无限并发”。
常见做法:
- worker pool
- semaphore
- bounded queue
- rate limit
1.9.4 · 9.4 把取消当成一等公民#
真实系统中,任务经常会因为:
- 超时
- 用户离开
- 上游失败
- 服务关闭
而被取消。取消路径如果没有设计好,资源泄漏和悬挂任务就会出现。
1.9.5 · 9.5 观测并发系统#
并发系统必须有可观测性:
- 队列长度
- 锁等待时间
- goroutine / thread 数量
- 超时率
- 任务取消率
- 吞吐与尾延迟
否则系统“看起来卡住了”,你很难知道卡在哪。
1.10 · 10. 一个最小心智模型#
可以用下面这套心智模型理解大多数并发系统:
- 系统里有很多任务
- 任务需要一些执行体才能推进
- 某些步骤需要等待外部事件
- 某些步骤需要访问共享资源
- 同步原语负责约束访问顺序
- 调度器决定谁先跑
- 取消、超时、错误传播决定系统如何收尾
如果这几个元素你都画得出来,这个并发系统通常就能被解释清楚。
1.11 · 11. 和相关主题的关系#
- async: 关注等待语义和控制流挂起,不等于并发本身
- Go: concurrency pattern: 关注 Go 里的 goroutine、channel、Future 风格模式
- memory: 关注缓存一致性、伪共享、内存访问代价,这些直接影响并发性能
- Network Programming: 高并发网络服务通常依赖 event loop、线程池和背压机制
1.12 · 12. 一句话总结#
并发不是“让代码同时跑起来”这么简单,而是:
在有限资源下,让多个任务安全地交错推进,并在共享状态、失败、取消和性能之间做清晰的设计取舍。