线程池中遇到的小问题
一个小问题
前言
服务器的负载在各个时间段是无法预测的,比如某个时间点大量用户访问,虽然现在高并发的服务器都趋向用户处理无阻塞的设计,但类似于磁盘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、 动态伸缩