上一篇文章讲了Reactor模式的关键结构I/O复用和事件分发,现在我们来关注一下它们的使用。
我们已经实现了一个Epoller类来实现I/O复用,具体的使用方法就是Epoller::Poll()函数等待事件的发生,该函数有一个超时时间,超过这个时间即使没有事件发生也会返回,那么我们如何让它一直工作呢?很明显就是使用while循环。
一个事件循环的大概逻辑如上图,就是循环反复地调用Poll(),现在我们抽象出一个类Looper来管理这个循环。
下面是部分Looper的代码,在这个类中我们持有唯一的一个Epoller指针,通过AddEventBase()等函数往Epoller上注册或修改事件,注意,我们传入的事件是经过包装后的EventBase类,里面已经有了事件处理函数了。我们可以通过Start()开始事件循环,所有的处理都在该循环内完成。
class Looper
{
public:
Looper();
~Looper();
// 开始事件循环
void Start();
// 注册事件
void AddEventBase(std::shared_ptr<EventBase> eventbase) { epoller_->Add(eventbase); }
void ModEventBase(std::shared_ptr<EventBase> eventbase) { epoller_->Mod(eventbase); }
void DelEventBase(std::shared_ptr<EventBase> eventbase) { epoller_->Del(eventbase); }
private:
// 底层的I/O复用类
std::unique_ptr<Epoller> epoller_;
};
一个线程只能有一个Looper。现在让我们考虑一下多个线程下Looper之间如何分配任务,为后面打下基础。
如上,现在这个进程中有三个线程,他们共享了进程的地址空间,每个线程各有一个事件循环Looper,如何实现在Looper_1上让Looper_2执行某个函数呢?
这里我们可以在每个事件循环中都增加一个任务队列,并暴露出相应的接口,Looper_1通过Looper_2的接口往后者的任务队列里投放任务,Looper_2再从中取出执行即可,为此这里需要修改一下事件循环的逻辑。
在每次发生事件后,处理完事件后不再是直接等待下一次事件发生,而是先检查任务队列是否有任务,处理完任务队列后再继续等待,这样就可以实现线程间的任务分配了。
但是这里又出现了一个问题,就是Looper_1把任务丢到Looper_2的任务队列后,Looper_2并不能及时的执行该任务,它只有在发生事件或者Poll()超时之后才能得到机会执行,显然这是无法容忍的。解决问题的办法就是在Looper_2上注册一个用来唤醒的文件描述符,在把任务放入Looper_2的任务队列后,往该文件描述符上写一个字符,那么Looper_2就会监测到该文件描述符发生了可读事件,也就从Poll()返回,开始处理这个可读事件和任务队列了。
下面就是关于线程间任务分配和唤醒处理的相关接口,其他线程通过AddTask()把任务交给该Looper的任务队列,然后可通过WakeUp()唤醒该Looper去处理任务。因为任务队列可由多个线程访问,所有需要加上互斥锁进行保护。
class Looper
{
public:
using Task = std::function<void()>;
// 唤醒循环以处理Task,配合Handle使用
void WakeUp();
void HandleWakeUp();
void RunTask(Task&& task);
void AddTask(Task&& task);
void HandleTask();
private:
// 用来唤醒的描述符以及关注该描述符的事件基类
int wakeup_fd_;
std::shared_ptr<EventBase> wakeup_eventbase_;
// 任务队列以及保护该队列的互斥锁
std::mutex mutex_;
std::vector<Task> task_queue_;
};
有的时候我们需要在一段时间后在Looper上执行某个任务,而不是立刻执行,这时就需要定时器了。
首先看Timer,我们称之为定时器,里面封装了发生的时间以及对应要执行的函数。
有了多个定时器,就需要一个TimerQueue队列来管理定时器,管理的策略也很简单,即一个优先队列,发生时间先的定时器排在前面:
要将定时器整合到Looper上,我们使用的方法是把定时器当做一个事件,具体来说就是申请一个定时器文件描述符,我们可以设置该文件描述符的超时时间为定时器队列队头的时间,当到达这个时间时该文件描述符上就会发生可读事件,这时我们就从定时器队列上把队头的定时器执行掉,接着更新该文件描述符的超时时间为新的队头的时间,然后等待下一次超时,如此循环反复。
在Looper上我们暴露出来一个接口,传入一个任务和一段时间间隔,Looper会把该任务变成一个定时器放入定时器队列中,并检查是否需要更新定时器文件描述符的超时时间。当该定时器时间到期后便会执行该任务。
void RunTaskAfter(Task&& task, Nanosecond interval);
关于时间,这里我使用的是c++11的std::chrono类,定义在base/timestamp.h,具体的使用读者可以去查询相关资料,这里不做赘述。