在muduo库里边有两种线程:一种里边的事件循环专门处理新用户连接(`mainLoop`( 也就是`baseLoop`)),一种里边的事件循环专门处理对应连接的所有读写事件(`ioLoop`)。
EventLoop起到一个驱动循环的功能,Poller负责从事件监听器上获取监听结果,Channel类将fd及其相关属性封装,并将fd及其感兴趣事件和发生的事件以及不同事件对应的回调函数封装在一起,这样在各个模块中传递更加方便。接着被EventLoop调用。
成员变量
private:
//当mainloop获得一个新用户的channel,通过轮询算法选择一个subloop,通过该成员唤醒subloop处理
int wakeupFd_; // 用于唤醒 EventLoop 的文件描述符,
std::unique_ptr<Channel> wakeupChannel_; // 唤醒通道,与 wakeupFd_ 关联
`wakeupFd_`是非常重要的一个成员,与之对应的`wakeupChannel_`,起到了一个唤醒`loop`所在的线程的作用,因为当前线程主要阻塞在`poll`函数上,唤醒的方法时手动激活这个`wakeupChannel_`, 写入几个字节让`Channel`变为可读, 当然这个`Channel`也注册到`Pooll`中
std::atomic<bool> looping_; // 是否正在进行事件循环
std::atomic<bool> quit_; // 退出标志
//bool eventHandling_; // 是否正在处理事件
//int iteration_; // 事件循环的迭代次数
std::thread::id threadId_; // 当前 EventLoop 所在的线程 ID
std::unique_ptr<Poller> poller_; // Poller 对象,用于监听文件描述符的事件
Timestamp pollReturnTime_; // poll 函数返回的时间戳
std::unique_ptr<TimerQueue> timerQueue_; // 定时器队列,用于处理定时任务
ChannelList activeChannels_; // 活动通道列表,存储当前有事件发生的通道
Channel *currentActiveChannel_; // 当前正在处理的活动通道
std::atomic<bool> callingPendingFunctors_; // 是否正在调用待处理的函数对象
mutable std::mutex mutex_; // 互斥锁,用于保护待处理函数对象列表
std::vector<Functor> pendingFunctors_; //// 待执行的任务队列,loop所有需要处理的回调操作
// 线程局部变量,用于存储当前线程的 EventLoop 指针,防止一个线程创建多个EventLoop
__thread EventLoop *t_loopInThisThread = nullptr;
// poll 函数的超时时间,单位为毫秒
const int kPollTimeMs = 10000; //10s
// 函数类型定义,用于表示一个无参数无返回值的函数对象
using Functor = std::function<void()>;
// 定时器回调函数类型定义
using TimerCallback = std::function<void()>;
// 通道列表类型定义
using ChannelList = std::vector<Channel*>;
重要方法
`poller`监听哪些`channel`发生了事件,然后上报给`EventLoop`,通知`channel`处理相应的事件
// 开始事件循环
void loop()
{
assert(!looping_);
assertInLoopThread(); // 断言处于创建该对象的线程中
looping_ = true;
quit_ = false; // 重置退出标志
LOG_TRACE << "EventLoop " << this << " start looping";
while (!quit_)
{
// 清空活动通道列表
activeChannels_.clear();
// 监听两类fd,一种是client的fd,另一种是wakeupFd
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
// 遍历活动通道列表,处理每个通道的事件
for (auto it : activeChannels_)
{
currentActiveChannel_ = it;
it->handleEvent(pollReturnTime_);
}
currentActiveChannel_ = NULL;
// 处理待处理的函数对象
//执行当前EventLoop事件循环需要处理的回调函数
doPendingFunctors();
}
LOG_INFO<<"EventLoop %p stop looping"<<this<<endl;
looping_ = false;
}
从代码中,我们可以看出最核心的部分就是调用了Poller的`poll`方法,它返回了发生的事件channel列表以及发生的时间`now`,接着可以看出还有一个`doPendingFunctors`函数
`mainLoop`事先注册一个回调`cb`(需要`subLoop`来执行)`wakeupFd`唤醒`subLoop`后,执行`cb`
//处理待处理的函数对象
void doPendingFunctors()
{
std::vector<Functor> functors;
callingPendingFunctors_ = true;
{
// 加锁,确保线程安全
std::lock_guard<std::mutex> lock(mutex_);
// 将待处理的函数对象列表交换到局部变量中
functors.swap(pendingFunctors_);
}
// 遍历并调用每个函数对象
for (const Functor &functor : functors)
{
functor();
}
callingPendingFunctors_ = false;
}
`doPendingFunctors()` 是 Muduo 网络库中实现跨线程任务执行的核心机制,它允许在其他线程中向 EventLoop 提交回调函数,并确保这些函数在 EventLoop 所在的线程中执行。实际上,这个函数就是用来执行回调的,值得注意的一点就是: 这里使用了一个比较巧妙的思想就是,使用一个局部的vector和`pendingFunctors_`的交换,这样就避免了因为要读取这个`pendingFunctors_`的时候,没有释放锁,而新的事件往里写得时候写不进去(`mainloop`向`subloop`里面写回调)。
两个关键接口`runInLoop`和`queueInLoop`
// 在当前 EventLoop 所在线程中运行指定的函数对象
void runInLoop(const Functor &cb)
{
if (isInLoopThread())
{
// 若在当前线程中,直接调用函数对象
cb();
}
else
{
//若非当前loop线程中执行cb,就需要唤醒loop所在线程,执行cb
queueInLoop(std::move(cb));
}
}
// 将函数对象添加到待处理队列中,唤醒loop所在线程,执行cb
void queueInLoop(const Functor &cb)
{
{
// 加锁,确保线程安全
std::lock_guard<std::mutex> lock(mutex_);
// 将函数对象添加到待处理队列中
pendingFunctors_.push_back(std::move(cb));
}
//callingPendingFunctors_当前loop正在执行回调,但loop又有了新的回调
if (!isInLoopThread() || callingPendingFunctors_)
{
// 若不在当前线程中或正在调用待处理的函数对象,唤醒 EventLoop 所在线程
wakeup();
}
}
唤醒机制
//向wakeupFd_写一个数据,weakeupChannel就发生读事件,当前loop线程就会被唤醒
void wakeup()
{
uint64_t one = 1;
ssize_t n = SockketsOps::write(wakeupFd_,&one,sizeof one);
if(n != sizeof one)
{
LOG_ERROR("EventLoop::wakeup() writes %lu bytes instead of 8 \n",n);
}
}
// 处理唤醒通道的读事件
void handleRead()
{
uint64_t one = 1;
// 从唤醒通道的文件描述符读取一个 64 位整数
ssize_t n = Sockets::read(wakeupFd_, &one, sizeof(one));
if (n != sizeof(one))
{
// 若读取失败,记录错误日志
LOG_ERROR << "EventLoop::handleRead() reads " << n << "bytes instead of 8";
}
}
muduo并非采用
而是通过`wakeupFd_`直接进行线程间的通信。一句话总结“one loop per thread” 保证了每个 EventLoop 的操作都在唯一线程中执行,而跨线程操作通过异步队列 + 唤醒机制间接实现,既避免了线程安全问题,又保持了系统的灵活性和高性能。