线程池中遇到的小问题

一个小问题

前言

         服务器的负载在各个时间段是无法预测的,比如某个时间点大量用户访问,虽然现在高并发的服务器都趋向用户处理无阻塞的设计,但类似于磁盘io、或者某些阻塞的操作是难免的,面对这种任务,一般都会把它们扔进线程池。

       服务器的三个池子:内存池、线程池、连接池,都是一次性向系统申请较多资源,在用户态自我管理,以避免频繁同操作系统交互使性能降低。在刚入职的时候我师傅当时定位过一个问题,API操作时延过大,后来发现是malloc操作引发的。还有最近,服务器在测试某个场景下,因为应用程序瞬间提交上万caps的线程创建请求,致使内存消耗殆尽,程序coredump。最近刚看过ngx内存池的设计,相当的巧妙,你可以看到,它几乎没有给操作系统一点产生碎片的机会。

 

线程池

         最近看了网上的资料,以及产品的代码,实现了一个简单的线程池,线程池的操作归于起来无非是:create、destroy、addtask。如果要实现自我管理、动态调整线程池的大小可能还会有伸缩的操作,很多应用程序在刚起来的时候就会一次性向系统申请足量的资源,后面不归还、不再申请,有些应用程序则申请少量资源,跟随系统负载做动态调整。举个简单的例子,某个产品的应用程序在一起就会向系统申请200G+的内存,加载几千个线程,即使没有一个用户,而类似ngx在没有跑起来用户的时候它消耗的系统资源少之又少。

      

#include <stdio.h>

#include <stdlib.h>

#include <pthread.h>

#include <errno.h>

#include <sys/types.h>

#include <sys/stat.h>

 

typedef void (*thread_execute_func)(void*);

typedef struct task_t task_t;

typedef struct queue_t queue_t;

typedef struct thread_pool_t thread_pool_t;

 

struct task_t{

    void* arg;

    thread_execute_func handler;

    struct task_t *next;

};

 

struct queue_t{

    struct task_t *head;

    struct task_t *tail;

    unsigned short task_max;

    unsigned short task_num;

};

 

struct thread_pool_t{

    pthread_key_t   key;

    pthread_mutex_t lock;

    pthread_cond_t  cond;

    unsigned short thread_num;

    struct queue_t task;

};

 

void* pthread_pool_main(void* arg){

    thread_pool_t* pool = (thread_pool_t*)arg;

    task_t* task;

    int run = 1;

    struct timeval now;

    struct timespec time_val;

 

    pthread_detach(pthread_self());

    pthread_setspecific(pool->key, (void*)&run);

 

    while(run){

        pthread_mutex_lock(&pool->lock);

        while(NULL == pool->task.head){

 

            (void)gettimeofday(&now, NULL);

            time_val.tv_sec = now.tv_sec + 2;

            time_val.tv_nsec = 0;

 

            pthread_cond_timedwait(&pool->cond, &pool->lock, &time_val);

            pthread_mutex_unlock(&pool->lock);

            continue;

        }

 

        task = pool->task.head;

        pool->task.head = pool->task.head->next;

        if (NULL ==  pool->task.head){

             pool->task.tail = NULL;

        }

 

        pthread_mutex_unlock(&pool->lock);

 

        if (NULL != task){

            task->handler(task->arg);

            free(task);

        }

    }

 

    printf("thread %lu exit\n", pthread_self());

    pthread_exit(0);

}

 

pthread_key_t g_key;

thread_pool_t *pthread_pool_create(unsigned short thread_num)

{

    pthread_t pid;

    int idx;

    int ret;

 

    if (thread_num < 0){

        return NULL;

    }

 

    thread_pool_t* pool = (thread_pool_t*)malloc(sizeof(thread_pool_t));

    if (NULL == pool){

        printf("malloc a thread_pool_t failed errno %d\n", errno);

        return NULL;

    }

 

 

    if (pthread_key_create(&pool->key, NULL) < 0){

    }

 

    g_key = pool->key;

 

    if (pthread_mutex_init(&pool->lock, NULL) < 0) {

    }

 

    if (pthread_cond_init(&pool->cond, NULL) < 0){

    }

 

    pool->thread_num = thread_num;

    for(idx=0; idx<thread_num; ++idx)

    {

        ret = pthread_create(&pid, NULL, pthread_pool_main, pool);

        if (ret < 0){

        }

    }

    return pool;

}

 

void pthread_pool_addtask(thread_pool_t* pool, thread_execute_func handler, void* arg)

{

    task_t *task;

 

    task = (task_t*)malloc(sizeof(task_t));

    if (NULL == task){

    }

 

    task->arg = arg;

    task->handler = handler;

    task->next = NULL;

 

    if (pthread_mutex_lock(&pool->lock) < 0){

    }

 

    if (NULL == pool->task.tail){

        pool->task.tail = task;

    }

 

    else{

        pool->task.tail->next = task;

        pool->task.tail = task;

    }

 

    if (NULL == pool->task.head){

        pool->task.head = pool->task.tail;

    }

 

    pthread_cond_signal(&pool->cond);

    pthread_mutex_unlock(&pool->lock);

}

 

void pthread_pool_exit(void* lock)

{

    int *wait = (int*)lock;

    int *run;

 

    run = (int*)pthread_getspecific(g_key);

    *run = 0;

    pthread_setspecific(g_key, (void*)run);

 

    *wait = 0;

}

 

void pthread_pool_destroy(thread_pool_t* pool)

{

    int idx;

    task_t* p;

    volatile int lock;

 

    for(idx=0; idx<pool->thread_num; ++idx)

    {

        lock = 1;

        pthread_pool_addtask(pool, pthread_pool_exit, (void*)&lock);

 

        while(lock){

            usleep(100);

        }

    }

 

    while(pool->task.head)

    {

        p = pool->task.head->next;

        free(pool->task.head);

        pool->task.head = p;

    }

 

 

    pthread_mutex_unlock(&pool->lock);

    pthread_cond_destroy(&pool->cond);

    pthread_key_delete(pool->key);

    free(pool);

}

 

 

void test_fun(void* arg)

{

    printf("%d\n", (int)arg);

}

 

int main(int argc, char* argv[])

{

    int idx;

    thread_pool_t* pool;

 

    while(1)

    {

        pool = pthread_pool_create(100);

        if (NULL == pool)

        {

            printf("pthread_pool_create failed errno %d\n", errno);

            return -1;

        }

 

        for(idx=0; idx<150; ++idx)

        {

            pthread_pool_addtask(pool, test_fun, (void*)idx);

        }

 

        pthread_pool_destroy(pool);

        sleep(2);

    }

 

    sleep(2);

    return 0;

}

 

问题

         这个代码很简单,简单的生产者、消费者模式。但是它的线程池退出函数的有一点很让我困惑,它使用了优雅的退出方式,TDS可以保证动态伸缩。我也看过比较暴力的退出方式,在线程执行函数中循环检测线程池的stop标志位,然后销毁线程置位停止位,这种方式会把一个线程池的线程全部杀掉。

       volatile int lock;

它对lock这个变量使用了禁止编译器优化,当时我认为这种方式完全多此一举,为什么呢?从volatile的用法说起。

赋值

学过单片机、或者接触过一些底层硬件的都知道,连续给一个寄存器赋值是完全没有任何问题的,比如第一次是指令,第二次是数据

       Reg = 0xaa

       Reg = 0xff

但是这种用法换做gcc编译器它就会给你优化成

       Reg = 0xff

它认为你给一个地址连续赋值没有意义,后面一个值会把前面值覆盖掉。我们为了避免这种现象就会告知编译器不要优化它。

       Volatile Reg = 0xaa

       Volatile Reg = 0xff

 

取值

         CPU中ALU的速度因为过于快速,而内存总线的速度相对太慢,因此在ALU和内存之间加入高速缓存,将常用的数据、指令放在高速缓存,也就是Dcache、Icache。当然中间还有MMU、writeBack之类的,这儿不做说明。原理图如下:

         也就是说如果经常读取某个地址的值,就有可能被缓存在cache中,对cpu来说它可不管你这是内存地址、网卡地址、或者总线上的其它地址,它只认地址,不认硬件。下面两条指令就有可能陷入死循环,reg是一个网卡的一个status reg,此处在扫描等待数据过来。

       reg = 0

       while(!reg)

原因

         这些都是一些比较低级的平台。为什么我会认为在已经使用了操作系统的x86平台上面不会出现这种情况呢?如果一个硬件平台保证不了操作cache中的数据和内存中的不一致,那我们每次读取操作岂不是都加个volatile!!!再次回到上面的问题,在线程池销毁函数中

  1、销毁函数添加一个任务,将lock赋值1,然后将lock的地址传递到线程池中,然后它等待 lock变为0

  2、线程池中的某个线程竞争到了任务队列中的任务,然后执行,在执行线程退出函数中将 lock置位0

  3、销毁函数等到lock为0,然后继续销毁下一个线程

 

那么销毁函数可能遇到lock因为缓存到cache中然后导致死循环吗?当时XX俊说如果寄存器优化的就有可能,寄存器优化这个在这个不说明。

构造死循环

         为了构造死循环,创造多线程环境,刚开始会怀疑gcc会因为在两个不同的文件中才会出这个问题,那么新建两个文件 1.c 2.c 在搞个简单的makefile,使用O2优化。

 

#include <stdio.h>

#include <pthread.h>

#include <stdio.h>

#include <pthread.h>

 

int i = 1;

 

extern void* func2(void* arg);

 

void* func1(void* arg)

{

    while(i){

    //    usleep(100);

}

 

    pthread_exit(0);

}

 

int main(int argc, char* argv[])

{

    pthread_t th1;

    pthread_t th2;

 

    pthread_create(&th1, NULL, func1, NULL);

    sleep(1);

    pthread_create(&th2, NULL, func2, NULL);

 

    pthread_join(th1, NULL);

    pthread_join(th2, NULL);

 

    return 0;

}

 

extern int i;

void* func2(void* arg)

{

    i = 0;

    pthread_exit(0);

}

 

CC  = gcc

RM  = rm -rf

 

objects = 1.o 2.o

 

all : $(objects)

    $(CC) -o bin $^ -lpthread

   

%.o : %.c

    $(CC) -c -O2 $< -g

 

.PHONY: all clean

clean:

$(RM) *.o bin

 

 

     在XX哥的修改下出现了死循环,为什么会死循环呢?反汇编看看

 

Func1函数一进去就判断了i的值,然后就跳转到 804858b这个地址,这个地址处指令在干嘛?原地踏步!! 相当于 nop mov r1, r1

可以试一试如果在func1的函数中稍微加一点函数调用或者变量使用之类,函数就没法死循环,也就是编译器的优化条件苛刻啊。

 

结论

也就是说线程并发的这类用法可能导致死循环,但是条件苛刻,volatile使它更可靠。

 

线程池的讨论

1、  多线程单队列的瓶颈点

2、  多线程多对列提升点

3、  通用的线程池(线程池中的线程是否执行特定函数)会导致核间不均吗?

4、  线程池中的线程最好完善自身信息将pool挂在自己下面,作为线程的参数

5、  优雅退出、暴力退出

6、  动态伸缩

 

posted @ 2016-09-30 23:47  --大道至简--  阅读(1117)  评论(0编辑  收藏  举报