在写这篇文章之前,我没有太多c/c++的编程经验,但是本着对数据库线程池的好奇,还是打算从头学起。而我觉得学习一个产品或者一个理论最好的方式就是实现一遍,debug一遍,从中找出优秀的东西以及不好的设计。
写这篇文章之前,我们会用到 pthread库(POSIX thread (pthread) libraries),下面我们来设计一个简单的线程池的实现。
设计方式一
假设一个线程池有四个线程(初始化好),每次一个请求过来,线程池中的线程都会被唤醒,并且执行该请求: 
Thd1,Thd2,Thd3,Thd4都被唤醒,此时Thd2运气好,获得了执行该请求的机会,其他线程则继续等待。
代码实现如下:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *thread_accept_request(void *message);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condition_var = PTHREAD_COND_INITIALIZER;
int main(int argc, const char * argv[]) {
pthread_t tdlist[4];
char *m[4]={"Thd1","Thd2","Thd3","Thd4"};
int i=0;
for(i=0;i<4;i++)pthread_create(&tdlist[i],NULL,thread_accept_request,(void *)m[i]);
while(1){
sleep(2);
pthread_cond_signal(&condition_var); 请求过来,唤醒相应的Thread进行处理
}
}
//具体处理的函数主体
void *thread_accept_request(void *message){
while(1){
pthread_mutex_lock(&mutex);
pthread_cond_wait( &condition_var, &mutex );
printf("accept request %s\n",message);
pthread_mutex_unlock(&mutex);
}
return NULL;}
这种设计的缺陷主要有两点:
1.如果每次很多请求过来,则每次被唤醒的线程需要处理完所有的请求,否则这些请求会被丢弃。同时,也导致某个线程比较繁忙,而其他比较空闲的线程却不能协助处理。
2.每次唤醒,只会有一个优先级最高的线程/等待时间最长的线程被唤醒(pthread_cond_signal决定),其他线程则处于空闲状态。
设计方式二

所有请求过来了先放入队列,然后唤醒一个线程去队列取请求进行处理,其他线程则在满足特定的条件下取请求进行处理。我们可以认为有4个线程处于等待状态(伪代码如下):
while(1){
pthread_mutex_lock(&mutex);
pthread_cond_wait(&condition,&mutex);
request=queue.pop();
pthread_mutex_unlock(&mutex);
if(request){
process_request(request)
}
}
//一旦有请求过来,则唤醒其中一个线程进行处理:
if(request_is_coming){
for(i=0;i<n_events;i++)
queue.push(event[i]);
pthread_cond_broadcast(&condition); 唤醒线程进行处理
}
这种设计方式的优缺点:
我们可以看到,这种方式设计的优点非常明显,容易实现。缺点就是多个event过来,每次唤醒所有的Thread进行处理,但是如果线程数小于请求数,最终会导致队列爆满,大量请求堆积。同时,每次唤醒所有的请求,会存在对mutex的抢占,导致cpu过高(所谓的惊群效应)。那么有没有什么更好的优化方案呢 ?
1.pthread_cond_wait 替换成 pthread_cond_timeoutwait。过一定的时间后,线程会自动醒来,并去队列里去请求进行处理。
2.有另外一个线程去检测当前队列的情况,比如没10ms检测一次,发现队列中有请求,但是10ms内没有线程处理过请求,则尝试唤醒一个线程从队列中取一个请求进行处理。
3.设置一个Listener线程专门的监听事件,其他的线程则等待被监听线程唤醒,取出相应的event进行处理。当然,也不能每次全部唤醒,这样惊群效应仍然无法避免。所以,可以把thread 分为两类:active/waiting(active标识正在处理任务的线程,waiting标识等待调度的线程),并将waiting的worker线程放入waiting队列,这样,每次有新的任务到来,Listener除了将任务放入到等待队列中外,还需要将waiting队列中的线程取一个并唤醒它,让它处理当前等待队列中的任务。简单的图形描述如下:

这种方式的当然也存在很多缺点:
1.比如请求过多,Listener线程就会很繁忙。比如,100个请求过来,Listener线程需要将100个请求放入等待队列中,然后再唤醒/创建相应的worker线程进行处理。而此在这过程中,又有100个请求过来了,此时单个Listener线程可能处理不过来。最终这种情况往复循环,导致大量请求被阻塞。
2.整个请求队列是用一把mutex来控制,如果waiting线程取请求进行处理与Listener将请求塞入请求队列同时发生/多个waiting线程同时醒过来取请求进行处理,则会发生请求队列 mutex的争用,对整个系统的性能带来一定的影响。
设计方式三
为了解决单个队列由于单Listener线程带来的阻塞问题,因此我们可以讲wokers+Listener线程看成一个Thread Group。这样多个Thread Group就会有多个Listener线程,而且每个Group对应一个任务请求队列,这样通过multi instance的方式就缓解了单个Thread Group的竞争问题。

这种方式很好的优化了上述的惊群效应和请求堆积问题,同时多个Thread Group缓解了单个Group的竞争问题,但是也带来了实现的复杂性。
这中方式的缺点:
1.所有的请求都平等的放入队列中,可能有的请求并不需要及时处理,而有的请求需要被及时处理,没有做很好的区分 。同时,同一个Thread Group中Active的线程过多,也可能造成CPU处理不过来。
优化措施:
1.设置优先级队列和普通队列,将优先级比较高的请求放入优先级队列,而将优先级低的请求放入低优先级队列。同时限制每个Group中允许Active的线程数 。
每次从高优先级队列取请求,如果没有,则从低优先级队列取请求:

现实案例:
比如一个MySQL中,一个Transaction已经开始,我们认为这个Transaction结束之前的后续的请求都需要被及时处理,因此都需要放入高优先级队列。而比如普通的connection认证请求,我们认为它可以不被及时处理,所以放入低优先级队列。
通过这种优化,我们将紧急请求放入快车道,非紧急请求放入慢车道,很好的优化了并发处理请求数的同时,也优化了用户体验 。当然,线程池的实现远比我们上述讲解的要复杂的多,后续会根据Percona Thread Pool的实现来进行分析,同时实现一个类似的线程池。




