知识卡片

TPC与prethread:每连接一线程解决旧问题,又引入新问题

结构图卡

内容

TPC(Thread Per Connection)是[[PPC与prefork:每连接一进程模型的三大瓶颈]]的直接演化:每来一个新连接就创建一个线程去处理,而不是创建一个进程。因为线程比进程更轻量,创建代价明显更低,而且多线程共享同一进程的内存空间,线程间通信自然比进程间通信(IPC)简单得多,所以TPC本质上是解决(或至少大幅弱化)了PPC的两个核心痛点:fork代价高和父子进程通信复杂——流程上父进程accept连接后创建子线程处理读写和业务,子线程处理完直接close即可(不像PPC还要父进程也配合close一次,因为线程共享文件描述符,不存在复制)。但TPC并非没有代价,它引入了三个新问题:一是创建线程虽然比创建进程轻,但依然不是零成本,在真正的高并发场景(比如每秒上万连接)下仍然会遇到性能瓶颈;二是线程间不再需要进程通信,但共享内存本身带来了互斥和同步的复杂度,稍不注意就可能引入死锁;三是多线程共享同一进程空间意味着彼此会互相影响,某个线程一旦出现异常(比如内存越界),有可能直接拖垮整个进程。而且TPC依然没有摆脱CPU线程调度和上下文切换的开销这个和PPC类似的底层限制——正因如此,在并发数只有几百这个量级的场景下,实践中反而更多选择PPC而不是TPC,因为PPC没有死锁风险,也不存在多线程互相拖累的问题,稳定性反而更高。prethread是TPC对应的”预创建”变体,思路和prefork一致:提前把线程创建好,省去现场创建线程的开销;但因为多线程之间数据共享和通信天然方便,prethread在实现上比prefork更灵活,常见方式有主进程accept后把连接分发给某个线程处理、或者让多个子线程都尝试accept、最终由操作系统保证只有一个成功两种。Apache的MPM worker模式本质上就是一种prethread方案,并做了一个关键改进:先创建多个进程,每个进程内部再创建多个线程——这样即使某个进程内某个线程异常导致整个进程退出,其他进程依然能继续对外提供服务,不会造成服务器整体宕机,这个”进程套线程”的设计本身就是在拿多进程的隔离性去弥补多线程的脆弱性。理论上prethread能比prefork支撑更多并发,Apache MPM worker模式默认支持16×25=400个并发处理线程。

结构图

flowchart TB
  A["TPC:每连接一线程"]
  A --> B["解决PPC的:fork代价高<br/>+父子进程通信复杂(线程更轻,内存共享)"]
  A --> C["新引入:高并发下创建线程仍有代价"]
  A --> D["新引入:线程互斥/共享带来死锁风险"]
  A --> E["新引入:线程异常可能拖垮整个进程"]
  C --> F["实践:几百并发场景反而更常用PPC<br/>因为PPC无死锁风险、多进程互不拖累"]
  D --> F
  E --> F
  A --> G["prethread变体:预先创建线程<br/>省去现场创建开销"]
  G --> H["Apache MPM worker:多进程套多线程<br/>某线程异常只拖垮所在进程,其他进程仍可用<br/>默认16×25=400并发线程"]

参考来源

- 位置:《从零开始学架构》第18讲《单服务器高性能模式:PPC与TPC》"TPC""prethread"(源文件:_epub-src/OEBPS/text00001.html) - 结论依据:原文说明"TPC 实际上是解决或者弱化了 PPC fork 代价高的问题和父子进程通信复杂的问题",并列出三个新问题"创建线程……高并发时……还是有性能问题……线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题……某个线程出现异常时,可能导致整个进程退出",总结"在并发几百连接的场景下,反而更多地是采用 PPC 的方案,因为 PPC 方案不会有死锁的风险,也不会多进程互相影响,稳定性更高",并说明Apache MPM worker"首先创建多个进程,每个进程里面再创建多个线程……即使某个子进程里面的某个线程异常导致整个子进程退出,还会有其他子进程继续提供服务",直接支撑本卡片结论与结构图。 - 原始内容:TPC 实际上是解决或者弱化了 PPC fork 代价高的问题和父子进程通信复杂的问题……在并发几百连接的场景下,反而更多地是采用 PPC 的方案,因为 PPC 方案不会有死锁的风险,也不会多进程互相影响,稳定性更高……Apache 服务器的 MPM worker 模式……首先创建多个进程,每个进程里面再创建多个线程。