Reactor模式
EventLoop
实现逻辑分析
针对于EventLoop的设计还是严格遵循其核心思想one loop per thread思想,也就是说一个线程只可以拥有一个EventLoop实例,那么为什么这样实现?主要有以下两点原因
- 线程安全性:多线程环境下,确保每一个线程只有一个EventLoop实例,这样就可以避免线程竞争条件,因为EventLoop内部大部分操作都是线程不安全的,必须让其所属线程来执行
- 并发性能:将每个EventLoop限定在一个特定的线程中,mudou库可以在多线程环境中并行处理事件,避免了线程之间频繁加锁和竞争的性能开销
EventLoop每次执行的时候,都会检查当前线程是否已经创建了EventLoop实例
EventLoop::EventLoop()
: looping_(false),
threadId_(CurrentThread::tid()) // 获取当前线程ID
{
LOG_TRACE << "EventLoop created " << this << " in thread " << threadId_;
if (t_loopInThisThread) // 检查当前线程是否已有 EventLoop 实例
{
LOG_FATAL << "Another EventLoop " << t_loopInThisThread
<< " exists in this thread " << threadId_;
}
else
{
t_loopInThisThread = this; // 绑定当前线程的 EventLoop 实例
}
}
EventLoop内部提供了线程安全检查接口
- isInLoopThread():判断调用是否在EventLoop创建的线程中执行,如果当前线程的ID与EventLoop中保存的线程ID不匹配,那么就可以证明不是EventLoop所属的线程调用的
- assertInLoopThread():该函数的主要作用就是调用线程不在EventLoop创建的线程中,则终止程序运行
void EventLoop::assertInLoopThread()
{
if (!isInLoopThread())
{
abortNotInLoopThread(); // 终止程序
}
}
bool EventLoop::isInLoopThread() const
{
return threadId_ == CurrentThread::tid(); // 检查当前线程ID是否与EventLoop线程一致
}
EventLoop的生命周期跟随其线程相同,线程退出其也会跟着退出,它会析构之间进行检查
EventLoop::~EventLoop()
{
assert(!looping_); // 事件循环必须已经停止
t_loopInThisThread = NULL; // 清空当前线程的EventLoop指针
}
EventLoop::loop()是时间循环的核心,负责监听文件描述符等操作(代码对源码进行了缩减)
void EventLoop::loop()
{
assert(!looping_);
assertInLoopThread(); // 检查是否在EventLoop线程中调用
looping_ = true;
while (!quit_) // 事件循环,直到 quit_ 被设置为 true
{
poller_->poll(kPollTimeMs, &activeEvents); // 轮询I/O事件
for (EventList::iterator it = activeEvents.begin();
it != activeEvents.end(); ++it)
{
(*it)->handleEvent(); // 处理事件
}
doPendingFunctors(); // 执行待处理的任务
}
looping_ = false;
}
总结EventLoop的设计原则
- 每个线程只拥有一个EventLoop实例
- EventLoop的所有操作都是与其绑定线程去执行
- 内部设计的有线程安全检查机制,确保线程正确性和安全性
线程池与多进程方式对该架构优化思路
线程池优化思路
- 主线程:该线程持有一个Reactor,专门负责接收新连接,然后分发新连接给工作线程
- 线程池:多个工作线程组成,每一个工作线程都有自己的EventLoop,专门处理交付给自己的连接
- 任务分发:新连接到来的时候,主线程可以根据自己的需要使用轮询或者哈希或者其他更加高效的方式交付给任务给工作线程
- 任务处理:工作线程开始处理交付给自己的任务
多进程优化思路
- 主进程:创建监听套接字,并通过fork()创建多个子进程
- 工作进程:每一个工作进程都运行自己的EventLoop,专门负责处理来自客户端的请求,可以通过进程间通信的方式分发信息
- 负载均衡:新连接到来后,主进程就将闲着的工作进程干活
I/O线程在modou网络库中作用分析
服务器中采用I/O线程管理客户端连接(一个I/O线程也就是一个Reactor)
- 非阻塞I/O:I/O线程与非阻塞I/O配合使用功能,这样就可以避免因等待I/O操作完成而被阻塞,从而提升系统的吞吐量
- 事件驱动: I/O线程通过事件驱动机制来管理I/O事件,只有当I/O事件发生的时候才进行处理,从而有效提高性能
I/O线程在mudou的作用分析
每一个EventLoop对象都会绑定到一个线程,这个线程也就是I/O线程,然后EventLoop负责管理和处理该线程中所有的I/O事件
- socket读写事件:当有数据可读或者可写的时候,I/O线程会被唤醒,并且通过回调函数来处理对应的事件
- 定时器事件:I/O线程可能会管理一些定时任务,确保在特定的时间点触发对应的回调函数
- 任务队列:一些异步任务可以通过runInLoop()函数传递给I/O线程中,从而确保任务在I/O线程中安全的被执行
I/O线程的理解
I/O线程的核心功能就是等待并处理I/O事件,而不阻塞其他任务的执行,利用该设计允许服务器在处理大量的I/O操作的时候可以保持高效快速的效应,不需要为每一个连接创建单独的线程
因为I/O线程非常适合高并发场景,所以其应用主要在于以下几个方面
- HTTP/HTTPS服务器:处理多个客户端请求,同时保障每个连接的I/O操作都是非阻塞的
- 即时通讯:如果在大规模消息通信过程中,通过事件驱动机制,可以实现延迟消息的转发
- 数据流处理系统:对来自多个客户端的数据进行读取、处理和存储
Reactor重要结构
Channel类
核心功能分析
- I/O事件的监听和回调处理
- Channel是不拥有文件描述符的,而是绑定一个fd监听这个fd上发生的I/O事件,真正管理fd的是Connection
- Channel对不同的事件进行监听,并且可以为每一种事件类型设置对应的回调函数
- Channel与EventLoop关系
- 每个Channel对象只属于一个EventLoop,EventLoop负责管理Channel并调用其预先设置好的回到函数
- 具体实现逻辑,I/O事件发生的时候,EventLoop调用Poller首先检查fd的状态,然后通知对应Channel来处理函数
- 事件管理
- Channel内部通过标记events_ 和 revents来管理所关心的事件和当前发生的事件
- 文件描述符的管理
- Channel类中fd_表示的是当前监听的文件描述符,events_则是表示Channel关心的事件,revents_则是实际发生的事件
Poller类
Poller功能分析
- Poller类核心作用:监控多个文件描述符,然后将这些文件描述符分发给对应的Channel进行处理
- Poller是一个I/O多路复用机制的封装,在modou库中既封装了poll 还封装了epoll ,其生命周期是和EventLoop的生命周期一致的
内部机制分析
- pollfds_:因为Poller是通过pollfds_保存所有正在监听的文件描述符和其事件
- 内部也提供函数,当找到活跃的fd的时候,通知对应的Channel进行处理
void Poller::fillActiveChannels(int numEvents, ChannelList* activeChannels) const
{
for (PollFdList::const_iterator pfd = pollfds_.begin(); pfd != pollfds_.end() && numEvents > 0; ++pfd)
{
if (pfd->revents > 0)
{
--numEvents;
ChannelMap::const_iterator ch = channels_.find(pfd->fd);
Channel* channel = ch->second;
channel->set_revents(pfd->revents); // 设置事件
activeChannels->push_back(channel); // 添加到活跃的Channel列表中
}
}
}
梳理Channel与Poller类之间交互逻辑
- 新连接到达服务器后,服务器为该连接的socket创建一个Channel,同时将socket的文件描述符传递给Channel(Channel并不管理文件描述符,文件描述符的生命周期是由Connection来进行管理的)
- 服务器设置该Channel的回调函数(例如可以设置其读写异常等回调处理函数)
- 服务器通过Poller::updateChannel()会将Channel注册

最低0.47元/天 解锁文章
4341

被折叠的 条评论
为什么被折叠?



