首先,我们得将事件分发器dispatcher抽象出来,dispatcher的主要作用实现socket的监听,注册。抽象的event_dispatcher结构(类)是利用回调函数进行设计的如下所示。我们可以基于这个event_dispatcher去实现基于select,poll或者epoll的模型,这样的思路也是Netty这种框架的基本思路。
struct event_dispatcher { /*对应实现*/const char *name; /*初始化函数 */void *(*init)(struct event_loop * eventLoop); /*通知dispatcher新增一个channel事件*/int (*add)(struct event_loop* eventLoop, struct channel* channel); /*通知dispatcher删除一个channel事件*/int (*del)(struct event_loop* eventLoop,struct channel* channel); /*通知dispatcher更新channel对应的事件*/int (*update)(struct event_loop* eventLoop,struct channel* channel); /*实现事件分发*/int (*dispatch)(struct event_loop* eventLoop,struct timeval*); /*清除数据 */void (*clear)(struct event_loop * eventLoop);};需要注意的是这里面的event_loop是代表一个封装后的线程,这个下面介绍。假设我们这里想实现基于epoll的事件分发机制,这里可以实现如下:
static void *epoll_init(struct event_loop*);static int epoll_add(struct event_loop*, struct channel*);static int epoll_del(struct event_loop*, struct channel*);static int epoll_update(struct event_loop*, struct channel*);static int epoll_dispatch(struct event_loop*, struct timeval*);static void epoll_clear(struct event_loop*);const struct event_dispatcher epoll_dispatcher = { "epoll", epoll_init, epoll_add, epoll_del, epoll_update, epoll_dispatch, epoll_clear,};也就是说,我们最终accept或者accept得到的socket都会委托给epoll_dispatcher里面这几个回调方法进行注册监听读写事件,从而完成数据的读写交互。
2、main-sub线程初始化
接下来,我们先看一下整体的处理流程,其中main-event_loop线程负责accept新连接,然后将新连接封装成channel,并从线程池(eventloopGroup)中根据Round Robin算法选择一个event_loop进行处理IO读写事件,整体的流程如下所示。
这里首先要解决的第一个问题是,main-eventloop如何等子线程初始化完成,也就是说只有将子线程初始化完成才能将main-eventloop得到的连接传递给子线程进行处理,注册到子线程的dispatcher上进行读写事件的监听。
我们这里采用的方式是mutex和condition,如下所示,我们先初始化完成了main-eventloop线程,然后通过main-eventloop线程去创建子线程并初始化,看到下面的for循环了吗?其中event_loop_thread_init函数用于创建该eventloop线程中的cmutx和cond,调用的函数分别是pthread_mutex_init和pthread_cond_init。
void thread_pool_start(struct thread_pool *threadPool){ assert(!threadPool->started); assertInSameThread(threadPool->mainLoop); threadPool->started = 1; if (threadPool->thread_number <= 0) { return; } //创建N个 eventloopThread线程 threadPool->eventLoopThreads = malloc(threadPool->thread_number*sizeof(struct event_loop_thread)); for (int i = 0; i < threadPool->thread_number; ++i) { event_loop_thread_init(&threadPool->eventLoopThreads[i],i); event_loop_thread_start(&threadPool->eventLoopThreads[i]); }}接着,我们进入到event_loop_thread_start函数看看,首先调用pthread_create函数创建一个线程,线程的入库函数就是event_loop_run方法,也就是一个死循环,不断的监听IO事件,处理事件。
从上面的代码可以看到,event_loop_thread_run 一上来也是进行了加锁,之后初始化 event_loop 对象,当初始化完成之后,调用了 pthread_cond_signal 函数来通知此时阻塞在 pthread_cond_wait 上的主线程。这样,主线程就会从 wait 中苏醒,代码得以往下执行。子线程本身也通过调用 event_loop_run 进入了一个无限循环的事件分发执行体中,等待子线程 reator 上注册过的事件发生。//由主线程调用,初始化一个子线程,并且让子线程开始运行event_loop
struct event_loop *event_loop_thread_start(struct event_loop_thread *eventLoopThread) {
pthread_create(&eventLoopThread->thread_tid, NULL, &event_loop_thread_run, eventLoopThread);
assert(pthread_mutex_lock(&eventLoopThread->mutex) == 0);
while (eventLoopThread->eventLoop == NULL) {
assert(pthread_cond_wait(&eventLoopThread->cond, &eventLoopThread->mutex) == 0);
}
assert(pthread_mutex_unlock(&eventLoopThread->mutex) == 0);
return eventLoopThread->eventLoop;
}
void *event_loop_thread_run(void *arg) {
struct event_loop_thread *eventLoopThread = (struct event_loop_thread *) arg;
pthread_mutex_lock(&eventLoopThread->mutex);
// 初始化化event loop,之后通知主线程
eventLoopThread->eventLoop = event_loop_init_with_name(eventLoopThread->thread_name);
pthread_cond_signal(&eventLoopThread->cond);
pthread_mutex_unlock(&eventLoopThread->mutex);
//子线程event loop run
event_loop_run(eventLoopThread->eventLoop);
}
这种main-Eventloop初始化子eventloop的方法,总的来说就是用子线程中的eventloop是否为空,如果为空说明子线程没有初始化,则main eventloop调用pthread_cond_wait出让CPU。子线程一进来就加锁,然后进行初始化。等子线程初始化完成,则调用pthread_cond_signal唤醒main eventloop继续执行。除了这种方式,还有其他eventfd方式也可以进行主线程和子线程初始化,这里不做介绍了。
3、round-robin算法
round-robin算法是一种负载均衡算法,在负载均衡算法中是最简单和最容易理解的一种算法,其核心思路就是按顺序进行分配。在我们这里也是按照子线程的顺序轮流进行获取eventloop,那么怎么获取呢?
struct event_loop *thread_pool_get_loop(struct thread_pool* threadPool){ assert(threadPool->started); assertInSameThread(threadPool->mainLoop); //优先选择当前主线程 struct event_loop *selected = threadPool->mainLoop; //从线程池中按照顺序挑选出一个线程 if (threadPool->thread_number > 0) { selected=threadPool->eventLoopThreads[threadPool->position].eventLoop; if (++threadPool->position >= threadPool->thread_number) { threadPool->position = 0; } } return selected;}
这里的threadPool->thread_number参数是初始化子线程传递进去的,是一个固定值。我们用一个position变量代表取到了第几个线程。当position达到thread_number的时候,则将position清零,也就是从头开始下一轮分配线程eventloop。
这里也可以看出,如果没有子线程,也就是单Reactor模式,我们也不会进入这个选择线程的for循环了,只能选择main eventloop thread进行处理IO事件了。
4、小结
到这里,我们知道了主线程和子线程初始化的顺序和过程,现在初始化好了主线程和子线程,那么下一篇我们将看看主线程如何处理新连接,又如何注册到子线程。子线程收到读写事件,如何处理读写事件。