三者关系
| 单位 | 内存 | 切换成本 | 通信 |
|---|---|---|---|
| 进程 | 独立 | 高(内核态) | IPC / 管道 |
| 线程 | 共享地址空间 | 中 | 共享内存,需加锁 |
| 协程 | 共享线程 | 低(用户态) | 通道 / 变量 |
为什么协程更轻
线程由操作系统调度,上千条就会让上下文切换吃光 CPU;协程在用户态由运行时调度,单线程可挂几万协程,遇到等待(IO)就让出,不占内核资源。
代价与坑
- 共享内存要加锁:线程/协程共用变量,竞态会出诡异 bug;
- 别在协程里算重 CPU:阻塞调度器会卡住同线程其他协程;
- 理解并行与并发:多核才是并行,单核只是并发切换。
一句话选型
CPU 密集用多进程 / 多线程吃满核;IO 密集用协程或异步,用少量线程扛高并发。
实战案例:三个并发选型失误
- CPU 密集任务用了协程:图像压缩跑在单线程事件循环里,整个服务被卡住。CPU 密集应交给多进程或线程池,别阻塞调度器。
- 线程池按核数配却忽略等待:任务是纯计算却把线程数设得远超核数,上下文切换开销反而更大。线程数应结合等待时间与计算时间估算。
- 共享可变状态没加锁:多线程下计数丢失、缓存错乱。并发写共享状态必须加锁或用原子操作,优先考虑不可变数据与消息传递。
常见问题(FAQ)
协程一定比线程轻吗?创建与切换更轻,但一旦执行阻塞调用仍会卡住所在调度器,必须配合异步 I/O。多进程有什么代价?内存不共享、进程间通信更贵、启动更慢;换来隔离与真正的并行。并发数设多少合适?I/O 密集可按“并发 ≈ 吞吐 × 平均耗时”估算,再用压测验证,别凭感觉调大。怎么排查并发 Bug?用竞态检测工具、可复现的压测脚本,并在日志中带请求 ID,别只靠偶发复现。
并发程序的测试与观测
并发缺陷的难点在于复现,因此测试与观测手段比编码技巧更关键。以下几点在实践中最有价值。
- 把并发点显式化:共享状态、临界区与锁的顺序都应能在代码里一眼看出。若需要读很久才能判断某处是否线程安全,说明设计已经过于隐晦,应重构为更清晰的边界。
- 用注入式调度做测试:把等待与调度做成可注入的依赖,测试时用可控的顺序强制交错,能稳定复现“先检查后使用”这类竞态,而不是靠随机等待碰运气。
- 压测要覆盖突发:并发问题常在流量突增时暴露,压测应包含阶梯上升与瞬间尖峰两种模式,观察队列积压、拒绝率与延迟分位数,而不是只看平均吞吐。
- 观测线程与协程状态:导出线程栈、协程数量与队列长度,并设阈值告警。协程数量持续增长通常意味着阻塞调用混入了异步路径,或任务未能正确结束。
- 明确背压策略:当生产速度超过消费速度时,必须显式选择:限流、丢弃或堆积。没有背压设计的并发系统在压力下会以不可预期的方式失败。
容量与资源的关系
并发度并非越高越好。每个并发单元都占用内存与句柄,且会争抢 CPU。把并发度与资源上限、下游承载能力一起评估,才能得到既不过载也不闲置的取值。
异步代码的调试体验
异步调用栈通常不完整,出错时难以定位调用来源。建议为每个任务携带可追踪的上下文标识,并在日志中一致输出;同时在异常处理里保留原始堆栈,避免经过层层包装后丢失关键信息。这些习惯能显著缩短异步问题的排查时间。