WebServer(4) EventLoop类
本文最后更新于:8 个月前
WebServer(4) EventLoop类
写在前面
源码链接:https://github.com/yxiao-yang/Tiny-Webserver.git
正文
1 EventLoop的作用
EventLoop 可以算是 muduo 的核心类了,EventLoop 对应着事件循环,其驱动着 Reactor 模型。我们之前的 Channel 和 Poller 类都需要依靠 EventLoop 类来调用。
- Channel 负责封装文件描述符和其感兴趣的事件,里面还保存了事件发生时的回调函数
- Poller 负责
I/O
复用的抽象,其内部调用epoll_wait
获取活跃的 Channel - EventLoop 相当于 Channel 和 Poller 之间的桥梁,Channel 和 Poller 之间并不之间沟通,而是借助着 EventLoop 这个类。
这里上代码,我们可以看见 EventLoop 的成员变量就有 Channel 和 Poller。
1 |
|
其实 EventLoop 也就是 Reactor
模型的一个实例,其重点在于循环调用 epoll_wait
不断的监听发生的事件,然后调用处理这些对应事件的函数。而这里就设计了线程之间的通信机制了。
最初写socket
编程的时候会涉及这一块,调用epoll_wait
不断获取发生事件的文件描述符,这其实就是一个事件循环。
1 |
|
2 EventLoop重要成员变量
1 |
|
wakeupFd_
:如果需要唤醒某个EventLoop
执行异步操作,就向其wakeupFd_
写入数据。activeChannels_
:调用poller_->poll
时会得到发生了事件的Channel
,会将其储存到activeChannels_
中。pendingFunctors_
:如果涉及跨线程调用函数时,会将函数储存到pendingFunctors_
这个任务队列中。
3 EventLoop重要方法
3.1 判断是否跨线程调用 IsInLoopThread()
muduo 是主从Reactor
模型,主Reactor
负责监听连接,然后通过轮询方法将新连接分派到某个从Reactor
上进行维护。
- 每个线程中只有一个
Reactor
- 每个
Reactor
中都只有一个EventLoop
- 每个
EventLoop
被创建时都会保存创建它的线程值。
1 |
|
1 |
|
3.2 EventLoop 创建之初都做了什么?
1 |
|
这里关注一下eventfd
的创建,创建时指定其为非阻塞
1 |
|
3.3 EventLoop 销毁时的操作
1 |
|
- 移除
wakeupChannel
要监视的所有事件 - 将
wakeupChannel
从Poller
上移除 - 关闭
wakeupFd
- 将EventLoop指针置为空
3.4 EventLoop 事件驱动的核心——Loop()
调用 EventLoop::loop() 正式开启事件循环,其内部会调用 Poller::poll -> ::epoll_wait
正式等待活跃的事件发生,然后处理这些事件。
- 调用
poller_->Poll(kPollTimeMs, &activeChannels_)
将活跃的 Channel 填充到 activeChannels 容器中。 - 遍历 activeChannels 调用各个事件的回调函数
- 调用
DoPengdingFunctiors()
处理跨线程调用的回调函数
1 |
|
3.5 EventLoop 如何执行分派任务
EventLoop 使用 runInLoop(Functor cb)
函数执行任务,传入参数是一个回调函数,让此 EventLoop 去执行任务,可跨线程调用。比如可以这么调用:
1 |
|
这是 TcpServer
类的一处代码,其在接收了一个新连接后会创建一个对应的TcpConnection
对象来负责连接。而TcpConnection
是需要执行一些初始化操作的,这里就是让EventLoop执行TcpConnection
的初始化任务。
我们继续看一下 EventLoop::RunInLoop
的内部
1 |
|
IsInLoopThread
判断本线程是不是创建该 EventLoop 的线程
- 如果是创建该 EventLoop 的线程,则直接同步调用,执行任务
- 如果不是,则说明这是跨线程调用。需要执行
QueueInLoop
3.6 EventLoop 是如何保证线程安全的
还以上述的那个例子:
1 |
|
这里获取的 ioLoop 是从线程池中某个线程创建而来的,那么可以知道创建 ioLoop 的线程和目前运行的线程不是同一个线程,那么这个操作是线程不安全的。
一般为了保证线程安全,我们可能会使用互斥锁之类的手段来保证线程同步。但是,互斥锁的粗粒度难以把握,如果锁的范围很大,各个线程频繁争抢锁执行任务会大大拖慢网络效率。
而 muduo 的处理方法是,保证各个任务在其原有的线程中执行。如果跨线程执行,则将此任务加入到任务队列中,并唤醒应当执行此任务的线程。而原线程唤醒其他线程之后,就可以继续执行别的操作了。可以看到,这是一个异步的操作。
接下来继续探索QueueInLoop
的实现:
1 |
|
QueueInLoop
的实现也有很多细节,首先可以看到在局部区域生成一个互斥锁(支持RALL
),然后再进行任务队列加入新任务的操作。
这是因为可能此EventLoop
会被多个线程所操纵,假设多个线程调用loop->QueueInLoop(cb)
,都向此任务队列加入自己的回调函数,这势必会有线程间的竞争情况。需要在此处用一个互斥锁保证互斥,可以看到这个锁的粒度比较小。
再往下,注意下方这个判断,if (!IsInLoopThread() || callingPendingFunctors_)
,第一个很好理解,不在本线程则唤醒这个 EventLoop
所在的线程。第二个标志位的判断是什么意思呢?
callingPendingFunctors_
这个标志位在 EventLoop::DoPendingFunctors()
函数中被标记为 true。 也就是说如果 EventLoop 正在处理当前的 PendingFunctors 函数时有新的回调函数加入,我们也要继续唤醒。 倘若不唤醒,那么新加入的函数就不会得到处理,会因为下一轮的 epoll_wait 而继续阻塞住,这显然会降低效率。这也是一个 muduo 的细节。
继续探索 Wakeup()
函数,从其名字就很容易看出来,这是唤醒其他线程的操作。如何唤醒那个EventLoop
的所在线程呢,其实只要往其 wakeupFd_
写数据就行。
每个EventLoop
的wakeupFd_
都被加入到epoll
对象中,只要写了数据就会触发读事件,epoll_wait
就会返回。因此EventLoop::loop
中阻塞的情况被打断,Reactor
又被事件「驱动」了起来。
1 |
|
这里可以看另一个例子,TcpServer
的销毁连接操作。会在baseLoop
中获取需要销毁的连接所在的ioLoop
,然后让ioLoop
执行销毁操作,细节可以看注释。
1 |
|
3.7 EventLoop 是如何处理 pendingFunctors 里储存的函数的?
这里又是一处细节。我们为什么不直接遍历这个容器,而是又不嫌麻烦地定义了一个 functors 交换我们 pendingFunctors 的元素,然后遍历 functors?
我们如果直接遍历 pendingFunctors,然后在这个过程中别的线程又向这个容器添加新的要被调用的函数,那么这个过程是线程不安全的。如果使用互斥锁,那么在执行回调任务的过程中,都无法添加新的回调函数。这是十分影响效率的。
所以我们选择拷贝这个时候的回调函数,这样只需要用互斥锁保护一个交换的操作。锁的粗粒度小了很多。我们执行回调操作就直接遍历这个functors
容器,而其他线程继续添加回调任务到 pendingFunctors
。
1 |
|
3.8 主动关闭事件循环时会发生什么?
1 |
|
quit_
置为 true- 判断是否是当前线程调用,若不是则唤醒
EventLoop
去处理事件。
第二点可以深究一下,通过 while(!quit_)
可以判断,唤醒之后会继续处理一轮事件,然后再进入判断语句,然后因为 quit_ = true
而退出循环。
所以如果可以调用成功,说明之前的任务都处理完了,不会出现正在处理任务的时候突然退出了。但是不能保证未来事件发生的处理。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!