quicX 引用关系与内存管理
本文档系统性介绍 quicX 代码库中对象生命周期、所有权层级、引用关系的设计约定。理解本文档后,你能够:
- 判断一个成员应当使用
shared_ptr/weak_ptr/unique_ptr/ 裸指针; - 在新增回调、定时器、事件订阅时,避免引入循环引用与悬空引用;
- 在 Review 代码时,快速识别违反所有权约定的写法。
1. 指导原则:Exclusive Ownership + Observer Reference
Section titled “1. 指导原则:Exclusive Ownership + Observer Reference”quicX 采用「独占所有权 + 观察者引用」模型。核心规则只有两条:
- 每个对象有且仅有一个”所有者”。所有者用
shared_ptr(若需共享)或unique_ptr(若独占)持有其生命周期。 - 所有其他”使用者”通过
weak_ptr观察。使用时lock(),若为空则放弃本次操作。
这个模型替代了早期版本中弥漫的「人人 shared_ptr,互相持有」的写法,后者会导致:
- 循环引用泄漏:
A->shared_ptr<B>,B->shared_ptr<A>,两个对象都无法释放; - 回调延长生命周期:
AddTimer([self=shared_from_this()]{...})让已经”关闭”的对象无法销毁; - 析构次序依赖:多个
shared_ptr的析构次序难以预测,容易 use-after-free。
2. 所有权层级全景图
Section titled “2. 所有权层级全景图”quicX 的对象生命周期形成一棵有向无环的所有权树。自顶向下:
QuicClient / QuicServer ← 用户持有 │ owns(shared_ptr) ├── common::IEventLoop (I/O + Timer 枢纽) ├── ISender (UDP 发送器) └── IWorker (连接容器,包含 Master/Worker) │ owns(shared_ptr in conn_map_) └── IConnection (BaseConnection) │ owns(unique_ptr) ├── TimerCoordinator ├── ConnectionIDCoordinator ├── PathManager ├── StreamManager ── owns(shared_ptr) ─→ IStream ├── ConnectionCloser ├── FrameProcessor ├── EncryptionLevelScheduler └── PacketBuilderHTTP/3 层同样遵循这一结构:
Http3Client / Http3Server │ owns(shared_ptr) └── IQuicClient / IQuicServer (复用 QUIC 层) └── Http3Connection │ owns(shared_ptr) └── ReqRespBaseStream (RequestStream / ResponseStream / PushStream) │ owns(shared_ptr) └── IQuicBidirectionStream (QUIC 层 Stream,由 StreamManager 所有)2.1 所有者(Owner)一览
Section titled “2.1 所有者(Owner)一览”| 对象 | 所有者 | 持有方式 |
|---|---|---|
IEventLoop | QuicClient / QuicServer | shared_ptr |
ISender (UDP) | QuicClient / QuicServer | shared_ptr |
IWorker | Master / QuicClient | shared_ptr |
BaseConnection | Worker::conn_map_ | shared_ptr<IConnection> |
IStream (QUIC) | StreamManager(隶属 BaseConnection) | shared_ptr |
ReqRespBaseStream (H3) | Http3Connection | shared_ptr |
各 Coordinator / Manager | BaseConnection | unique_ptr |
2.2 观察者(Observer)一览
Section titled “2.2 观察者(Observer)一览”| 持有者 → 被观察对象 | 字段形式 | 说明 |
|---|---|---|
BaseConnection::event_loop_ → IEventLoop | std::weak_ptr<IEventLoop> | QuicClient 是真实所有者 |
IStream::event_loop_ → IEventLoop | std::weak_ptr<IEventLoop> | 同上 |
Worker::event_loop_ → IEventLoop | std::weak_ptr<IEventLoop> | 同上 |
ResponseStream::http_processor_ → IHttpProcessor | std::weak_ptr<IHttpProcessor> | 应用层处理器,服务器顶层持有 |
http3::IStream 回调 → http3::IConnection | std::weak_ptr<IConnection> (lambda 捕获) | 见 §3.5,禁止 stream 反向强引用 connection |
经验法则:向上/向侧的引用一律使用 weak_ptr;只有向下的(所有者→被所有者)才允许 shared_ptr。
3. 关键 API 规范
Section titled “3. 关键 API 规范”3.1 回调与 shared_from_this()
Section titled “3.1 回调与 shared_from_this()”对需要在异步回调中访问自身的类(典型:BaseConnection, ReqRespBaseStream),必须继承 std::enable_shared_from_this<T>。
class BaseConnection: public IConnection, public std::enable_shared_from_this<BaseConnection> { ... };在短生命周期回调(如一次性定时器、OnX 事件处理)中使用 weak_from_this() + lock():
// 推荐写法:不延长生命周期,连接已销毁则安静放弃timer_coordinator_->AddTimer([weak = weak_from_this()] { auto self = weak.lock(); if (!self) return; self->OnIdleTimeout();}, timeout_ms);例外 —— Self-pinning:当一段代码必须原子性地完成(如 OnStateToClosing 会触发用户回调,用户回调可能释放最后一个 shared_ptr),才使用局部 shared_from_this() 钉住自己:
void BaseConnection::OnStateToClosing() { auto self = shared_from_this(); // 钉住,确保本函数内不会被析构 // 触发用户 close callback —— 用户可能 release 最后一个强引用 if (callbacks_.close_cb) callbacks_.close_cb(...); // 继续做清理工作 connection_closer_->Start();}3.2 EventLoop::AddFixedProcess
Section titled “3.2 EventLoop::AddFixedProcess”每事件循环迭代都会执行的 callback,必须使用 weak_ptr 守卫重载:
// 正确:owner 过期后 callback 自动失效并被移除event_loop_->AddFixedProcess( weak_from_this(), // std::weak_ptr<void> [weak = weak_from_this()] { auto self = weak.lock(); if (!self) return; self->DoPeriodicWork(); });
// 已废弃(且会在 ASan 下产生环引用泄漏):event_loop_->AddFixedProcess([self = shared_from_this()] { ... });原理:无 owner 守卫的 fixed process 会在 EventLoop::fixed_processes_ 里长期持有 shared_ptr,与对象形成环 —— 只有手动 ClearFixedProcesses() 才能断开。有 owner 守卫的版本,每次 iteration 前 owner.lock(),为空即跳过并从列表移除。
3.3 Timer 的生命周期
Section titled “3.3 Timer 的生命周期”EventLoop 内部 timers_ 持有所有已登记 timer 的 callback 闭包。若闭包捕获 shared_ptr<BaseConnection>,则:
- 对象关闭时 timer 未触发 → 闭包持有的 shared_ptr 直到 timer 过期才释放
- 若 master loop 先 stop → timer 永不触发 → 泄漏 ~120KB/连接
因此:
- 优先用
weak_from_this()捕获。 - 关闭路径上,
QuicClient::Destroy/QuicServer::Destroy必须在 loop 停止后调用event_loop_->ClearAllTimers(),把所有闭包显式释放。
3.4 Worker 分发数据包时的 Connection Pinning
Section titled “3.4 Worker 分发数据包时的 Connection Pinning”Worker::HandlePacket 从 conn_map_ 查到一个 IConnection,立刻拷贝一份 shared_ptr 到局部,再调用 OnPackets():
// 正确auto conn_sp = conn_map_[cid_hash]; // 拷贝 shared_ptrif (!conn_sp) return;conn_sp->OnPackets(now, packets); // 即使回调中把自己从 conn_map_ 移除,这里仍安全原因:OnPackets 内部可能触发 CONNECTION_CLOSE 回调 → 用户 callback → Destroy() → 从 conn_map_ 擦除自己。若直接用 conn_map_[...]->OnPackets() 这种”走引用”写法,ASan 会检出 use-after-free。
3.5 HTTP/3 Connection:两阶段初始化 + Weak-Self Lambda
Section titled “3.5 HTTP/3 Connection:两阶段初始化 + Weak-Self Lambda”http3::IConnection 与其子类 ClientConnection / ServerConnection 在构造期需要创建 control / QPACK encoder / QPACK decoder 等内部 stream,并把若干回调绑定到自身(HandleError / HandleSettings / HandlePushPromise 等)。这里有两个绝不能踩的雷点:
- 构造函数里
weak_from_this()是空的。std::enable_shared_from_this<T>的内部 weak_ptr 只在std::make_shared<T>完成后才被填充,构造体内调用weak_from_this()/shared_from_this()都会失败(前者得到永远 expired 的 weak,后者抛bad_weak_ptr)。 - 回调若直接
std::bind(&Xxx::HandleYyy, this, ...)或捕获裸this,当 stream 回调晚于 connection 析构触发时,会调到已销毁对象的虚函数,触发__cxa_pure_virtual→ SIGABRT。
因此 http3::IConnection 采用两阶段初始化:
// 1) 构造函数:仅做不依赖 shared_from_this 的初始化IConnection(unique_id, quic_conn, error_handler); // 仅赋值成员
// 2) 工厂创建后立刻调用 Init(),此时 shared_from_this() 已可用auto conn = std::make_shared<ServerConnection>(...);conn->Init(); // 内部创建 control / QPACK streams 并绑定 weak-self 回调三类回调的标准写法
Section titled “三类回调的标准写法”(a) 父类公共回调 —— 走 IConnection 提供的工厂:
std::function<void(uint64_t, uint32_t)> MakeErrorHandler();std::function<void(const std::unordered_map<uint16_t, uint64_t>&)> MakeSettingsHandler();
// 子类用法control_stream_->SetErrorCallback(MakeErrorHandler());control_stream_->SetSettingsCallback(MakeSettingsHandler());工厂内部用 weak_ptr<IConnection> 绑定,回调触发时 lock(),连接已销毁则安静放弃。
(b) 子类私有回调 —— 在 callsite 用 WeakSelfAs<T>() 内联 weak-lambda:
auto weak_self = WeakSelfAs<ClientConnection>();stream->SetPushPromiseCallback([weak_self](uint64_t push_id) { if (auto self = weak_self.lock()) self->HandlePushPromise(push_id);});WeakSelfAs<T>() 是 IConnection 提供的 protected 模板,本质是 weak_ptr<T>(static_pointer_cast<T>(shared_from_this()))。这样子类私有方法不需要 friend、也不污染父类接口。
(c) Stream 状态回调 —— 在 IConnection::Init() 里统一注册:
void IConnection::Init() { auto weak_self = weak_from_this(); // 此刻已可用 quic_connection_->SetStreamStateCallBack( [weak_self](uint64_t stream_id, StreamState state) { if (auto self = weak_self.lock()) self->OnStreamState(stream_id, state); });}// bind 裸 this:connection 析构后 stream 回调触发 → __cxa_pure_virtualcontrol_stream_->SetErrorCallback( std::bind(&ServerConnection::HandleError, this, _1, _2));
// 捕获 shared_from_this():形成 Connection → streams_ → Stream → cb → Connection 的环control_stream_->SetErrorCallback( [self = shared_from_this()](uint64_t id, uint32_t ec) { self->HandleError(id, ec); });
// 把 stream 创建放在构造函数里再 bind weak:weak_from_this() 此时是空的ServerConnection::ServerConnection(...) { control_stream_ = std::make_shared<ControlStream>(...); control_stream_->SetErrorCallback(MakeErrorHandler()); // 绑定的 weak 永远 expired}正确做法:构造函数只保存参数,stream 创建与回调绑定全部下沉到 Init()。
3.6 Sender 的共享
Section titled “3.6 Sender 的共享”ISender(UDP 发送器)被多方持有是正常的:
QuicClient/QuicServer创建并作为真实所有者;Worker::sender_、BaseConnection::sender_均为shared_ptr—— 这是”共享下游工具”,不会构成循环。
这是独占模型的唯一例外:纯数据出口(sender/logger/metrics)允许多个 shared_ptr 共享,因为它们本身不持有任何上层对象。
4. 关闭与析构流程
Section titled “4. 关闭与析构流程”这是最容易出 bug 的路径,必须一字不差地按以下次序执行:
用户调用 QuicClient::Destroy() │ ├── 1) 对每个 IWorker: worker->Shutdown() │ └── 清空 conn_map_ / connecting_set_ / active_send_connections_ │ → 让 BaseConnection 的 shared_ptr 引用计数归零 │ ├── 2) event_loop_->ClearAllTimers() │ └── 释放 timers_ 中所有闭包 —— 包括 self-pinned 的 BaseConnection │ ├── 3) master_event_loop_ reset / 线程 join │ └── 4) 所有 shared_ptr 成员析构其中第 1 步会触发每个 BaseConnection 的完整关闭序列:
Worker::Shutdown → conn.reset() (引用计数 --) → 若是最后一个引用 → ~BaseConnection() → state_machine_ 走到 Closed → OnStateToClosed() → 各 unique_ptr 成员按反向声明顺序析构RFC 9000 的 Closing/Draining timer 如果还挂在 EventLoop,必须由第 2 步兜底清理。
5. 常见陷阱与正确写法对照
Section titled “5. 常见陷阱与正确写法对照”陷阱 1:把自己绑进 fixed process
Section titled “陷阱 1:把自己绑进 fixed process”// 泄漏:EventLoop 持 shared_ptr<Worker>,Worker 持 shared_ptr<EventLoop>(早期设计)→ 环event_loop_->AddFixedProcess([self = shared_from_this()] { self->ProcessSend();});
// 正确event_loop_->AddFixedProcess(weak_from_this(), [weak = weak_from_this()] { auto self = weak.lock(); if (!self) return; self->ProcessSend();});陷阱 2:Stream 强引用 Connection
Section titled “陷阱 2:Stream 强引用 Connection”// Stream 与 Connection 双向 shared_ptr → 连接关闭后均无法释放class Stream { std::shared_ptr<BaseConnection> conn_; // 错};
// 改为回调 + EventLoop 观察class IStream { std::weak_ptr<common::IEventLoop> event_loop_; std::function<void(std::shared_ptr<IStream>)> active_send_cb_; // Connection 传入};陷阱 3:测试辅助函数返回短 tuple
Section titled “陷阱 3:测试辅助函数返回短 tuple”// event_loop 出函数即析构,BaseConnection::event_loop_.lock() 返回 nullptrstd::tuple<Conn, Conn, Sender, Sender> Make() { auto loop = MakeEventLoop(); ... return {client, server, cs, ss}; // loop 死了}
// 把 event_loop 也塞回 tuple,由测试作用域持有std::tuple<Conn, Conn, Sender, Sender, std::shared_ptr<IEventLoop>> Make() { ... return {client, server, cs, ss, loop};}陷阱 4:构造函数里调 weak_from_this() / shared_from_this()
Section titled “陷阱 4:构造函数里调 weak_from_this() / shared_from_this()”// enable_shared_from_this 的内部 weak 在 make_shared 完成后才填充// 这里 weak 永远 expired,回调一辈子都 lock 不到class Http3Conn : public std::enable_shared_from_this<Http3Conn> {public: Http3Conn() { auto weak = weak_from_this(); stream_->SetCb([weak]{ if (auto s = weak.lock()) s->Do(); }); }};
// 两阶段初始化:构造只赋值,Init 里再绑回调class Http3Conn : public std::enable_shared_from_this<Http3Conn> {public: Http3Conn(...) { /* 仅保存参数 */ } void Init() { auto weak = weak_from_this(); // 此时已可用 stream_ = std::make_shared<Stream>(...); stream_->SetCb([weak]{ if (auto s = weak.lock()) s->Do(); }); }};
// 调用方auto c = std::make_shared<Http3Conn>(...);c->Init(); // 必须紧跟 make_shared陷阱 5:Stream 回调里 std::bind(&X::M, this, ...)
Section titled “陷阱 5:Stream 回调里 std::bind(&X::M, this, ...)”// stream 回调可能晚于 connection 析构触发 → 调用纯虚函数 → __cxa_pure_virtualcontrol_stream_->SetErrorCallback( std::bind(&ServerConnection::HandleError, this, _1, _2));
// 父类公共回调走工厂control_stream_->SetErrorCallback(MakeErrorHandler());
// 子类私有回调走 WeakSelfAs<T>() 内联 lambdaauto weak_self = WeakSelfAs<ServerConnection>();push_stream->SetCallback([weak_self](uint64_t id) { if (auto self = weak_self.lock()) self->HandlePush(id);});陷阱 6:OnStateToClosing 中触发用户回调
Section titled “陷阱 6:OnStateToClosing 中触发用户回调”// 若用户回调释放最后一个 shared_ptr,本函数后续代码访问 this 即为 UAFvoid OnStateToClosing() { if (close_cb_) close_cb_(...); connection_closer_->Start(); // this 可能已析构}
// self-pinvoid OnStateToClosing() { auto self = shared_from_this(); if (close_cb_) close_cb_(...); connection_closer_->Start(); // 安全}6. 验证工具
Section titled “6. 验证工具”本仓库在 CI 中长期开启以下检查:
- AddressSanitizer (ASan):
build-asan/,每次核心 benchmark / 单元测试 run 后校验零循环引用、零 use-after-free、零 heap-use-after-free。 - LeakSanitizer (LSan):
test/perf/lsan_suppressions.txt仅过滤BlockMemoryPool的全局/线程本地池(那是正常的全局生命周期,不是环引用)。 - 单元测试:
test/unit_test/quic/connection/下专门有connection_close_test.cpp、connection_base_close_behavior_test.cpp、path_migration_test.cpp覆盖关闭 / 迁移场景下的生命周期。
任何新增成员引入 shared_ptr 时,请先自问:
这个字段是在表达”我是它的所有者”吗?如果不是 —— 用
weak_ptr。
7. 附:术语速查
Section titled “7. 附:术语速查”| 术语 | 含义 |
|---|---|
| Owner | 对对象生命周期负责的唯一角色(shared_ptr/unique_ptr 持有者) |
| Observer | 仅需访问、不对生命周期负责的引用者(weak_ptr 持有者) |
| Self-pinning | 在一段关键代码的入口 auto self = shared_from_this(); 延长自身生命周期直到函数结束 |
| Guarded Fixed Process | AddFixedProcess(weak_ptr<void> owner, cb),owner 过期自动跳过 |
| Connection Pinning | Worker 分发包前先复制一份 shared_ptr<IConnection> 到局部,防止分发途中被擦除 |
| Two-phase Init | 构造函数只赋值成员;Init() 在 make_shared 之后再被调用,里面才能用 weak_from_this() / shared_from_this() 绑定回调 |
| Weak-self Lambda | auto weak_self = weak_from_this(); + [weak_self](...){ if (auto s = weak_self.lock()) s->Do(); },HTTP/3 Connection 层绑定 stream 回调的标准写法 |
对应 Exclusive Ownership 重构 + HTTP/3 Connection 两阶段初始化修复完成。
