Linux应用框架cpp-tbox之事件驱动EventLoop

cpp-tbox项目链接 https://gitee.com/cpp-master/cpp-tbox

更多精彩内容欢迎关注微信公众号:码农练功房
在这里插入图片描述
往期精彩内容:
Linux应用框架cpp-tbox之弱定义
Linux应用框架cpp-tbox之日志系统设计

什么是Reactor模式

Reactor模式是一种事件驱动的设计模式,广泛应用于异步I/O处理,特别是在需要高效处理大量并发连接的服务器程序中,如Web服务器、数据库服务器、即时通讯软件等。

Reactor模式的核心组件是一个事件循环(EventLoop)线程,它负责监听、分发并处理来自I/O源的事件。

cpp-tbox采用的也是Reactor模式,为了便于我们理解什么是Reactor模式,库作者给出过一个贴近生活的例子:

Reactor线程就像是一个银行的办事柜台。如果遇到很轻松就能完成的事务,比如查询余额,柜台工作人员就立即处理了。

如果遇到的是比较繁重的工作,比如大额的取款,柜台工作人员便令后面的工作人员进行操作,让顾客在休息区等待。

柜台工作人员则继续接待其它的顾客。

等后面的工作人员取出了大金额的现金后,柜台工作人员呼叫取钱的顾客,并将现金给到该顾客。

这个过程中,柜台工作人员就是Reactor线程,后面的工作人员就是线程池的工作线程。

查询余额则是非阻塞性任务,取大额现金则是阻塞性的任务。

在 tbox.main 框架编程中,一切都是基于事件驱动的。具体操作就是:向Reactor注册某个事件的回调函数。当该事件发生了,Reactor就会回调之前注册的函数。 这种模型对注册的回调函数有三个基本的要求:不要阻塞!不要阻塞!不要阻塞!

Reactor模式关键组件

以下是基于Reactor模式EventLoop编程的基本组成部分:

  • 事件分离器(Event Demultiplexer): 在大多数Unix-like系统中,可以是select、poll、epoll或kqueue等IO多路复用机制。它的作用是监控多个I/O事件源(如文件描述符),并告知哪些是就绪的(可读、可写或异常)。
  • 事件处理器(Event Handler): 一个或多个处理器,负责具体的事件处理逻辑。每个事件(如新连接请求、数据可读取、数据写入完成)都有对应的处理器。
  • 事件循环(Event Loop): 核心组件,不断地执行以下循环:
  1. 调用事件分离器等待就绪事件;
  2. 分发就绪事件给相应的事件处理器;
  3. 执行处理器中的回调函数或任务;
  4. 重复上述过程。
  • 同步队列(Synchronization Queue): 可选组件,用于存放待处理的任务或事件,帮助管理并发和同步。

整体结构图

下图是cpp-tbox中EventLoop的代码模型:
请添加图片描述
其中Loop对象的创建、释放,由ContexImp对象负责管理。
Loop是一个抽象接口,用于屏蔽不同IO多路复用机制,CommonLoop则是对公共代码进行了提取,EpollLoop是对epoll的封装。
FdEvent封装了文件描述符,用于处理IO事件;SignalEvent和TimerEvent则分别对信号、定时器进行了封装。
这三类事件对象都由Loop提供对象创建接口,但是他们的生命周期由上层来管理。这三类事件先按下不表,我们此次着重Loop的设计。

runLoop

runLoop所在的线程也叫做IO线程,主要用来监控多个I/O事件源。

// cpp-tbox\modules\event\engines\epoll\loop.cpp
void EpollLoop::runLoop(Mode mode)
{
    if (epoll_fd_ < 0)
        return;
    std::vector<struct epoll_event> events;
    events.resize(max_loop_entries_);
    
    runThisBeforeLoop();
    keep_running_ = (mode == Loop::Mode::kForever);
    do {
        int fds = epoll_wait(epoll_fd_, events.data(), events.size(), getWaitTime());

        beginLoopProcess();

        handleExpiredTimers();  // 处理定时器事件

        for (int i = 0; i < fds; ++i) {
            epoll_event &ev = events.at(i);
            // 派发给事件处理器处理  调用回调函数
            EpollFdEvent::OnEventCallback(ev.events, ev.data.ptr);
        }
        
        handleNextFunc();      // 处理本次loop循环中需要立即执行的动作
        
        /// If the receiver array size is full, increase its size with 1.5 times.
        if (UNLIKELY(fds >= max_loop_entries_)) {
            max_loop_entries_ = (max_loop_entries_ + max_loop_entries_ / 2);
            events.resize(max_loop_entries_);
        }

        endLoopProcess();

    } while (keep_running_);

    runThisAfterLoop();
}

epoll_wait()会阻塞直到至少有一个文件描述符变为就绪。
当epoll_wait()返回后,填充一个epoll_event结构数组events,其中包含了就绪的文件描述符和发生的事件类型。然后主要做三件事

  1. 处理定时事件
  2. 遍历events数组,把就绪事件,派发给事件处理器处理
  3. 处理本次loop循环中需要立即执行的动作

事件注册

在runLoop中,我们看到了就绪事件的派发,那这些事件是什么时候注册上去的呢?
答案是在enable具体事件的时候:

FdEvent* sp_fd_read  = sp_loop->newFdEvent();
FdEvent* sp_fd_write = sp_loop->newFdEvent();

sp_fd_read->initialize(STDIN_FILENO, FdEvent::kReadEvent, Event::Mode::kPersist);    
sp_fd_write->initialize(STDOUT_FILENO, FdEvent::kWriteEvent, Event::Mode::kOneshot);  

sp_fd_read->enable();
sp_fd_write->enable();

我们进一步查看代码,发现enable最终会调用到EpollFdEvent::reloadEpoll(),完成事件注册。
需要注意的是事件注册要在IO线程中被执行

runNext

runNext用于注入本回调完成后立即执行的函数,无加锁操作,有较高的效率,但是不支持跨线程与跨Loop间调用
常用于不方便在本函数中执行的操作,比如释放对象自身。

// cpp-tbox\modules\event\common_loop_run.cpp
Loop::RunId CommonLoop::runNext(Func &&func, const std::string &what)
{
    RunId run_id = allocRunNextId();
    run_next_func_queue_.emplace_back(RunFuncItem(run_id, std::move(func), what));

    auto queue_size = run_next_func_queue_.size();
    if (queue_size > water_line_.run_next_queue_size)
        LogNotice("run_next_queue_size: %u", queue_size);

    if (queue_size > run_next_peak_num_)
        run_next_peak_num_ = queue_size;

    return run_id;
}

实现非常简单,向任务队列中加入任务,然后runLoop(IO线程)调用handleNextFunc取出任务队列中任务执行。

runInLoop

runInLoop用于向IO线程注入下一轮将执行的函数,有加锁操作,支持跨线程,跨Loop间调用。常用于不同Loop之间委派任务或其它线程向Loop线程妥派任务。

对于通过runInLoop委派的任务,如果epoll_wait处于阻塞状态,那么委派的任务将一直不能得到执行。这里需要有一种机制,能够唤醒处于阻塞状态的epoll_wait。这里采用了对 eventfd 的读写来实现对 epoll_wait的唤醒

首先在在loop进入循环前,在runThisBeforeLoop函数中先创建一个eventfd ,设置回调函数CommonLoop::handleRunInLoopFunc(即事件处理器处理),完成事件注册:

// cpp-tbox\modules\event\common_loop.cpp
void CommonLoop::runThisBeforeLoop()
{
    int event_fd = CreateEventFd();

    FdEvent *sp_read_event = newFdEvent("CommonLoop::sp_run_read_event_");
    if (!sp_read_event->initialize(event_fd, FdEvent::kReadEvent, Event::Mode::kPersist)) {
        close(event_fd);
        delete sp_read_event;
        return;
    }

    using std::placeholders::_1;
    sp_read_event->setCallback(std::bind(&CommonLoop::handleRunInLoopFunc, this));
    sp_read_event->enable(); // 唤醒事件注册

    std::lock_guard<std::recursive_mutex> g(lock_);
    loop_thread_id_ = std::this_thread::get_id();
    run_event_fd_ = event_fd;
    sp_run_read_event_ = sp_read_event;

    if (!run_in_loop_func_queue_.empty())
        commitRunRequest();

    resetStat();
}

在调用runInLoop派发任务时,向任务队列中加入任务,然后在commitRunRequest函数中向之前注册的event_fd写入数据,从而唤醒IO线程。

// cpp-tbox\modules\event\common_loop_run.cpp
Loop::RunId CommonLoop::runInLoop(Func &&func, const std::string &what)
{
    std::lock_guard<std::recursive_mutex> g(lock_);

    RunId run_id = allocRunInLoopId();
    run_in_loop_func_queue_.emplace_back(RunFuncItem(run_id, std::move(func), what));

    if (sp_run_read_event_ != nullptr)
        commitRunRequest();  // 唤醒IO线程

    auto queue_size = run_in_loop_func_queue_.size();
    if (queue_size > water_line_.run_in_loop_queue_size)
        LogNotice("run_in_loop_queue_size: %u", queue_size);

    if (queue_size > run_in_loop_peak_num_)
        run_in_loop_peak_num_ = queue_size;

    return run_id;
}

run

run接口用于自动选择 runNext或是 runInLoop。当与Loop在同一线程时,选择 runNext,否则选择 runInLoop。

// cpp-tbox\modules\event\common_loop_run.cpp
Loop::RunId CommonLoop::run(Func &&func, const std::string &what)
{
    bool can_run_next = true;
    {
        std::lock_guard<std::recursive_mutex> g(lock_);
        if (isRunningLockless() && !isInLoopThreadLockless())
            can_run_next = false;
    }

    if (can_run_next)
        return runNext(std::move(func), what);
    else
        return runInLoop(std::move(func), what);
}

当不知道派发任务时是选择runNext还是runInLoop时,选run就对了。

定时事件处理

定时器实现借助了epoll_wait的第四个参数timeout。

如果这个参数为-1,则一直阻塞。

如果这个参数为0,则立即返回。

如果设置成正数,则在超时后,epoll_wait返回。

这里的核心函数是getWaitTime,每次拿的都是最早的一个定时器与当前时间的间隔。

int64_t CommonLoop::getWaitTime() const
{
    if (hasNextFunc())
    {
        return 0;
    }
    /// Get the top of minimum heap
    int64_t wait_time = -1;
    if (!timer_min_heap_.empty()) {
        wait_time = timer_min_heap_.front()->expired - GetCurrentSteadyClockMilliseconds();
        if (wait_time < 0) 
            wait_time = 0;
    }
    return wait_time;
}

需要注意的是定时器设置要在IO线程中被执行

总结

  1. eventfd是Linux系统提供的一种高效、灵活的事件通知机制,在涉及在异步编程、资源同步、资源管理等使用场景下可以考虑。
  2. loop线程的事务与资源只能在loop线程中使用,外部线程想调用loop线程的资源,要使用runInLoop()进行委托。也就是所谓的:线程隔离。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值