← 返回文章列表

进程、线程与协程:并发的三种执行单位

并发入门

三者关系

单位内存切换成本通信
进程独立高(内核态)IPC / 管道
线程共享地址空间中共享内存,需加锁
协程共享线程低(用户态)通道 / 变量

为什么协程更轻

线程由操作系统调度,上千条就会让上下文切换吃光 CPU;协程在用户态由运行时调度,单线程可挂几万协程,遇到等待(IO)就让出,不占内核资源。

代价与坑

  1. 共享内存要加锁:线程/协程共用变量,竞态会出诡异 bug;
  2. 别在协程里算重 CPU:阻塞调度器会卡住同线程其他协程;
  3. 理解并行与并发:多核才是并行,单核只是并发切换。

一句话选型

CPU 密集用多进程 / 多线程吃满核;IO 密集用协程或异步,用少量线程扛高并发。

实战案例:三个并发选型失误

  1. CPU 密集任务用了协程:图像压缩跑在单线程事件循环里,整个服务被卡住。CPU 密集应交给多进程或线程池,别阻塞调度器。
  2. 线程池按核数配却忽略等待:任务是纯计算却把线程数设得远超核数,上下文切换开销反而更大。线程数应结合等待时间与计算时间估算。
  3. 共享可变状态没加锁:多线程下计数丢失、缓存错乱。并发写共享状态必须加锁或用原子操作,优先考虑不可变数据与消息传递。

常见问题(FAQ)

协程一定比线程轻吗?创建与切换更轻,但一旦执行阻塞调用仍会卡住所在调度器,必须配合异步 I/O。多进程有什么代价?内存不共享、进程间通信更贵、启动更慢;换来隔离与真正的并行。并发数设多少合适?I/O 密集可按“并发 ≈ 吞吐 × 平均耗时”估算,再用压测验证,别凭感觉调大。怎么排查并发 Bug?用竞态检测工具、可复现的压测脚本,并在日志中带请求 ID,别只靠偶发复现。

并发程序的测试与观测

并发缺陷的难点在于复现,因此测试与观测手段比编码技巧更关键。以下几点在实践中最有价值。

  1. 把并发点显式化:共享状态、临界区与锁的顺序都应能在代码里一眼看出。若需要读很久才能判断某处是否线程安全,说明设计已经过于隐晦,应重构为更清晰的边界。
  2. 用注入式调度做测试:把等待与调度做成可注入的依赖,测试时用可控的顺序强制交错,能稳定复现“先检查后使用”这类竞态,而不是靠随机等待碰运气。
  3. 压测要覆盖突发:并发问题常在流量突增时暴露,压测应包含阶梯上升与瞬间尖峰两种模式,观察队列积压、拒绝率与延迟分位数,而不是只看平均吞吐。
  4. 观测线程与协程状态:导出线程栈、协程数量与队列长度,并设阈值告警。协程数量持续增长通常意味着阻塞调用混入了异步路径,或任务未能正确结束。
  5. 明确背压策略:当生产速度超过消费速度时,必须显式选择:限流、丢弃或堆积。没有背压设计的并发系统在压力下会以不可预期的方式失败。

容量与资源的关系

并发度并非越高越好。每个并发单元都占用内存与句柄,且会争抢 CPU。把并发度与资源上限、下游承载能力一起评估,才能得到既不过载也不闲置的取值。

异步代码的调试体验

异步调用栈通常不完整,出错时难以定位调用来源。建议为每个任务携带可追踪的上下文标识,并在日志中一致输出;同时在异常处理里保留原始堆栈,避免经过层层包装后丢失关键信息。这些习惯能显著缩短异步问题的排查时间。