一文带你搞清楚什么是线程池

3 阅读5分钟

内容简介

1.线程池介绍:

当客户端并发请求服务器的时候,服务器的响应是通过创建线程的方式来实现的。但是当并发的请求过于密集的时候,比如同时有十万个客户端请求服务,创建这些线程的开销是不可接受的:按照POSIX标准,一个线程占用8M的空间, 也就是说1G(1024M)的内存只能接受创建128个线程,假设一个服务器的内存是16G,同时也只能够接受创建2048个线程。这对于十万个客户端请求来说,简直是杯水车薪。所以我们需要----线程池。

2.线程池的好处:

  • 避免线程太多,使得内存耗尽
  • 避免创建线程与销毁线程多产生的代价
  • 任务与执行分离

3.线程池原理

客户端请求被加入任务队列(FIFO)等待,线程池通过锁和条件变量通信机制,让任务队列的任务有序进入执行队列,而执行队列创建线程来有序执行这些任务。那么问题来了,何谓有序?

  • 先来后到,管理组件负责推送任务
  • 一个线程执行一个任务
  • 当修改底层数据结构的的时候(访问共享资源),不能发生竞争

基于C的线程池实现

代码自取:Github - ThreadPool.c

支持层:双向链表构造任务队列和执行队列、定义线程池(管理组件)的数据结构

对于线程池的任务队列、执行队列两个组成部分,实现中都采用了简单的双向链表的底层数据结构。

//任务队列的结构体
 struct nTask {
  void (*task_func) (void *arg);
  void *user_data;

  struct nTask *prev;
  struct nTack *next;
};

在以上代码的第三行,我们在任务队列中声明一个函数指针,便于需要执行的时候跳转到合适的处理函数。

//执行队列的结构体
struct nWorker {
  pthread_t threadid;

  int terminate; //终止标识
  struct nManager *manager;
  
  struct nWorker *prev;
  struct nWorker *next;
};

以上代码中,terminate:终止标识设置为1的时候,意味着需要退出执行循环。

//管理组件的结构体
typedef struct nManager {
  struct nTask *tasks;
  struct nWorker *workers;

  pthread_mutex_t mutex;
  pthread_cond_t cond; //条件变量
} TreadPool;

比较有意思的是第6行所定义的互斥锁和第7行所定义的条件变量:这是线程池实现线程同步的关键

  • 全局一把互斥锁----访问共享资源的操作需要先取得锁
  • 全局唯一条件变量----作用是通知有任务在等待,需要唤醒一个线程
接口层:实现如下接口功能
  • 线程池回调:nThreadPoolCallBack
  • 线程池创建:nThreadPoolCreate
  • 线程池销毁:nThreadPoolDestory
  • 推送到任务队列:nThreadPoolPushTask

线程池创建函数:线程池创建一定数量的线程(thread)用于执行(worker), 这里的实现是线程池使用两个for循环分别创建执行队列和对应的线程(用threadid来唯一标识)。在对执行队列进行任何修改之前,都需要上锁防止竞争。

int nThreadPoolCreate(ThreadPool *pool,int numWorkers) {
  if (pool == NULL) return -1;
  if (numWorkers < 1) numWorkers = 1;
  memset(pool, 0, sizeof(ThreadPool));

  pthread_cond_t blank_cond = PTHREAD_COND_INITIALIZER;
  memcpy(&pool->cond, &blank_cond, sizeof(pthread_cond_t));

  pthread_mutex_init(&pool->mutex, NULL);

  int i = 0;
  for (i = 0;i < numWorkers;i++) {
    struct nWorker *worker =(struct nWorker*)malloc(sizeof(struct nWorker));
    if (worker == NULL) {
      perror("malloc");
      return -2;
    }
    memset(worker, 0, sizeof(struct nWorker));
    worker->manager = pool;

    int ret = pthread_create(&worker->threadid, NULL, nThreadPoolCallBack, worker);
    if (ret) {
      perror("pthread_create");
      free(worker);
      return -3;
    }
    LIST_INSERT(worker, pool->workers);
  }
  printf("call_back\n");
  return 0;
}

推送到任务队列:一个任务被创建的时候,应该有一个接口函数用于将此任务放到任务队列当中,同样,在使用宏LIST_INSERT修改任务队列的时候需要先加锁保护,防止竞争。重点是,第六行使用了pthread_cond_signal这个函数用于唤醒一个线程,这是实现同步的关键机制。

int nThreadPoolPushTask(ThreadPool *pool, struct nTask *tasks) {
  pthread_mutex_lock(&pool->mutex);

  LIST_INSERT(tasks, pool->tasks);

  pthread_cond_signal(&pool->cond);//唤醒一个等待这个条件的线程
  
  pthread_mutex_unlock(&pool->mutex);

  return 0;
}

回调函数:这是线程池中的消费者,是一个while(1)循环。由于涉及LIST_REOMVE删除任务队列中队列的操作,所以需要先获取锁,但是这时我们并不清楚线程目前是否可以被执行,所以在第9行调用了pthread_cond_wait来达成:

  • 释放锁,为了不在等待的时候一直占用唯一的互斥锁
  • 等待此cond条件变量的线程一旦唤醒,重新获取锁。

将执行的任务从任务队列中删去,while(1)循环在terminate被置位的时候break退出,调用task_func实际执行。

//线程回调函数
static void *nThreadPoolCallBack(void *arg) {
  struct nWorker *worker = (struct nWorker*)arg;

  while (1) {
    pthread_mutex_lock(&worker->manager->mutex);
    while (worker->manager->tasks == NULL) {
      if (worker->terminate) break;
      pthread_cond_wait(&worker->manager->cond, &worker->manager->mutex);
    }
    //避免死锁
    if (worker->terminate) {
      pthread_mutex_unlock(&worker->manager->mutex);
      break;
    }

    struct nTask *task = worker->manager->tasks;
    LIST_REMOVE(task, worker->manager->tasks);

    pthread_mutex_unlock(&worker->manager->mutex);

    task->task_func(task);
  }

  free(worker);

  return NULL;
}
业务层:其实所谓的业务就是创建很多线程,测试线程池是否正常运作
void task_entry(struct nTask *task) {
  //struct nTask *task = (struct nTask*)task;
  int idx = *(int *)task->user_data;

  printf("idx:%d\n", idx);

  free(task->user_data);
  free(task);
}

int main(void) {
  ThreadPool pool;

  nThreadPoolCreate(&pool,THREADPOOL_INIT_COUNT);
  printf("nThreadPoolCreate -- finish\n");

  int i = 0;
  for (i = 0;i < TASK_INIT_SIZE;i++) {
    struct nTask *task = (struct nTask*)malloc(sizeof(struct nTask));
    if (task == NULL) {
      perror("malloc");
      exit(1);
    }
    memset(task, 0, sizeof(struct nTask));

    task->task_func = task_entry;
    task->user_data = malloc(sizeof(int));

    *(int *)task->user_data = i;

    nThreadPoolPushTask(&pool, task);
  }

  getchar();
}

值得注意的是memset函数的使用是将一块内存区域的内容复制到另一内存当中,但是在这里由于第二个参数为0,所以起到的实际作用是将task所指向结构体的内容全部置为所对应的0值,起到实际上的初始化效果。