目录
设计思路
我们说过一个EventLoop要绑定一个线程,**未来该EventLoop所管理的所有的连接的操作都需要在这个EventLoop绑定的线程中进行,**所以我们该如何实现将EventLoop和线程绑定呢?
按照我们前面实现的EventLoop的逻辑,构造函数的时候,_thread_id(std::this_thread::get_id()) 也就是EventLoop构造时就直接绑定了当前线程了,**那么我们就需要先创建线程,然后在线程的入口函数中来创建一个EventLoop对象,这样我们的EventLoop对象就和一个线程绑定了,**创建完之后它可以将这个EventLoop的指针返回给TcpServer用来分配给新连接进行关联。
那么我们可不可以先创建一批EventLoop对象再来绑定线程呢?
从技术的角度来说当然可以,我们只需要设置一个接口用来设置EventLoop的_thread_id就行了,但是这样做会存在一个问题: 当我们创建一批EventLoop的时候,再创建一批线程来进行绑定之前,是有一个时间窗口 的,在这个时间窗口中,如果有新连接到来,且有新事件到来,那么就会出现问题。
就好比一家繁忙的餐厅,你是经理需要分配服务员(线程)负责特定的餐桌区域(EventLoop)。**正确的做法是:先雇佣服务员,培训他们,然后才开门营业接待客人。**但如果你决定先开门营业,让餐桌区域准备好接待客人,然后才开始招聘和分配服务员,就会出现危险的时间窗口:
客人已经入座点餐(连接已建立),甚至食物已经准备好(事件已到来),但没有服务员知道这些餐桌是他们负责的!结果就是客人坐在那里等待,没人来服务他们,食物在厨房里变冷,而餐厅陷入混乱。
也就是我们的_thread_id还没有切换到我们想要的线程id上,还在创建EventLoop的线程中,那么这时候操作就是由这个创建EventLoop的线程执行了,而后续切换线程之后,又会由新的线程来执行操作,那么就会出现一个连接的所有操作并不在一个线程中全部完成,可能会出现在多个线程中执行的情况,
再想象这样一个场景:餐厅开始营业,最初由经理(创建EventLoop的线程)临时担任服务员的角色,开始接待客人和处理订单。然后在客人就餐过程中,经理突然告诉新来的服务员:"从现在开始,这些桌子由你负责了",然后经理离开去做其他工作。
这会导致严重的混乱:
- 服务员不知道这些客人已经点了什么菜
- 不清楚客人的特殊要求或过敏信息
- 不了解客人的用餐进度
- 甚至可能重复上菜或忘记上某些菜品
客人的体验会非常糟糕,因为他们的服务被分割在两个不同的服务人员之间,没有连贯性和一致性。
这是我们不想看到的,我们要确保一个连接的所有操作都在一个线程中执行,所以我们不能采取这种方案。
那么我们的方案:就只能是先创建线程再创建EventLop对象,后续会通过特定的方式返回这个EventLoop的指针交给TcpServer进行分配。
为了方便操作,我们可以设置一个新的模块,也就是EventLoopThread模块,专门用于创建一个线程并创建绑定一个EventLoop。
它内部有两个成员,一个就是我们的线程对象,另一个就是我们的EventLoop的指针。
所以我们要设置线程入口函数,然后在线程入口函数中创建一个EventLoop对象,创建完之后执行EventLoop的Start来完成事件的循环。
**但是这时候就会有一个问题,****因为线程的创建到创建好一个EventLoop对象并设置指针变量是有一个时间窗口的,**同时,在设置我们的指针成员的同时,有可能由其他的线程需要获取这个指针,那么就会出现读写并发的情况,或者说这个指针变量会有线程安全问题。
我们还拿餐厅的例子进行加以理解
想象餐厅正在准备开业,**经理在招聘并培训新的服务员。**每个服务员需要熟悉自己的工作区域、学习餐厅系统并拿到自己的工作牌(相当于创建EventLoop并设置线程ID的过程)。
这时候,另一位经理(其他线程)已经开始在前台接待客人,并试图将客人分配给"正在培训中"的服务员。但问题是,这些服务员可能还没有完成培训,没有拿到工作牌,甚至可能还没有被正式雇佣!
那么我们需要保护这个指针指针变量的互斥与同步访问,需要使用一个互斥锁和一个条件变量来保证指针的安全。
在餐厅中,解决方案也是使用一个协调板和一个明确的流程:
- 招聘和培训完全结束后,才将服务员的信息放到协调板上
- 前台经理只查看协调板上已确认可用的服务员
- 使用一个信号系统(如专门的管理员)确保协调板的更新和查看不会同时发生
类的设计
综上我们知道了EventLoop的流程
- 首先,EventLoopThread对象被创建(可能在主线程或其他线程中)
- 当调用EventLoopThread的构造方法时,它会创建一个新的线程
- 这个新线程开始执行StartRoutine()函数
- 在StartRoutine()函数内部,线程创建一个新的EventLoop对象
- 线程将这个EventLoop对象的指针安全地赋值给共享变量_loop
- 通知等待的线程EventLoop已经创建完成
- 开始运行EventLoop的事件循环

那么EventLoopThread类的成员如下:
cpp
class EventLoopThread
{
private:
EventLoop* _loop;
std::thread _thread;
std::mutex _mutex; //保护_loop安全
std::condition_variable _cond; //实现同步
private:
//线程的入口函数
void StartRoutine();
public:
EventLoopThread(){}
//提供一个接口用于获取内部的EventLoop
//意味着这个_loop会被多个线程竞争,那么需要锁和条件变量来实现同步互斥
//因为未来线程刚创建的时候,在还没有创建好EventLoop对象的时候,这时候就可能会被主线程或者其他线程来获取Loop了,那么这时候是线程不安全的,所以需要加锁保护
//同时,为了防止线程中的EventLoop对象还没创建就有线程来获取,我们需要再使用一个条件变量。 申请到锁之后,如果条件不满足,线程还需要在条件变量下等待,直到条件满足再来竞争锁并获取锁
EventLoop* GetEventLoop();
};
模块的实现
入口函数很简单,无非就是加锁创建完EventLoop对象之后,唤醒在条件变量下等待的线程,然后就开始执行EventLoop的Start循环逻辑。
私有接口
cpp
//线程的入口函数
void StartRoutine()
{
//加锁创建对象
EventLoop* loop = new EventLoop();
{
std::unique_lock<std::mutex> lock(_mutex);
_loop = loop;
}
//唤醒条件变量下的线程
_cond.notify_all();
//启动EventLoop循环
loop -> Start();
}
公有接口
然后就是构造函数,无非就是初始化_loop和设置thread的入口函数
cpp
EventLoopThread()
:_loop(nullptr)
,_thread(std::bind(&EventLoopThread::StartRoutine, this)) // 创建一个新线程,并指定StartRoutine作为线程的入口函数
{}
剩下的就是一个获取EventLoop的接口了,其实无非就是加锁和条件变量下等待这两个步骤:
cpp
EventLoop* GetEventLoop()
{
EventLoop* ret = nullptr;
{
std::unique_lock<std::mutex> lock(_mutex); //加锁
_cond.wait(lock,[&](){return _loop!=nullptr;}); //判断函数返回值为真
//走到这里说明被唤醒了
ret = _loop;
}
return ret;
}