暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MySQL Thread Pool 分析 一(概念与实现)

自己的设计师 2016-11-11
482

    在写这篇文章之前,我没有太多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的实现来进行分析,同时实现一个类似的线程池。

文章转载自自己的设计师,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论