[{"data":1,"prerenderedAt":1708},["ShallowReactive",2],{"\u002F2026\u002F()-why-reactor-and-one-loop-per-thread":3,"surround-\u002F2026\u002F()-why-reactor-and-one-loop-per-thread":1697},{"id":4,"title":5,"body":6,"categories":1675,"date":1677,"description":13,"draft":1678,"extension":1630,"image":1679,"meta":1680,"navigation":1682,"path":1683,"permalink":1679,"published":1679,"readingTime":1684,"recommend":1679,"references":1679,"seo":1689,"sitemap":1690,"stem":1691,"tags":1692,"type":1695,"updated":1677,"__hash__":1696},"content\u002Fposts\u002F2026\u002F网络层(一) why reactor and one-loop per thread.md","网络层(一) why reactor and one-loop per thread",{"type":7,"value":8,"toc":1634},"minimark",[9,14,38,41,46,49,52,59,62,72,75,92,95,131,137,144,151,153,157,160,163,170,188,191,197,200,205,213,305,318,324,329,335,338,340,344,350,353,356,360,387,395,398,402,413,420,426,436,450,454,460,493,495,499,502,768,771,775,786,793,800,804,807,818,822,825,836,840,859,862,864,868,871,877,880,884,904,910,925,932,939,968,974,977,983,990,1006,1013,1015,1019,1022,1026,1045,1056,1062,1071,1075,1093,1099,1102,1106,1121,1131,1133,1137,1140,1144,1208,1219,1223,1230,1314,1320,1329,1332,1336,1343,1404,1411,1417,1423,1441,1445,1448,1454,1460,1463,1467,1488,1490,1494,1497,1550,1553,1567,1570,1572,1575,1622,1625],[10,11,13],"h1",{"id":12},"从零写一个高并发网络运行时一总体架构","从零写一个高并发网络运行时（一）：总体架构",[15,16,17],"blockquote",{},[18,19,20,21,25,26,30,31,30,34,37],"p",{},"这是一个系列博客，记录我毕设项目 ",[22,23,24],"code",{"code":24},"high-concurrency-runtime"," 的网络层设计与实现。\n作为对照系，我会反复参考几个工业级实现：",[27,28,29],"strong",{},"nginx","、",[27,32,33],{},"envoy",[27,35,36],{},"muduo","... 具体参考附录。\n第一篇先讲清\"为什么是 Reactor + One-Loop-Per-Thread\"——这是后面所有故事的底座。",[39,40],"hr",{},[42,43,45],"h2",{"id":44},"_0-一个最朴素的问题","0. 一个最朴素的问题",[18,47,48],{},"我认为绝大多数人包括我， 在初次学习网络编程时都写过一个服务端客户端的程序。\n服务端: 创建监听socket, 绑定端口, 启动监听, 接受连接, 处理业务逻辑, 关闭服务。\n客户端: 创建连接socket, 配置并连接服务端， 处理读写， 关闭连接。",[18,50,51],{},"在最基本的两个TCP服务端与TCP客户端中， 服务端阻塞等待连接, 然后连接到了等待客户端写数据。\n阻塞这时候可能是很糟糕的事情, 服务端被卡住了然后它无法处理新的连接。",[18,53,54,55,58],{},"当时的解决方案: 主服务端只处理连接问题, 读写单独开一个进程\u002F线程。",[27,56,57],{},"thread-per-connection","模型",[18,60,61],{},"伪代码:",[63,64,70],"pre",{"className":65,"code":67,"language":68,"meta":69},[66],"language-cpp","while (true) {\n  int conn = accept(listen_fd, ...);\n  std::thread([conn] { handle(conn); }).deatch();\n}\n","cpp","",[22,71,67],{"__ignoreMap":69},[18,73,74],{},"这是一种好且容易想到的方案, 至少当时如此。",[18,76,77,78,81,82,84,85,88,89,91],{},"问题在于, 随着互联网的兴起,一个 HTTP 服务器，要支持 10K 并发连接，怎么做？\n",[22,79,80],{"code":80},"Igor Sysoev",": ",[22,83,29],{"code":29}," 的作者, 在俄罗斯的一家公司上班时, 被 ",[22,86,87],{"code":87},"Appache"," 在高并发下进程数爆炸的情况逼疯了, 自此 ",[22,90,29],{"code":29}," 诞生。",[18,93,94],{},"这是臭名昭著的 C10k 问题。\n为什么一线程一连接的模型顶不住了?",[96,97,98,108,116,125],"ol",{},[99,100,101,81,104,107],"li",{},[27,102,103],{},"进程",[22,105,106],{"code":106},"Linux"," 的文件句柄数量有限, 这意味着进程\u002F线程开的数量有上限, 理论上并没有达到处理百万并发的数量。 真实的原因并不是这个, 这个最大值只是一个理论值。",[99,109,110,81,113,115],{},[27,111,112],{},"内存",[22,114,106],{"code":106}," 的线程 默认 8MB, 10k 连接是 80GB 的虚拟内存。 无论实际上只用了少数的虚存，这种吃内存的开销仍不容忽视。",[99,117,118,121,122],{},[27,119,120],{},"调度",": 内核级线程切换涉及 TLB 失效、缓存污染。\n当线程数大于CPU的核心数量， 那么线程之间上下文的开销就很明显。 ",[22,123,124],{"code":124},"CPU 大部分时间花在切换而不是真实干活上!",[99,126,127,130],{},[27,128,129],{},"竞态",": 线程之间共享状态。 如何设计锁，原子变量，CAS, 无锁数据结构。 这是一个重要话题, 避免一个线程独占资源导致整个服务器性能急剧下降。",[18,132,133,136],{},[27,134,135],{},"C10k"," 二十年前提出的核心矛盾。\n现代高性能网络框架——从 nginx 到 envoy 到 Node.js——给出的答案都是同一个词：",[15,138,139],{},[18,140,141],{},[27,142,143],{},"事件驱动（event-driven）",[18,145,146,147,150],{},"具体落到设计模式上，就是 ",[27,148,149],{},"Reactor 模式","。",[39,152],{},[42,154,156],{"id":155},"_1-reactor-是什么","1. Reactor 是什么",[18,158,159],{},"提问: 一个线程只能处理一个连接吗?",[18,161,162],{},"Reactor 模式的核心只有一句话：",[15,164,165],{},[18,166,167],{},[27,168,169],{},"用一个线程，通过 I\u002FO 多路复用，把\"等\"换成\"被通知\"。",[18,171,172,173,176,177,180,181,180,184,187],{},"不是\"每个连接一个线程在 ",[22,174,175],{"code":175},"read()"," 上阻塞\"——而是把所有 fd 注册到一个多路复用器（",[22,178,179],{"code":179},"epoll","\u002F",[22,182,183],{"code":183},"kqueue",[22,185,186],{"code":186},"IOCP","）上，问内核：\"这堆 fd 谁就绪了，告诉我。\"内核返回就绪列表，应用线程依次处理。",[18,189,190],{},"最小化的 Reactor 循环长这样：",[63,192,195],{"className":193,"code":194,"language":68,"meta":69},[66],"while (running) {\n    events = poller.wait(timeout);   \u002F\u002F 阻塞，但只阻塞一个线程\n    for (event : events) {\n        dispatch(event);             \u002F\u002F 同步处理就绪的 fd\n    }\n}\n",[22,196,194],{"__ignoreMap":69},[18,198,199],{},"就是如此简单。\n所有复杂性——SubReactor、线程池、定时器、连接池——都是围绕这个循环的扩展。",[201,202,204],"h3",{"id":203},"_11-reactor-的四个角色","1.1 Reactor 的四个角色",[18,206,207,208,212],{},"按 Schmidt 在 ",[209,210,211],"em",{},"POSA2"," 里的定义，Reactor 模式包含四个角色：",[214,215,216,232],"table",{},[217,218,219],"thead",{},[220,221,222,226,229],"tr",{},[223,224,225],"th",{},"角色",[223,227,228],{},"职责",[223,230,231],{},"本项目对应",[233,234,235,251,274,290],"tbody",{},[220,236,237,243,246],{},[238,239,240],"td",{},[27,241,242],{},"Handle",[238,244,245],{},"操作系统资源句柄（fd）",[238,247,248],{},[22,249,250],{"code":250},"int sockfd",[220,252,253,258,265],{},[238,254,255],{},[27,256,257],{},"Event Demultiplexer",[238,259,260,261,264],{},"调用 ",[22,262,263],{"code":263},"epoll_wait"," 等待事件",[238,266,267,270,271],{},[22,268,269],{"code":269},"runtime::net::Poller","（基类）+ ",[22,272,273],{"code":273},"EPollPoller",[220,275,276,281,284],{},[238,277,278],{},[27,279,280],{},"Event Handler",[238,282,283],{},"事件回调接口",[238,285,286,289],{},[22,287,288],{"code":288},"runtime::net::Channel"," 持有的 read\u002Fwrite\u002Fclose\u002Ferror 回调",[220,291,292,297,300],{},[238,293,294],{},[27,295,296],{},"Reactor",[238,298,299],{},"注册 Handler、运行事件循环、分发事件",[238,301,302],{},[22,303,304],{"code":304},"runtime::net::EventLoop",[18,306,307,308,317],{},"代码上一一对应（",[309,310,314],"a",{"href":311,"rel":312},"https:\u002F\u002Fgithub.com\u002Fakiba-miku\u002Fhigh-concurrency-runtime\u002Fblob\u002Fmain\u002Finclude\u002Fruntime\u002Fnet\u002Fevent_loop.h",[313],"nofollow",[22,315,316],{"code":316},"include\u002Fruntime\u002Fnet\u002Fevent_loop.h","）：",[63,319,322],{"className":320,"code":321,"language":68,"meta":69},[66],"class EventLoop : public NonCopyable {\npublic:\n  void Loop();                          \u002F\u002F 事件循环主体\n  void UpdateChannel(Channel* channel); \u002F\u002F 注册 Handler\n  ...\nprivate:\n  std::unique_ptr\u003CPoller> poller_;            \u002F\u002F Event Demultiplexer\n  std::vector\u003CChannel*> active_channels_;     \u002F\u002F 本轮就绪的 Handler 列表\n};\n",[22,323,321],{"__ignoreMap":69},[15,325,326],{},[18,327,328],{},"真实的EventLoop 循环。拿到内核通知就绪的fd连接， 然后处理",[63,330,333],{"className":331,"code":332,"language":68,"meta":69},[66],"  while (!quit_) {\n    active_channels_.clear();\n    poll_return_time_ = poller_->Poll(kPollTimeMs, &active_channels_);\n\n    for (Channel* channel : active_channels_) {\n      channel->HandleEvent(poll_return_time_);\n    }\n\n    DoPendingFunctors(); \u002F\u002F 跨线程投递队列\n  }\n",[22,334,332],{"__ignoreMap":69},[18,336,337],{},"总结: 单 Reactor就是从原来单线程处理连接读写， 改为注册fd, 等通知， 处理。 这样可以处理多个就绪的连接。",[39,339],{},[42,341,343],{"id":342},"_2-为什么单-reactor-不够从-reactor-到-multi-reactor","2. 为什么单 Reactor 不够：从 Reactor 到 Multi-Reactor",[18,345,346,347,150],{},"单 Reactor 的瓶颈很明显：",[27,348,349],{},"只用一个 CPU 核",[18,351,352],{},"如果机器有 32 核，那剩下 31 个核都在闲着等单核处理完事件——这在 10Gbps 网卡和 100K+ QPS 场景下完全不可接受。",[18,354,355],{},"业界的两类扩展方案：",[201,357,359],{"id":358},"_21-multi-processnginx-流派","2.1 Multi-Process（nginx 流派）",[18,361,362,363,366,367,370,371,374,375,378,379,382,383,386],{},"nginx 用 ",[27,364,365],{},"master + worker 多进程"," 模型。master 进程创建监听 socket 后，",[22,368,369],{"code":369},"fork()"," 出 N 个 worker 进程（N 通常是 CPU 核心数）。每个 worker 进程独立运行一个完整的 Reactor 循环，",[27,372,373],{},"共享同一个 listen fd","，靠内核的 ",[22,376,377],{"code":377},"accept()"," 互斥（旧 Linux 上还要用 ",[22,380,381],{"code":381},"accept_mutex"," 显式互斥避免惊群；新内核有 ",[22,384,385],{"code":385},"SO_REUSEPORT"," 后这个就不需要了）。",[63,388,393],{"className":389,"code":391,"language":392},[390],"language-text","                                 ┌──────────┐\n                                 │  master  │ (配置加载、信号、worker 监控)\n                                 └────┬─────┘\n                                      │ fork\n                ┌───────────┬─────────┼─────────┬───────────┐\n                ▼           ▼         ▼         ▼           ▼\n            ┌───────┐   ┌───────┐ ┌───────┐ ┌───────┐   ┌───────┐\n            │worker │   │worker │ │worker │ │worker │   │worker │ ...\n            │epoll  │   │epoll  │ │epoll  │ │epoll  │   │epoll  │\n            └───────┘   └───────┘ └───────┘ └───────┘   └───────┘\n","text",[22,394,391],{"__ignoreMap":69},[18,396,397],{},"采用多进程而不是线程的优势在于, 进程之间天然隔离独占资源， 一个崩溃不能带崩其它。\n缺点: 进程之间通信麻烦(共享内存); 进程比线程更重，启动更慢。 连接不能跨 worker 间迁移。",[201,399,401],{"id":400},"_22-multi-threadenvoy-muduo-流派","2.2 Multi-Thread（envoy \u002F muduo 流派）",[18,403,404,405,408,409,412],{},"envoy 和 muduo 选择 ",[27,406,407],{},"多线程 + 多 EventLoop","。一个主线程（\"Main Reactor\"）只负责 ",[22,410,411],{"code":411},"accept","，N 个工作线程各运行一个 EventLoop（\"Sub Reactor\"），新连接被分发到某个 Sub Reactor 上独占。",[18,414,415,416,419],{},"这就是 ",[27,417,418],{},"One-Loop-Per-Thread","：",[63,421,424],{"className":422,"code":423,"language":392},[390],"              ┌──────────────────┐\n              │   Main Loop      │  accept() 新连接\n              │   (Acceptor)     │\n              └────────┬─────────┘\n                       │ round-robin 分配\n        ┌──────────────┼──────────────┐\n        ▼              ▼              ▼\n   ┌─────────┐   ┌─────────┐   ┌─────────┐\n   │ Sub Loop│   │ Sub Loop│   │ Sub Loop│ ...\n   │ epoll   │   │ epoll   │   │ epoll   │\n   │ conn[]  │   │ conn[]  │   │ conn[]  │\n   └─────────┘   └─────────┘   └─────────┘\n",[22,425,423],{"__ignoreMap":69},[18,427,428,431,432,435],{},[27,429,430],{},"关键不变量","：一条连接一旦分配给某个 Sub Loop，它的所有 I\u002FO、状态、回调都",[27,433,434],{},"只在那个线程上跑","，直到关闭。线程之间不共享连接状态。",[18,437,438,441,442,445,446,449],{},[27,439,440],{},"优点","：单线程 简单，连接对象可以挂任何 C++ 对象；同一线程天然无锁和竞争。\n",[27,443,444],{},"缺点","：线程崩溃带走全部连接；线程数固定，不像进程那样能用 ",[22,447,448],{"code":448},"cgroup"," 隔离。",[201,451,453],{"id":452},"_23-本项目的选择","2.3 本项目的选择",[18,455,456,457,459],{},"这个项目走的是 ",[27,458,418],{},"（受 muduo 影响很深）。原因有三：",[96,461,462,468,478],{},[99,463,464,467],{},[27,465,466],{},"C++ 写多线程更顺手","：一个进程一份配置、一份连接池、一份指标，比多进程方便得多。",[99,469,470,473,474,477],{},[27,471,472],{},"目标场景是 API 网关","：网关本身要持有大量上游连接池、健康检查状态、限流器，这些状态在多进程模型里需要复杂的共享内存或者 IPC，多线程模型里直接 ",[22,475,476],{"code":476},"shared_ptr"," + 锁。",[99,479,480,419,483,30,486,30,489,492],{},[27,481,482],{},"作为毕设，更适合讲清楚 C++ 现代特性",[22,484,485],{"code":485},"std::any",[22,487,488],{"code":488},"shared_from_this",[22,490,491],{"code":491},"std::function"," 这些在多线程模型里有自然的舞台。",[39,494],{},[42,496,498],{"id":497},"_3-四个对照系异同表","3. 四个对照系：异同表",[18,500,501],{},"把四个参照系按几个关键维度并排：",[214,503,504,525],{},[217,505,506],{},[220,507,508,511,513,515,517,520],{},[223,509,510],{},"维度",[223,512,29],{},[223,514,33],{},[223,516,36],{},[223,518,519],{},"libevent",[223,521,522],{},[27,523,524],{},"本项目",[233,526,527,546,567,586,607,636,674,695,726,747],{},[220,528,529,532,535,537,539,542],{},[238,530,531],{},"并发模型",[238,533,534],{},"多进程 + 单 Reactor\u002F进程",[238,536,407],{},[238,538,407],{},[238,540,541],{},"单 event_base（也可多）",[238,543,544],{},[27,545,407],{},[220,547,548,551,554,557,560,562],{},[238,549,550],{},"语言",[238,552,553],{},"C",[238,555,556],{},"C++14\u002F17",[238,558,559],{},"C++11",[238,561,553],{},[238,563,564],{},[27,565,566],{},"C++20",[220,568,569,572,575,577,579,582],{},[238,570,571],{},"主要平台",[238,573,574],{},"Linux\u002FUnix",[238,576,106],{},[238,578,106],{},[238,580,581],{},"跨平台（含 Windows）",[238,583,584],{},[27,585,106],{},[220,587,588,591,594,597,600,603],{},[238,589,590],{},"多路复用抽象",[238,592,593],{},"event module（epoll\u002Fkqueue\u002Fselect 静态选）",[238,595,596],{},"dispatcher（基于 libevent2）",[238,598,599],{},"Poller 基类（运行时选）",[238,601,602],{},"event_base + backend（编译期选）",[238,604,605],{},[27,606,599],{},[220,608,609,612,619,622,625,631],{},[238,610,611],{},"内存管理",[238,613,614,615,618],{},"池（",[22,616,617],{"code":617},"ngx_pool_t","）",[238,620,621],{},"标准 C++ + tcmalloc",[238,623,624],{},"标准 C++",[238,626,627,628],{},"C ",[22,629,630],{"code":630},"malloc\u002Ffree",[238,632,633],{},[27,634,635],{},"标准 C++ + ObjectPool（定时器）",[220,637,638,641,647,656,663,666],{},[238,639,640],{},"连接对象",[238,642,643,646],{},[22,644,645],{"code":645},"ngx_connection_t","，池化复用",[238,648,649,652,653],{},[22,650,651],{"code":651},"ConnectionImpl","，",[22,654,655],{"code":655},"unique_ptr",[238,657,658,652,661],{},[22,659,660],{"code":660},"TcpConnection",[22,662,476],{"code":476},[238,664,665],{},"用户管理",[238,667,668],{},[27,669,670,652,672],{},[22,671,660],{"code":660},[22,673,476],{"code":476},[220,675,676,679,682,685,688,690],{},[238,677,678],{},"模块\u002F扩展",[238,680,681],{},"编译期模块（phase handler）",[238,683,684],{},"编译期 filter chain",[238,686,687],{},"无（库性质）",[238,689,687],{},[238,691,692],{},[27,693,694],{},"HTTP Router + std::any context",[220,696,697,700,703,706,709,715],{},[238,698,699],{},"跨线程通信",[238,701,702],{},"N\u002FA（多进程，无共享）",[238,704,705],{},"dispatcher::post()，eventfd 唤醒",[238,707,708],{},"runInLoop，eventfd 唤醒",[238,710,711,714],{},[22,712,713],{"code":713},"event_active","，pipe\u002Fsocket 唤醒",[238,716,717],{},[27,718,719,180,722,725],{},[22,720,721],{"code":721},"RunInLoop",[22,723,724],{"code":724},"QueueInLoop","，eventfd 唤醒",[220,727,728,731,734,737,739,742],{},[238,729,730],{},"定时器",[238,732,733],{},"红黑树",[238,735,736],{},"libevent min-heap",[238,738,733],{},[238,740,741],{},"min-heap",[238,743,744],{},[27,745,746],{},"侵入式红黑树 + ObjectPool",[220,748,749,752,755,758,761,763],{},[238,750,751],{},"背压",[238,753,754],{},"隐式（recv 节流）",[238,756,757],{},"显式 watermark callback",[238,759,760],{},"无",[238,762,760],{},[238,764,765],{},[27,766,767],{},"显式 HighWaterMark callback",[18,769,770],{},"几个值得展开的差异：",[201,772,774],{"id":773},"_31-nginx多进程-phase-handler","3.1 nginx：多进程 + phase handler",[18,776,777,778,781,782,785],{},"nginx 是这一类里最\"古典\"的——1999 年的设计，要在那个年代的硬件上扛 1 万连接，多进程是当时唯一靠谱的选择。它的 ",[22,779,780],{"code":780},"event module"," 是编译期决定的（",[22,783,784],{"code":784},".\u002Fconfigure --with-poll_module","），运行时不能切。",[18,787,788,789,792],{},"nginx 的 phase handler 模型把一个 HTTP 请求拆成 11 个阶段（POST_READ、SERVER_REWRITE、FIND_CONFIG……），每个阶段可以挂多个模块。这种设计",[27,790,791],{},"优先稳定性和可调试性","——nginx.conf 写错了配置不会让进程崩，因为模块是松耦合的。",[18,794,795,796,799],{},"我学到的：",[27,797,798],{},"配置的稳定性比代码的优雅性重要","。这一点这个项目目前还没做到——Router 配置是硬编码的，下一阶段要加配置文件解析时会回头借鉴。",[201,801,803],{"id":802},"_32-envoyservice-mesh-时代的-reactor","3.2 envoy：service mesh 时代的 Reactor",[18,805,806],{},"envoy（2017）是这一类里最年轻的。它的 dispatcher 底层其实就是 libevent2，但在上层加了非常多东西：filter chain、connection manager、watermark buffer、circuit breaker、admin API……envoy 的代码量是 muduo 的几十倍，因为它要做的事情多得多。",[18,808,809,810,813,814,817],{},"envoy 最值得学的是 ",[27,811,812],{},"watermark buffer","——也就是我刚加的 ",[22,815,816],{"code":816},"HighWaterMarkCallback"," 的原型。envoy 在每条连接的 read\u002Fwrite buffer 上都设有高低水位，触发时反压上游。本项目目前只在 TcpConnection 的写方向加了高水位，read 方向的反压还没做，这是下一步。",[201,819,821],{"id":820},"_33-muduoc-网络库的教科书","3.3 muduo：C++ 网络库的教科书",[18,823,824],{},"muduo（陈硕，2010）是本项目最直接的灵感来源。它把 One-Loop-Per-Thread 的思想做得极其干净：Channel\u002FPoller\u002FEventLoop\u002FTcpConnection 四件套，几乎是后来所有 C++ 网络库的模板。",[18,826,827,828,831,832,835],{},"但 muduo 是 ",[27,829,830],{},"库","，不是 ",[27,833,834],{},"运行时","——它没有 HTTP 路由、没有负载均衡、没有限流熔断。这个项目相当于\"muduo 风格的 net 层 + nginx 风格的 gateway 层\"。",[201,837,839],{"id":838},"_34-libevent跨平台的代价","3.4 libevent：跨平台的代价",[18,841,842,843,846,847,850,851,854,855,858],{},"libevent 是这四个里唯一认真做跨平台的（支持 Windows IOCP、BSD kqueue、Solaris event ports）。代价是它的抽象层级比 muduo 多一层：",[22,844,845],{"code":845},"event_base"," → ",[22,848,849],{"code":849},"event"," → 用户回调。事件的注册是 C 风格的 ",[22,852,853],{"code":853},"event_new"," + ",[22,856,857],{"code":857},"event_add","，灵活但容易写错。",[18,860,861],{},"我没有跟 libevent 那条路——本项目只跑 Linux，所以 Poller 抽象只是为了便于测试（poll\u002Fselect 用来对照 epoll 的正确性），不为了真正可移植。",[39,863],{},[42,865,867],{"id":866},"_4-一条连接的生命周期本项目实现","4. 一条连接的生命周期：本项目实现",[18,869,870],{},"讲完原理，看代码。一条 TCP 连接从 accept 到 close 在本项目里的流程：",[63,872,875],{"className":873,"code":874,"language":392},[390],"[Client]                  [Main Loop]                   [Sub Loop N]\n   |                          |                              |\n   |---- connect ------------>|                              |\n   |                          | (Acceptor::HandleRead)       |\n   |                          | int connfd = accept(...)     |\n   |                          |                              |\n   |                          | TcpServer::NewConnection     |\n   |                          | EventLoop* loop =            |\n   |                          |   pool.GetNextLoop()         |\n   |                          | conn = make_shared\u003CTcpConn>  |\n   |                          | loop->RunInLoop(             |\n   |                          |   conn->ConnectEstablished)  |\n   |                          |--------(eventfd wakeup)----->|\n   |                          |                              | ConnectEstablished:\n   |                          |                              |   channel->Tie(self)\n   |                          |                              |   channel->EnableReading()\n   |                          |                              |   connection_callback_(conn)\n   |                          |                              |\n   |---- write \"hello\" ------>|                              |\n   |                          |                              | epoll_wait 返回 EPOLLIN\n   |                          |                              | Channel::HandleEvent\n   |                          |                              |   -> TcpConnection::HandleRead\n   |                          |                              |     -> input_buffer_.ReadFd\n   |                          |                              |     -> message_callback_(conn, buf)\n   |                          |                              |        (HTTP 解析 + 业务回调)\n   |                          |                              |     -> conn->Send(response)\n   |                          |                              |        -> output_buffer_.Append\n   |                          |                              |        -> channel->EnableWriting()\n   |                          |                              |\n   |                          |                              | epoll_wait 返回 EPOLLOUT\n   |                          |                              | -> HandleWrite\n   |                          |                              |    write(fd, output_buffer_, ...)\n   |\u003C--- \"world\" --------------|                              |\n   |                          |                              |\n   |---- close -------------->|                              |\n   |                          |                              | EPOLLHUP \u002F read==0\n   |                          |                              | -> HandleClose\n   |                          |                              |    state_ = Disconnected\n   |                          |                              |    close_callback_(conn)\n   |                          |                              |        -> TcpServer::RemoveConnection\n   |                          |                              |           (loop->RunInLoop(base_loop))\n   |                          |\u003C-------(eventfd wakeup)------|\n   |                          | connections_.erase(name)     |\n   |                          | sub_loop->QueueInLoop(       |\n   |                          |   conn->ConnectDestroyed)    |\n   |                          |--------(eventfd wakeup)----->|\n   |                          |                              | ConnectDestroyed:\n   |                          |                              |   channel->DisableAll()\n   |                          |                              |   channel->Remove()\n   |                          |                              | (shared_ptr 计数归零，析构)\n",[22,876,874],{"__ignoreMap":69},[18,878,879],{},"几个关键设计：",[201,881,883],{"id":882},"_41-跨线程通信靠-eventfd","4.1 跨线程通信靠 eventfd",[18,885,886,889,890,893,894,896,897,899,900,903],{},[22,887,888],{"code":888},"Main Loop"," 把新连接交给 ",[22,891,892],{"code":892},"Sub Loop"," 时，不能直接调用 ",[22,895,892],{"code":892}," 上的函数——那是另一个线程，会撞数据竞争。正确做法是把 lambda 塞进 ",[22,898,892],{"code":892}," 的 pending 队列，然后写一个字节到它的 ",[22,901,902],{"code":902},"eventfd"," 上：",[63,905,908],{"className":906,"code":907,"language":68,"meta":69},[66],"\u002F\u002F EventLoop::QueueInLoop()\nvoid EventLoop::QueueInLoop(Functor cb) {\n  {\n    std::lock_guard lk{mutex_};\n    pending_functors_.push_back(std::move(cb));\n  }\n  if (!IsInLoopThread() || calling_pending_functors_) {\n    Wakeup();   \u002F\u002F write(eventfd, ...)\n  }\n}\n",[22,909,907],{"__ignoreMap":69},[18,911,912,914,915,917,918,920,921,924],{},[22,913,892],{"code":892}," 的 ",[22,916,263],{"code":263}," 因为有 ",[22,919,902],{"code":902}," 上的可读事件而立刻返回，处理完正常的 IO 事件后调用 ",[22,922,923],{"code":923},"DoPendingFunctors()"," 跑队列里的 lambda。",[18,926,927,928,931],{},"这是 muduo 的发明，envoy 也用同样的机制（envoy 叫 ",[22,929,930],{"code":930},"Dispatcher::post","）。libevent 用的是 pipe 而不是 eventfd（早期 Linux 没 eventfd），效果一样但开销略大。",[201,933,935,936,938],{"id":934},"_42-shared_from_this-解决-use-after-free","4.2 ",[22,937,488],{"code":488}," 解决 use-after-free",[18,940,941,943,944,947,948,951,952,955,956,959,960,963,964,967],{},[22,942,660],{"code":660}," 用 ",[22,945,946],{"code":946},"std::shared_ptr"," 管理生命周期。",[22,949,950],{"code":950},"Channel"," 里的回调持有 ",[22,953,954],{"code":954},"weak_ptr\u003Cvoid>","（通过 ",[22,957,958],{"code":958},"Channel::Tie()"," 设置），在每次 ",[22,961,962],{"code":962},"HandleEvent"," 之前 ",[22,965,966],{"code":966},"lock()"," 一次：",[63,969,972],{"className":970,"code":971,"language":68,"meta":69},[66],"\u002F\u002F Channel::HandleEvent\nvoid Channel::HandleEvent(Timestamp receive_time) {\n  if (tied_) {\n    std::shared_ptr\u003Cvoid> guard = tie_.lock();\n    if (guard) {\n      HandleEventWithGuard(receive_time);\n    }\n    \u002F\u002F 如果 lock 失败，说明 TcpConnection 已被销毁，直接跳过\n  } else {\n    HandleEventWithGuard(receive_time);\n  }\n}\n",[22,973,971],{"__ignoreMap":69},[18,975,976],{},"这解决了一个非常隐蔽的 bug：连接关闭后，epoll 队列里可能还有\"已经过期\"的事件等待处理。如果不做 tie，回调会访问已释放的内存。",[18,978,979,980,982],{},"nginx 不需要这个机制，因为 ",[22,981,645],{"code":645}," 是从池里分配的——池在 worker 进程生命期内不释放，所以指针永远有效，但代价是连接对象上的字段必须手动 reset。",[201,984,986,987],{"id":985},"_43-状态本地化stdany-context_","4.3 状态本地化：",[22,988,989],{"code":989},"std::any context_",[18,991,992,993,995,996,998,999,1002,1003,150],{},"每条 ",[22,994,660],{"code":660}," 有一个 ",[22,997,989],{"code":989}," 字段。HTTP 层在这上面挂 ",[22,1000,1001],{"code":1001},"HttpContext","（解析状态机），Gateway 在这上面挂上下游关联信息。所有这些状态都在连接归属的 Sub Loop 线程上访问，",[27,1004,1005],{},"没有全局 map，没有锁",[18,1007,1008,1009,1012],{},"这是 One-Loop-Per-Thread 的精髓：",[27,1010,1011],{},"让数据本来就不需要被并发访问","，而不是用锁去保护它。",[39,1014],{},[42,1016,1018],{"id":1017},"_5-这次写代码时被绊了几次","5. 这次写代码时被绊了几次",[18,1020,1021],{},"讲点真实的——架构图画起来漂亮，写代码时被以下几个问题折磨过：",[201,1023,1025],{"id":1024},"_51-channel-的-indexsetindex-暴露问题","5.1 Channel 的 Index\u002FSetIndex 暴露问题",[18,1027,1028,995,1030,1033,1034,180,1037,1040,1041,1044],{},[22,1029,950],{"code":950},[22,1031,1032],{"code":1032},"index_"," 字段，记录它在 Poller 里的注册状态（新\u002F已添加\u002F已删除）。我最初把 ",[22,1035,1036],{"code":1036},"Index()",[22,1038,1039],{"code":1039},"SetIndex()"," 设为 public——结果 HTTP 层的代码就有人写了 ",[22,1042,1043],{"code":1043},"channel_->Index()"," 来\"查状态\"，但语义完全错了。",[18,1046,1047,1048,1051,1052,1055],{},"正确做法是把它 ",[22,1049,1050],{"code":1050},"private","，然后 ",[22,1053,1054],{"code":1054},"friend"," 给三个具体 Poller 实现：",[63,1057,1060],{"className":1058,"code":1059,"language":68,"meta":69},[66],"class Channel : public NonCopyable {\n  ...\nprivate:\n  friend class EPollPoller;\n  friend class PollPoller;\n  friend class SelectPoller;\n\n  int Index() const { return index_; }\n  void SetIndex(int idx) { index_ = idx; }\n};\n",[22,1061,1059],{"__ignoreMap":69},[18,1063,1064,1065],{},"教训：",[27,1066,1067,1068,1070],{},"封装不是写 ",[22,1069,1050],{"code":1050},"，是约束接口的语义边界。",[201,1072,1074],{"id":1073},"_52-send-失败时没有信号","5.2 Send 失败时没有信号",[18,1076,1077,1078,1081,1082,1085,1086,1089,1090,1092],{},"最初 ",[22,1079,1080],{"code":1080},"TcpConnection::Send"," 返回 ",[22,1083,1084],{"code":1084},"void","，连接关闭后调用 Send 静默丢弃——调用者完全无感知。这就是上周加 ",[22,1087,1088],{"code":1088},"bool Send(...)"," 返回值的原因。配合新加的 ",[22,1091,816],{"code":816},"，上层可以：",[63,1094,1097],{"className":1095,"code":1096,"language":68,"meta":69},[66],"if (!conn->Send(payload)) {\n  metrics_.dropped++;          \u002F\u002F 连接断了，丢的\n}\n\nconn->SetHighWaterMark(64 * 1024 * 1024);\nconn->SetHighWaterMarkCallback([](auto& c, size_t n){\n  LOG_WARN() \u003C\u003C \"slow downstream, buffered=\" \u003C\u003C n;\n});\n",[22,1098,1096],{"__ignoreMap":69},[18,1100,1101],{},"这套语义是从 envoy 的 watermark buffer 直接借的。",[201,1103,1105],{"id":1104},"_53-命名一致性","5.3 命名一致性",[18,1107,1108,1109,1112,1113,1116,1117,1120],{},"最容易被忽略但最容易爆炸的问题。Google C++ Style 规定成员变量后缀 ",[22,1110,1111],{"code":1111},"_","，但项目里曾经混了 ",[22,1114,1115],{"code":1115},"ownerLoop_","（驼峰）和 ",[22,1118,1119],{"code":1119},"owner_loop_","（snake_case）。每次新人\u002F未来的自己来看代码都要确认一下：\"这个项目到底用哪种风格？\"",[18,1122,1123,1124,1127,1128,150],{},"最近一次大清理后，全部统一为 ",[22,1125,1126],{"code":1126},"snake_case_","。这种事情",[27,1129,1130],{},"做的时候很无聊，不做的话半年后会成为离职原因",[39,1132],{},[42,1134,1136],{"id":1135},"_6-跑一个压测把架构选择落到数字上","6. 跑一个压测：把架构选择落到数字上",[18,1138,1139],{},"光讲原理没意思，跑组数据看看。",[201,1141,1143],{"id":1142},"_61-测试环境","6.1 测试环境",[214,1145,1146,1156],{},[217,1147,1148],{},[220,1149,1150,1153],{},[223,1151,1152],{},"项",[223,1154,1155],{},"值",[233,1157,1158,1169,1177,1192,1200],{},[220,1159,1160,1163],{},[238,1161,1162],{},"CPU",[238,1164,1165,1166,618],{},"AMD EPYC 9754（虚拟化后 ",[27,1167,1168],{},"2 vCPU",[220,1170,1171,1174],{},[238,1172,1173],{},"内核",[238,1175,1176],{},"Linux 6.1",[220,1178,1179,1182],{},[238,1180,1181],{},"服务端",[238,1183,1184,1187,1188,1191],{},[22,1185,1186],{"code":1186},"examples\u002Fdemo_http_server","（response body = ",[22,1189,1190],{"code":1190},"\"OK\"","，2 字节）",[220,1193,1194,1197],{},[238,1195,1196],{},"客户端",[238,1198,1199],{},"wrk 1 线程，HTTP\u002F1.1 keep-alive",[220,1201,1202,1205],{},[238,1203,1204],{},"测试时长",[238,1206,1207],{},"每组 10s（c=10000 那组 15s）",[18,1209,1210,1211,1214,1215,1218],{},"注意：",[27,1212,1213],{},"2 vCPU 是个很挤的环境","——wrk 自己也要占 CPU，所以 server 实际能拿到的只有约 1 个核。绝对数字不能跟生产机器比，但",[27,1216,1217],{},"配置之间的相对差","有参考价值。",[201,1220,1222],{"id":1221},"_62-et-vs-lt-在小报文下基本打平","6.2 ET vs LT 在小报文下基本打平",[18,1224,1225,1226,1229],{},"把 ",[22,1227,1228],{"code":1228},"ET"," 环境变量分别设为 0\u002F1，扫连接数：",[214,1231,1232,1248],{},[217,1233,1234],{},[220,1235,1236,1239,1242,1245],{},[223,1237,1238],{},"并发连接",[223,1240,1241],{},"ET=1 RPS",[223,1243,1244],{},"LT RPS",[223,1246,1247],{},"平均延迟（LT）",[233,1249,1250,1266,1282,1298],{},[220,1251,1252,1255,1258,1263],{},[238,1253,1254],{},"16",[238,1256,1257],{},"7430",[238,1259,1260],{},[27,1261,1262],{},"8245",[238,1264,1265],{},"1.16 ms",[220,1267,1268,1271,1274,1279],{},[238,1269,1270],{},"64",[238,1272,1273],{},"7768",[238,1275,1276],{},[27,1277,1278],{},"7934",[238,1280,1281],{},"3.92 ms",[220,1283,1284,1287,1290,1295],{},[238,1285,1286],{},"256",[238,1288,1289],{},"7092",[238,1291,1292],{},[27,1293,1294],{},"8066",[238,1296,1297],{},"14.02 ms",[220,1299,1300,1303,1306,1311],{},[238,1301,1302],{},"1024",[238,1304,1305],{},"7616",[238,1307,1308],{},[27,1309,1310],{},"7798",[238,1312,1313],{},"47.52 ms",[18,1315,1316,1319],{},[27,1317,1318],{},"结果反直觉","：LT 在所有并发档位都略快（2–14%）。",[18,1321,1322,1323,1325,1326,150],{},"为什么？2 字节响应下，ET 模式\"读到 EAGAIN 才停\"的优势几乎不存在（每次 ",[22,1324,175],{"code":175}," 就一个报文），但 ET 的事件分发逻辑（要在用户态处理\"已经 ready 但没读完\"的状态）反而多了几个分支。",[27,1327,1328],{},"报文越小，ET 越没优势",[18,1330,1331],{},"预期是：响应变大（比如 100KB 文件）、或者高并发下大量 short-lived 连接（HTTP\u002F1.0 风格），ET 才会反超。后面的 Buffer 一篇会用 100KB body 重测这个对比。",[201,1333,1335],{"id":1334},"_63-io_threads1-比-io_threads2-快-1016","6.3 io_threads=1 比 io_threads=2 快 10–16%",[18,1337,1338,1339,1342],{},"这是这次跑数据最意外的发现。把 ",[22,1340,1341],{"code":1341},"IO_THREADS"," 从 2 降到 1：",[214,1344,1345,1360],{},[217,1346,1347],{},[220,1348,1349,1351,1354,1357],{},[223,1350,1238],{},[223,1352,1353],{},"io=2 RPS",[223,1355,1356],{},"io=1 RPS",[223,1358,1359],{},"提升",[233,1361,1362,1376,1390],{},[220,1363,1364,1366,1368,1373],{},[238,1365,1270],{},[238,1367,1278],{},[238,1369,1370],{},[27,1371,1372],{},"9241",[238,1374,1375],{},"+16%",[220,1377,1378,1380,1382,1387],{},[238,1379,1286],{},[238,1381,1294],{},[238,1383,1384],{},[27,1385,1386],{},"9298",[238,1388,1389],{},"+15%",[220,1391,1392,1394,1396,1401],{},[238,1393,1302],{},[238,1395,1310],{},[238,1397,1398],{},[27,1399,1400],{},"8602",[238,1402,1403],{},"+10%",[18,1405,1406,1407,1410],{},"直觉上\"加线程加吞吐\"，",[27,1408,1409],{},"实际上 thread > core 是反优化","。原因：",[63,1412,1415],{"className":1413,"code":1414,"language":392},[390],"io_threads=2 时的线程：\n  - main loop (Acceptor + connections map)\n  - sub loop A\n  - sub loop B\n  - wrk thread\n合计 4 个活跃线程，争 2 个核 → 不停 context switch + cross-thread eventfd 唤醒\n",[22,1416,1414],{"__ignoreMap":69},[63,1418,1421],{"className":1419,"code":1420,"language":392},[390],"io_threads=1 时：\n  - main loop\n  - sub loop A\n  - wrk thread\n3 个线程，依然超 2 核，但少了一组跨 sub-loop 的 wakeup\n",[22,1422,1420],{"__ignoreMap":69},[18,1424,1064,1425,1428,1429,1432,1433,1436,1437,1440],{},[27,1426,1427],{},"One-Loop-Per-Thread 的最优线程数 ≈ 物理核心数","，超过就开始亏。nginx 默认 ",[22,1430,1431],{"code":1431},"worker_processes auto"," 也是按 CPU 核数来的，envoy 的 ",[22,1434,1435],{"code":1435},"--concurrency"," 默认值同理。我之前默认设 ",[22,1438,1439],{"code":1439},"io_threads=2"," 是想\"反正多线程总比少线程好\"，被数据打脸。",[201,1442,1444],{"id":1443},"_64-c10k-实测1-万长连接","6.4 C10K 实测：1 万长连接",[18,1446,1447],{},"最后压一组 10000 keep-alive 连接（LT, io=2，跑 15s）：",[63,1449,1452],{"className":1450,"code":1451,"language":392},[390],"Running 15s test @ http:\u002F\u002F127.0.0.1:18080\u002F\n  1 threads and 10000 connections\n    Latency   571.57ms  365.87ms   1.76s    69.56%\n    Req\u002FSec     6.45k     1.23k    8.95k    64.29%\n  90384 requests in 15.25s, 7.33MB read\nRequests\u002Fsec:   5925.20\n",[22,1453,1451],{"__ignoreMap":69},[18,1455,1456,1459],{},[27,1457,1458],{},"Socket errors: 0","。1 万连接稳定挂着，吞吐降到低并发的 ~75%，没有连接被踢掉。这就是 epoll + One-Loop-Per-Thread 模型相对 thread-per-connection 的根本胜利——thread-per-connection 在这台 2 核 VM 上跑 10K 线程会直接 OOM。",[18,1461,1462],{},"p99 延迟 1.76s 不算好看，但这是 2 vCPU 的物理瓶颈——10K 连接竞争 1 个 sub-loop 线程的服务能力，没有魔法。",[201,1464,1466],{"id":1465},"_65-这些数字给后续设计的启示","6.5 这些数字给后续设计的启示",[1468,1469,1470,1476,1482],"ul",{},[99,1471,1472,1475],{},[27,1473,1474],{},"默认线程数应该 = nproc，但要让用户能调","：后面 GatewayServer 的配置项需要暴露这个",[99,1477,1478,1481],{},[27,1479,1480],{},"背压在这个量级还不是瓶颈","：5925 RPS × 2 字节 ≈ 12KB\u002Fs，离 HighWaterMark 的 64MB 阈值远得很。等做完上游代理（响应可能几 MB）才会真正有戏",[99,1483,1484,1487],{},[27,1485,1486],{},"Poller 抽象（select\u002Fpoll\u002Fepoll 可切）的开销可以忽略","：上面的数字都是经过虚函数派发的，离 epoll 直接调用差距 \u003C 1%。下一篇会用 perf 给出具体数",[39,1489],{},[42,1491,1493],{"id":1492},"_7-收尾这个系列接下来会写什么","7. 收尾：这个系列接下来会写什么",[18,1495,1496],{},"按重要性排：",[96,1498,1499,1506,1512,1518,1524,1533,1538,1544],{},[99,1500,1501,1502,1505],{},"✅ ",[27,1503,1504],{},"总体架构","（本篇）",[99,1507,1508,1511],{},[27,1509,1510],{},"Poller 抽象层","——为什么保留 select\u002Fpoll 而不是只用 epoll？跨实现的契约怎么定？",[99,1513,1514,1517],{},[27,1515,1516],{},"Channel 与 fd 事件分发","——Tie 机制的来龙去脉、ET vs LT 的取舍",[99,1519,1520,1523],{},[27,1521,1522],{},"Buffer 的两种实现","——muduo 三段式 vs nginx ngx_chain，本项目两套都有",[99,1525,1526,1529,1530,1532],{},[27,1527,1528],{},"TcpConnection 生命周期","——",[22,1531,476],{"code":476}," 模型 vs 对象池模型",[99,1534,1535],{},[27,1536,1537],{},"TimerQueue：红黑树 vs 小根堆 vs 时间轮",[99,1539,1540,1543],{},[27,1541,1542],{},"背压机制 HighWaterMark","——和 envoy watermark buffer 的对比",[99,1545,1546,1549],{},[27,1547,1548],{},"HTTP Router：Trie 实现","——和 nginx phase handler 的对比",[18,1551,1552],{},"每一篇都会带：",[1468,1554,1555,1561,1564],{},[99,1556,1557,1558,618],{},"对应的源码引用（",[22,1559,1560],{"code":1560},"file_path:line_number",[99,1562,1563],{},"至少一个 benchmark 数字",[99,1565,1566],{},"对照 nginx\u002Fenvoy\u002Fmuduo\u002Flibevent 中至少一个的具体实现",[18,1568,1569],{},"如果你觉得哪个话题特别想看，告诉我，我可以调顺序。",[39,1571],{},[42,1573,1574],{"id":1574},"参考资料",[1468,1576,1577,1584,1587,1594,1601,1608,1615],{},[99,1578,1579,1580,1583],{},"Schmidt et al., ",[209,1581,1582],{},"Pattern-Oriented Software Architecture, Volume 2",", Wiley, 2000. （Reactor 模式的原始定义）",[99,1585,1586],{},"陈硕，《Linux 多线程服务端编程：使用 muduo C++ 网络库》，电子工业出版社，2013.",[99,1588,1589,1590],{},"nginx source: ",[309,1591,1592],{"href":1592,"rel":1593},"https:\u002F\u002Fgithub.com\u002Fnginx\u002Fnginx",[313],[99,1595,1596,1597],{},"envoy source: ",[309,1598,1599],{"href":1599,"rel":1600},"https:\u002F\u002Fgithub.com\u002Fenvoyproxy\u002Fenvoy",[313],[99,1602,1603,1604],{},"muduo source: ",[309,1605,1606],{"href":1606,"rel":1607},"https:\u002F\u002Fgithub.com\u002Fchenshuo\u002Fmuduo",[313],[99,1609,1610,1611],{},"libevent source: ",[309,1612,1613],{"href":1613,"rel":1614},"https:\u002F\u002Fgithub.com\u002Flibevent\u002Flibevent",[313],[99,1616,1617,1618],{},"本项目: ",[309,1619,1620],{"href":1620,"rel":1621},"https:\u002F\u002Fgithub.com\u002Fakiba-miku\u002Fhigh-concurrency-runtime",[313],[18,1623,1624],{},"下一篇见。",[63,1626,1632],{"className":1627,"code":1629,"language":1630,"meta":1631},[1628],"language-md","\u003C!-- 你可以在此处书写大纲，并在上方完成文章 -->\n","md","wrap",[22,1633,1629],{"__ignoreMap":69},{"title":69,"searchDepth":1635,"depth":1635,"links":1636},4,[1637,1639,1643,1648,1654,1661,1666,1673,1674],{"id":44,"depth":1638,"text":45},2,{"id":155,"depth":1638,"text":156,"children":1640},[1641],{"id":203,"depth":1642,"text":204},3,{"id":342,"depth":1638,"text":343,"children":1644},[1645,1646,1647],{"id":358,"depth":1642,"text":359},{"id":400,"depth":1642,"text":401},{"id":452,"depth":1642,"text":453},{"id":497,"depth":1638,"text":498,"children":1649},[1650,1651,1652,1653],{"id":773,"depth":1642,"text":774},{"id":802,"depth":1642,"text":803},{"id":820,"depth":1642,"text":821},{"id":838,"depth":1642,"text":839},{"id":866,"depth":1638,"text":867,"children":1655},[1656,1657,1659],{"id":882,"depth":1642,"text":883},{"id":934,"depth":1642,"text":1658},"4.2 shared_from_this 解决 use-after-free",{"id":985,"depth":1642,"text":1660},"4.3 状态本地化：std::any context_",{"id":1017,"depth":1638,"text":1018,"children":1662},[1663,1664,1665],{"id":1024,"depth":1642,"text":1025},{"id":1073,"depth":1642,"text":1074},{"id":1104,"depth":1642,"text":1105},{"id":1135,"depth":1638,"text":1136,"children":1667},[1668,1669,1670,1671,1672],{"id":1142,"depth":1642,"text":1143},{"id":1221,"depth":1642,"text":1222},{"id":1334,"depth":1642,"text":1335},{"id":1443,"depth":1642,"text":1444},{"id":1465,"depth":1642,"text":1466},{"id":1492,"depth":1638,"text":1493},{"id":1574,"depth":1638,"text":1574},[1676],"技术","2026-05-19 10:27:27",false,null,{"slots":1681},{},true,"\u002F2026\u002F()-why-reactor-and-one-loop-per-thread",{"text":1685,"minutes":1686,"time":1687,"words":1688},"24 min read",23.435,1406100,4687,{"title":5,"description":13},{"loc":1683},"posts\u002F2026\u002F网络层(一) why reactor and one-loop per thread",[1693,1694],"Net","C++","tech","-rZiUEH9Av2x9J4mZf78h6-JEPCJK0LSKaOH-goqOx4",[1698,1703],{"title":1699,"path":1700,"stem":1701,"date":1702,"type":1695,"children":-1},"KMP-(Knuth-Morris-Pratt)","\u002F2026\u002Fkmp-(knuth-morris-pratt)","posts\u002F2026\u002FKMP-(Knuth-Morris-Pratt)","2026-04-29 13:29:48",{"title":1704,"path":1705,"stem":1706,"date":1707,"type":1695,"children":-1},"内存池仿Nginx_C++实现","\u002F2026\u002Fnginx_c++","posts\u002F2026\u002F内存池仿Nginx_C++实现","2026-05-24 18:33:28",1779619501424]