从零理解 MPSC 无锁写队列:高性能网络框架的发送引擎

写在前面 你有没有想过,一个高并发网络服务器在同时给几千个客户端发消息时,底层到底在忙什么? 最直觉的做法是加把锁——谁要发消息就抢锁、写 socket、放锁。但问题来了:如果 1000 个协程同时要给同一个连接发数据,它们就得排队等这把锁。锁竞争带来的上下文切换、cache 失效,会把性能拖进泥潭。 Hical 框架的解法很酷:用一个 MPSC(多生产者单消费者)无锁队列 做缓冲,配合一个协程写循环做消费。发消息的线程只管往队列里扔节点(wait-free,永远不阻塞),写循环协程在 IO 线程上批量取出、合并发送。 这篇文章会带你从零搞懂这个设计。不需要你有无锁编程的基础,但最好知道 C++ 的 atomic、shared_ptr 和"什么是协程"大概是怎么回事。 一、先搞清楚问题:为什么普通的锁不行? 1.1 一个典型场景 假设你写了个聊天服务器。用户 A 发了条群消息,服务器要转发给群里 200 个在线用户。这意味着: 1 用户A的消息 → 广播逻辑 → 同时调用 200 个连接的 send() 如果 send() 内部用 mutex 保护写操作: 1 2 3 4 5 6 7 8 9 10 11 // 朴素实现(有严重性能问题) void Connection::send(std::string data) { std::lock_guard lock(writeMutex_); // 200个协程在这里排队 writeBuffer_.append(data); if (!writing_) { writing_ = true; doWrite(); // 触发实际的 socket 写入 } } 问题出在哪? ...

May 28, 2026 · 10 min · 2127 words

连接级 Atomic 时间戳超时的实现决策

起因 最初 Hical 的空闲超时实现就是传统做法:每个 HTTP 请求/每次 keep-alive 读等待都注册一个 steady_timer,读完成后取消,超时则关闭连接。实现上用的是 shared_ptr<function> 自引用环做回调链续期——每连接 2 次堆分配(shared_ptr 控制块 + function 对象),且每次续期都要重新构造回调。 v2.5.2 压测到 132K QPS 时,做热路径Review代码发现这个 timer 机制的问题: 每请求 2 次 epoll_ctl(注册 + 取消 timer) shared_ptr<function> 自引用环本身就有堆分配开销 140K QPS 下整体约产生 100 万次 epoll_ctl/sec,38% CPU 花在内核 _raw_spin_unlock_irqrestore(TCP spin_lock),而用户态框架代码只占不到 5% 瓶颈已经从用户态转移到内核态,减少进内核的次数成为核心策略。空闲超时的 timer 是明确可以砍掉的——30-60s 的超时精度要求本来就极低。 改良过程 分两步走: 第一步:先把 shared_ptr<function> 回调链改为独立协程 idleTimerLoop,消除自引用环和 2 次堆分配。这一步还是 per-connection 一个 timer 协程,只是实现更干净了。 第二步:发现即便使用协程,per-connection timer 仍然意味着每次 timer 到期时要走 scheduler 调度 + epoll_ctl。最终演化为"TcpServer 统一扫描"的设计——整个 server 只需要一个扫描协程,连接侧只写一个 atomic 值。 ...

May 12, 2026 · 2 min · 314 words