NebulaChat 框架学习笔记:深入理解 Reactor 与多线程同步机制

今天主要整理了 Reactor 框架中几个核心机制,包括 epolleventfdatomicvector.data() 的使用,还有多线程同步中 cv.wait() 的底层逻辑。

这些知识看似细节,实则是写高性能 C++ 网络程序的地基。

一、Reactor::loop() 的事件派发流程

loop() 是 Reactor 的"心脏",它通过 epoll_wait() 等待内核事件,并把每个事件分发给上层 ServerConnection 对象。

cpp 复制代码
void Reactor::loop() {
    if (!dispatcher_) return;
    running_.store(true, std::memory_order_release);

    while (running_.load(std::memory_order_acquire)) {
        int n = epoll_wait(epfd_, evlist_.data(), evlist_.size(), -1);
        for (int i = 0; i < n; ++i) {
            int fd = evlist_[i].data.fd;
            uint32_t ev = evlist_[i].events;

            if (fd == evfd_) { 
                DrainEventfd(evfd_); 
                continue; 
            }

            void* user = nullptr;
            {
                std::lock_guard<std::mutex> lk(users_mtx_);
                auto it = users_.find(fd);
                if (it != users_.end()) user = it->second;
            }
            dispatcher_(fd, ev, user);
        }
    }
}

参数传递逻辑

  • epoll_wait() 会返回一个就绪事件数组

  • 每个元素 evlist_[i] 包含:

    • data.fd → 哪个 socket 触发了;

    • events → 是可读、可写还是出错;

  • Reactor 会根据这个 fd 从 users_ 查出对应对象指针(Server*Connection*),并调用 dispatcher_(fd, ev, user) 交给上层处理。

这就是事件从内核 → Reactor → Server 的传递链。

二、eventfd 与 epfd 的区别与关系

名称 作用 谁创建 是否加入 epoll
epfd_ epoll 实例,用来监听所有 fd 事件 Reactor
evfd_ 唤醒用的"信号 fd" Reactor

evfd_ 是 Reactor 自己注册的一个 eventfd 文件描述符

当其他线程想唤醒 Reactor 时,只需要:

cpp 复制代码
uint64_t one = 1;
write(evfd_, &one, 8);

这时:

  • 内核把 evfd_ 标记为"可读";

  • epoll_wait() 立即被唤醒;

  • loop() 发现 fd == evfd_,就知道是"唤醒信号";

  • 调用 DrainEventfd() 清空计数,继续循环。

三、为什么要清空 eventfd?

eventfd 内部维护一个 64 位计数器:

  • write() 会加一;

  • read() 会清零。

如果不 read(),它会一直被标记为"可读",

而 epoll 在水平触发模式下会认为它永远就绪

导致 epoll_wait() 每次都立刻返回、CPU 100% 空转。

cpp 复制代码
static void DrainEventfd(int evfd) {
    uint64_t cnt;
    while (read(evfd, &cnt, sizeof(cnt)) == sizeof(cnt)) {}
}

总结一句话:

如果不清空 eventfd,它的"门铃灯"会一直亮着,

epoll_wait 每次都会被立即唤醒,形成死循环。

四、epoll_wait 的逻辑本质

epoll_wait() 的作用是:

从成千上万个注册的 fd 中,筛选出当前真正有事件的那些

如果有 10 万个连接,但只有 2 个有数据:

  • epoll_wait() 只返回那 2 个;

  • 你只遍历它们,不用再检查其他 99998 个。

这一点是 epoll 能支持高并发的根本原因。

即使你写了:

复制代码
for (int i = 0; i < n; ++i)

那也是在遍历"活跃 fd 列表",不是在轮询所有连接。

五、atomic 的 store/load 与内存序

cpp 复制代码
std::atomic<bool> running_{false};

store()load() 用来在线程间安全读写标志:

cpp 复制代码
running_.store(true, std::memory_order_release);
while (running_.load(std::memory_order_acquire)) { ... }

含义:

  • store:写入 true,并发布写入前的所有操作;

  • load:读取 running_,确保读取的是最新的状态;

  • 多线程下保证 stop() 设置的 false 会被另一个线程立即看到。

而不是像普通 bool 那样因为 CPU cache 而"看不见更新"。

六、为什么用 vector.data()?

evlist_ 是一个 std::vector<epoll_event>

epoll_wait() 要求传入的是裸指针:

cpp 复制代码
epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

所以我们用:

cpp 复制代码
evlist_.data()  // 返回底层数组的首地址 (epoll_event*)

这块内存在 vector 创建时就一次性分配好了:

cpp 复制代码
std::vector<epoll_event> evlist_(maxEvents);

只要不扩容(不 resize),它的地址在整个 loop 期间都是固定有效的。

七、为什么判断 if (fd == evfd_)

因为 Reactor 在 epoll 里注册了自己的 eventfd 唤醒源:

cpp 复制代码
epoll_ctl(epfd_, EPOLL_CTL_ADD, evfd_, &ev);

所以 epoll_wait 可能返回两种事件:

  1. 普通 socket:说明客户端有 I/O;

  2. eventfd:说明有线程调用了 wakeup()

而:

cpp 复制代码
if (fd == evfd_) {
    DrainEventfd(evfd_);
    continue;
}

这句就是在区分"网络事件"和"唤醒事件"。

八、cv.wait 的真实工作机制

std::condition_variable 是 C++ 的线程同步原语。

用法:

cpp 复制代码
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });

它做了三件事:

  1. 自动释放锁(让别的线程修改条件);

  2. 挂起当前线程(不占 CPU);

  3. 被唤醒后重新上锁,再检查条件。

唤醒不等于立刻拥有锁,多个线程被 notify_all() 唤醒后还要重新竞争锁

只有抢到锁的线程,wait 才会真正返回。

伪代码解释:

cpp 复制代码
// wait 内部行为
unlock(mtx);
sleep until notified;
lock(mtx);

所以:

cv.wait() 并不会永久持锁等待,而是"放锁→睡眠→醒来再上锁",

这样生产者才能改共享变量,否则会死锁。

九、唤醒时的锁竞争与条件检查

notify_one() 唤醒的线程会:

  1. 从内核等待队列中被唤醒;

  2. 尝试重新锁住 mutex;

  3. 拿到锁后返回 wait;

  4. 检查条件是否真的满足(可能虚假唤醒);

  5. 条件为真再继续执行。

这就是为什么标准建议用:

复制代码
cv.wait(lock, []{ return 条件; });

而不是直接 cv.wait(lock)

十.小结:今天的知识体系

模块 核心概念 关键机制
Reactor::loop epoll_wait 分发事件 从内核获取就绪 fd 并派发
eventfd 唤醒机制 跨线程通知 Reactor
DrainEventfd 清空计数防止死循环 保证下一轮 epoll 阻塞正常
atomic 线程间可见性 控制 loop 的安全退出
vector.data() 取底层指针 兼容 C 风格 epoll 接口
fd == evfd_ 区分唤醒与普通事件 内核事件源类型判断
cv.wait 条件等待 解锁→睡眠→唤醒→重新加锁
notify_one 唤醒机制 通知等待线程竞争锁继续执行
相关推荐
松☆10 分钟前
Flutter + OpenHarmony 实战:构建离线优先的跨设备笔记应用
笔记·flutter
kk哥889914 分钟前
Swift底层原理学习笔记
笔记·学习·swift
爱吃萝卜的美羊羊14 分钟前
ubuntu下国内升级ollama
linux·运维·ubuntu
mzhan01726 分钟前
Linux: console: printk: console_no_auto_verbose
linux·运维·服务器
Savvy..43 分钟前
天机学堂-Day01
linux·服务器·网络
w***15311 小时前
ubuntu 安装 Redis
linux·redis·ubuntu
Vince丶2 小时前
UE DirectExcel使用笔记
笔记·ue5
AA陈超2 小时前
Lyra学习004:GameFeatureData分析
c++·笔记·学习·ue5·虚幻引擎
阿恩.7702 小时前
2026年1月最新计算机、人工智能、经济管理国际会议:选对会议 = 论文成功率翻倍
人工智能·经验分享·笔记·计算机网络·金融·区块链
xlq223222 小时前
22.多态(下)
开发语言·c++·算法