线程池的含义跟它的名字一样,就是一个由许多线程组成的池子。

有了线程池,在程序中使用多线程变得简单。我们不用再自己去操心线程的创建、撤销、管理问题,有什么要消耗大量CPU时间的任务通通直接扔到线程池里就好了,然后我们的主程序(主线程)可以继续干自己的事去,线程池里面的线程会自动去执行这些任务。

另一方面,线程池提升了多线程程序的性能。我们不需要在大量任务需要执行时现创建大量线程,然后在任务结束时又销毁大量线程,因为线程池里面的线程都是现成的而且能够重复使用。一个理想的线程池能够合理地动态调节池内线程数量,既不会因为线程过少而导致大量任务堆积,也不会因为线程过多了而增加额外的系统开销。

线程池看上去很神奇的样子,那它是怎么实现的呢?线程这么虚渺在的东西也能像有形的物品一样圈在一个池子里?在只知道线程池这个名字的时候,我心里的疑惑就是这样的。

其实线程池的原理非常简单,它就是一个非常典型的生产者消费者同步问题。如果不知道我说的这个XXX问题也不要紧,我下面就解释。

根据刚才描述的线程池的功能,可以看出线程池至少有两个主要动作,一个是主程序不定时地向线程池添加任务,另一个是线程池里的线程领取任务去执行。且不论任务和执行任务是个什么概念,但是一个任务肯定只能分配给一个线程执行。

这样就可以简单猜想线程池的一种可能的架构了:主程序执行入队操作,把任务添加到一个队列里面;池子里的多个工作线程共同对这个队列试图执行出队操作,这里要保证同一时刻只有一个线程出队成功,抢夺到这个任务,其他线程继续共同试图出队抢夺下一个任务。所以在实现线程池之前,我们需要一个队列,我为这个线程池配备的队列单独放到了另一篇博客一个通用纯C队列的实现中。

这里的生产者就是主程序,生产任务(增加任务),消费者就是工作线程,消费任务(执行、减少任务)。因为这里涉及到多个线程同时访问一个队列的问题,所以我们需要互斥锁来保护队列,同时还需要条件变量来处理主线程通知任务到达、工作线程抢夺任务的问题。如果不熟悉条件变量,我在另一篇博客Linux C语言多线程库Pthread中条件变量的的正确用法逐步详解中作了详细说明。

准备工作都差不多了,可以开始设计线程池了。一个最简单线程池应该有什么功能呢?对于使用者来说,除了创建和销毁线程池,最简单的情况下只需要一个功能——添加任务。对于线程池自己来说,最简单的情况下不需要动态调节线程数量,不需要考虑线程同步、线程死锁等等一大堆麻烦的问题。所以最后的线程池API定义为:

  1. //thread_pool.h
  2.  
  3. #ifndef THREAD_POOL_H_INCLUDED
  4. #define THREAD_POOL_H_INCLUDED
  5.  
  6. typedef struct thread_pool *thread_pool_t;
  7.  
  8. thread_pool_t thread_pool_create(unsigned int thread_count);
  9.  
  10. void thread_pool_add_task(thread_pool_t pool, void* (*routine)(void *arg), void *arg);
  11.  
  12. void thread_pool_destroy(thread_pool_t pool);
  13.  
  14. #endif //THREAD_POOL_H_INCLUDED

创建线程池时指定线程池中应该固定包含多少工作线程,添加任务就是向线程池添加一个任务函数指针和任务函数需要的参数——这跟Pthread线程库中的普通线程创建函数pthread_create是一样的。根据这套线程池API,我们使用线程池的应用程序应该是这个套路:

  1. //test.c
  2.  
  3. #include "thread_pool.h"
  4. #include <stdio.h>
  5. #include <unistd.h>
  6. #include <pthread.h>
  7.  
  8. void* test(void *arg) {
  9. int i;
  10. for(i=0; i<5; i++) {
  11. printf("tid:%ld task:%ld\n", pthread_self(), (long)arg);
  12. fflush(stdout);
  13. sleep(2);
  14. }
  15. return NULL;
  16. }
  17.  
  18. int main() {
  19. long i=0;
  20. thread_pool_t pool;
  21.  
  22. pool=thread_pool_create(2);
  23.  
  24. for(i=0; i<5; i++) {
  25. thread_pool_add_task(pool, test, (void*)i);
  26. }
  27.  
  28. puts("press enter to terminate ...");
  29. getchar();
  30.  
  31. thread_pool_destroy(pool);
  32. return 0;
  33. }

上面这个测试程序向线程池添加了5个相同的任务,每个任务耗时10秒,但是线程池中只有2个工作线程,所以程序的运行结果是两个工作线程轮流把5个任务挨个做完。显示到屏幕上就是:前10秒两个工作线程轮流输出自己的线程ID和当前任务的任务号0和1,各输出5次;第二个10秒两个工作线程轮流输出自己的线程ID和当前任务的任务号2和3……

在这期间,主程序输出“press enter to terminate ...”并等待用户输入,任何时候都可以按回车让主程序继续往下,这样会强制终止所有工作线程并销毁线程池,最后程序退出。test程序运行效果截图如下:

最后就是线程池真正的实现了:

  1. //thread_pool.c
  2.  
  3. #include "thread_pool.h"
  4. #include "queue.h"
  5. #include <stdlib.h>
  6. #include <pthread.h>
  7.  
  8. struct thread_pool {
  9. unsigned int thread_count;
  10. pthread_t *threads;
  11. queue_t tasks;
  12. pthread_mutex_t lock;
  13. pthread_cond_t task_ready;
  14. };
  15.  
  16. struct task {
  17. void* (*routine)(void *arg);
  18. void *arg;
  19. };
  20.  
  21. static void cleanup(pthread_mutex_t* lock) {
  22. pthread_mutex_unlock(lock);
  23. }
  24.  
  25. static void * worker(thread_pool_t pool) {
  26. struct task *t;
  27. while(1) {
  28. pthread_mutex_lock(&pool->lock);
  29. pthread_cleanup_push((void(*)(void*))cleanup, &pool->lock);
  30. while(queue_isempty(pool->tasks)) {
  31. pthread_cond_wait(&pool->task_ready, &pool->lock);
  32. /*A condition wait (whether timed or not) is a cancellation point ... a side-effect of acting upon a cancellation request while in a condition wait is that the mutex is (in effect) re-acquired before calling the first cancellation cleanup handler.*/
  33. }
  34. t=(struct task*)queue_dequeue(pool->tasks);
  35. pthread_cleanup_pop(0);
  36. pthread_mutex_unlock(&pool->lock);
  37. t->routine(t->arg);/*todo: report returned value*/
  38. free(t);
  39. }
  40. return NULL;
  41. }
  42.  
  43. thread_pool_t thread_pool_create(unsigned int thread_count) {
  44. unsigned int i;
  45. thread_pool_t pool=NULL;
  46. pool=(thread_pool_t)malloc(sizeof(struct thread_pool));
  47. pool->thread_count=thread_count;
  48. pool->threads=(pthread_t*)malloc(sizeof(pthread_t)*thread_count);
  49.  
  50. pool->tasks=queue_create();
  51.  
  52. pthread_mutex_init(&pool->lock, NULL);
  53. pthread_cond_init(&pool->task_ready, NULL);
  54.  
  55. for(i=0; i<thread_count; i++) {
  56. pthread_create(pool->threads+i, NULL, (void*(*)(void*))worker, pool);
  57. }
  58. return pool;
  59. }
  60.  
  61. void thread_pool_add_task(thread_pool_t pool, void* (*routine)(void *arg), void *arg) {
  62. struct task *t;
  63. pthread_mutex_lock(&pool->lock);
  64. t=(struct task*)queue_enqueue(pool->tasks, sizeof(struct task));
  65. t->routine=routine;
  66. t->arg=arg;
  67. pthread_cond_signal(&pool->task_ready);
  68. pthread_mutex_unlock(&pool->lock);
  69. }
  70.  
  71. void thread_pool_destroy(thread_pool_t pool) {
  72. unsigned int i;
  73. for(i=0; i<pool->thread_count; i++) {
  74. pthread_cancel(pool->threads[i]);
  75. }
  76. for(i=0; i<pool->thread_count; i++) {
  77. pthread_join(pool->threads[i], NULL);
  78. }
  79. pthread_mutex_destroy(&pool->lock);
  80. pthread_cond_destroy(&pool->task_ready);
  81. queue_destroy(pool->tasks);
  82. free(pool->threads);
  83. free(pool);
  84. }

上面的worker函数就是工作线程函数,所有的工作线程都在执行着这个函数。它首先在互斥锁和条件变量的保护下从任务队列中取出一个任务,这个任务实际上是一个函数指针和调用函数所需的参数,所以执行任务就很简单了——用任务参数调用任务函数。函数返回以后,工作线程继续去抢任务。

这里没有处理任务函数的返回值问题,理论上任务函数返回以后线程池应该用某种机制通知主程序,然后主程序获取通过某种手段获取返回值,但这明显不是一个最简单的线程池需要操心的事。实际上,应用程序可以通过全局变量或传入的参数指针,加上额外的线程同步代码解决返回值的通知和获取问题。
还有一点需要注意,最后线程池销毁时会强制终止所有处于撤销点(cacellation
points)的工作线程,如果工作线程正在任务函数中没返回而且任务函数中有非手动创建的撤销点,那么任务函数就会在跑到撤销点时戛然而止,这可能导致意外结果。而如果任务函数中没有任何线程撤销点,那么线程池销毁函数会一直阻塞等待直到任务函数完成后才能终止对应的工作线程并返回。

要正确处理这个问题,线程池使用者必须通过自己的线程同步代码保证调用thread_pool_destroy之前所有任务都已经完成、终止或者取消。

非常精简的Linux线程池实现(一)——使用互斥锁和条件变量的更多相关文章

  1. linux 线程的同步 二 (互斥锁和条件变量)

    互斥锁和条件变量 为了允许在线程或进程之间共享数据,同步时必须的,互斥锁和条件变量是同步的基本组成部分. 1.互斥锁 互斥锁是用来保护临界区资源,实际上保护的是临界区中被操纵的数据,互斥锁通常用于保护 ...

  2. 进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)

    注:本分类下文章大多整理自<深入分析linux内核源代码>一书,另有参考其他一些资料如<linux内核完全剖析>.<linux c 编程一站式学习>等,只是为了更好 ...

  3. node源码详解(七) —— 文件异步io、线程池【互斥锁、条件变量、管道、事件对象】

    本作品采用知识共享署名 4.0 国际许可协议进行许可.转载保留声明头部与原文链接https://luzeshu.com/blog/nodesource7 本博客同步在https://cnodejs.o ...

  4. linux c 线程间同步(通信)的几种方法--互斥锁,条件变量,信号量,读写锁

    Linux下提供了多种方式来处理线程同步,最常用的是互斥锁.条件变量.信号量和读写锁. 下面是思维导图:  一.互斥锁(mutex)  锁机制是同一时刻只允许一个线程执行一个关键部分的代码. 1 . ...

  5. [转]一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程

    一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程 希望此文能给初学多线程编程的朋友带来帮助,也希望牛人多多指出错误. 另外感谢以下链接的作者给予,给我的学习带来了很大帮助 http ...

  6. Linux互斥锁、条件变量和信号量

    Linux互斥锁.条件变量和信号量  来自http://kongweile.iteye.com/blog/1155490 http://www.cnblogs.com/qingxia/archive/ ...

  7. linux 互斥锁和条件变量

    为什么有条件变量? 请参看一个线程等待某种事件发生 注意:本文是linux c版本的条件变量和互斥锁(mutex),不是C++的. mutex : mutual exclusion(相互排斥) 1,互 ...

  8. 线程私有数据TSD——一键多值技术,线程同步中的互斥锁和条件变量

    一:线程私有数据: 线程是轻量级进程,进程在fork()之后,子进程不继承父进程的锁和警告,别的基本上都会继承,而vfork()与fork()不同的地方在于vfork()之后的进程会共享父进程的地址空 ...

  9. 【Linux C 多线程编程】互斥锁与条件变量

    一.互斥锁 互斥量从本质上说就是一把锁, 提供对共享资源的保护访问. 1) 初始化: 在Linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化: 对于静态 ...

随机推荐

  1. 服务器IO瓶颈对MySQL性能的影响

    [背景] 之前我们碰到一些MySQL的性能问题,比如服务器日志备份时可能会导致慢查询增多,一句简单的select或insert语句可能执行几秒,IO负载较高的服务器更容易出现并发线程数升高,CPU上升 ...

  2. InvokeRepeating重复定时器

    JS // Starting in 2 seconds.// a projectile will be launched every 0.3 secondsvar projectile : Rigid ...

  3. 添加第一个控制器(Controllers)

    在MVC体系架构中,输入请求是由控制器Controller来处理的(负责处理浏览器请求,并作出响应).在ASP.NET MVC中Controller本身是一个类(Class)通常继承于System.W ...

  4. Linux学习笔记03—初识Linux

    命令介绍 忘记root密码的处理方法 系统安装盘的救援模式的使用 一.命令介绍 1.LS命令 ls 查看当前目录下的文件 Ls –l 等同于ll 查看目录的详细信息 Ls –a 查看当前目录下的所有文 ...

  5. VS2015 打包winform 安装程序

    最近开发了一个小软件.由于需要打包.网上找了一些资料.然后整合了起来.希望对大家有所帮助.不全面请见谅. 打包控件 InstallShield-Limited-Edition  下面是注册地址 htt ...

  6. [廖雪峰] Git 分支管理(2):Bug 分支

    软件开发中,bug 就像家常便饭一样.有了 bug 就需要修复,在 Git 中,由于分支是如此的强大,所以,每个 bug 都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除. 当你 ...

  7. 使用JavaScript的数组实现数据结构中的队列与堆栈

    今天在项目中要使用JavaScript实现数据结构中的队列和堆栈,这里做一下总结. 一.队列和堆栈的简单介绍 1.1.队列的基本概念 队列:是一种支持先进先出(FIFO)的集合,即先被插入的数据,先被 ...

  8. C++ 实践总结

     对于一个应用程序而言,静态链接库可能被载入多次,而动态链接库仅仅会被载入一次. Gameloft面试之错误一 Event: 面试官说例如以下程序是能够链接通过的. class Base { Pu ...

  9. Android 数据存储04之Content Provider

    Content Provider 版本 修改内容 日期 修改人 V1.0 原始版本 2013/2/25 skywang 1 URI 通用资源标志符(Universal Resource Identif ...

  10. 连接查询简析 join 、 left join 、 right join

    join :取两个表的合集: left join:左表的数据全部保留,然后增加右表与左表条件匹配的记录.如下 select cc.* from cloud_groups as cg left join ...