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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. //thread_pool.c
  2. #include "thread_pool.h"
  3. #include "queue.h"
  4. #include <stdlib.h>
  5. #include <pthread.h>
  6. struct thread_pool {
  7. unsigned int thread_count;
  8. pthread_t *threads;
  9. queue_t tasks;
  10. pthread_mutex_t lock;
  11. pthread_cond_t task_ready;
  12. };
  13. struct task {
  14. void* (*routine)(void *arg);
  15. void *arg;
  16. };
  17. static void cleanup(pthread_mutex_t* lock) {
  18. pthread_mutex_unlock(lock);
  19. }
  20. static void * worker(thread_pool_t pool) {
  21. struct task *t;
  22. while(1) {
  23. pthread_mutex_lock(&pool->lock);
  24. pthread_cleanup_push((void(*)(void*))cleanup, &pool->lock);
  25. while(queue_isempty(pool->tasks)) {
  26. pthread_cond_wait(&pool->task_ready, &pool->lock);
  27. /*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.*/
  28. }
  29. t=(struct task*)queue_dequeue(pool->tasks);
  30. pthread_cleanup_pop(0);
  31. pthread_mutex_unlock(&pool->lock);
  32. t->routine(t->arg);/*todo: report returned value*/
  33. free(t);
  34. }
  35. return NULL;
  36. }
  37. thread_pool_t thread_pool_create(unsigned int thread_count) {
  38. unsigned int i;
  39. thread_pool_t pool=NULL;
  40. pool=(thread_pool_t)malloc(sizeof(struct thread_pool));
  41. pool->thread_count=thread_count;
  42. pool->threads=(pthread_t*)malloc(sizeof(pthread_t)*thread_count);
  43. pool->tasks=queue_create();
  44. pthread_mutex_init(&pool->lock, NULL);
  45. pthread_cond_init(&pool->task_ready, NULL);
  46. for(i=0; i<thread_count; i++) {
  47. pthread_create(pool->threads+i, NULL, (void*(*)(void*))worker, pool);
  48. }
  49. return pool;
  50. }
  51. void thread_pool_add_task(thread_pool_t pool, void* (*routine)(void *arg), void *arg) {
  52. struct task *t;
  53. pthread_mutex_lock(&pool->lock);
  54. t=(struct task*)queue_enqueue(pool->tasks, sizeof(struct task));
  55. t->routine=routine;
  56. t->arg=arg;
  57. pthread_cond_signal(&pool->task_ready);
  58. pthread_mutex_unlock(&pool->lock);
  59. }
  60. void thread_pool_destroy(thread_pool_t pool) {
  61. unsigned int i;
  62. for(i=0; i<pool->thread_count; i++) {
  63. pthread_cancel(pool->threads[i]);
  64. }
  65. for(i=0; i<pool->thread_count; i++) {
  66. pthread_join(pool->threads[i], NULL);
  67. }
  68. pthread_mutex_destroy(&pool->lock);
  69. pthread_cond_destroy(&pool->task_ready);
  70. queue_destroy(pool->tasks);
  71. free(pool->threads);
  72. free(pool);
  73. }

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

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

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

转载于:https://www.cnblogs.com/wanghuaijun/p/9508186.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/366036.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux vim 执行shell命令行,Linux中vim和shell

在Linux系统中一切皆文件&#xff0c;配置服务其实就是在修改其配置文件的参数&#xff0c;而在日常文件中肯定少不了的就是编辑文档&#xff0c;这就离不开vim&#xff0c;而vim之所以能够得到广大厂商的青睐与用户的认可&#xff0c;原因在于vim编辑器中有三种模式&#xff1…

JS之setTimeOut与clearTimeOut

小练习1&#xff1a;针对HTML&#xff0c;分别使用 setTimeout 和 setInterval 实现以下功能&#xff1a; 点击按钮时&#xff0c;开始改变 fade-obj 的透明度&#xff0c;开始一个淡出&#xff08;逐渐消失&#xff09;动画&#xff0c;直到透明度为0在动画过程中&#xff0c…

运行时的Java 8参数名称

Java 8将引入一种更容易的方法来发现方法和构造函数的参数名称。 在Java 8之前&#xff0c;找到参数名称的方法是在编译阶段打开调试符号&#xff0c;这会在生成的类文件中添加有关参数名称的元信息&#xff0c;然后提取复杂的信息&#xff0c;需要处理字节码。获取参数名称。…

python 可执行文件_如何通过Python函数运行的可执行文件的终端...

我想抑制运行可执行文件的函数产生的所有终端输出. 我试图通过使用每次调用函数时临时重新定义stdout和stderr的上下文管理器来抑制Python函数的输出.这会抑制函数中的print调用产生的终端输出,但是当函数调用产生终端输出的可执行文件时,它似乎不起作用. 那么,如何抑制Python函…

嵌入式linux系统文件,嵌入式Linux文件系统知多少

Nand/Nor Flash在嵌入式Linux产品中&#xff0c;通常使用的存储介质为Nand Flash和Nor Flash&#xff0c;而手机、相机等产品通常使用eMMC、SD Card作为存储介质&#xff0c;导致这种差异的原因主要是成本考量。Nand Flash和Nor Flash具有低成本、高密度存储的优势。但是&#…

三分钟上手Highcharts简易甘特图

根据业务需求&#xff0c;找到了这个很少使用的图形&#xff0c;话不多说&#xff0c;看看该如何使用。首先要引入支持文件&#xff1a;可根据链接下载。 exporting.js&#xff1a;https://img.hcharts.cn/highcharts/modules/exporting.js xrange.js&#xff1a;https://img.h…

WEB语义化

WEB语义化让机器读懂内容&#xff0c;HTML就带有一定「语义」的标签&#xff0c;比如段落&#xff0c;标题&#xff0c;表格和图片等。让机器读懂内容&#xff0c;那么两种方案&#xff1a;第一种让机器变得更人工智能化&#xff0c;也就是现在大火的AI。第二种是人们去发布认可…

Jetty 9.1上的Java WebSockets(JSR-356)

最终发布了Jetty 9.1 &#xff0c;将Java WebSockets&#xff08;JSR-356&#xff09;引入了非EE环境。 这真是个好消息&#xff0c;今天的帖子将介绍如何将这个出色的新API与Spring Framework一起使用。 JSR-356定义了基于注释的简洁模型&#xff0c;以允许现代Java Web应用程…

如何用python画组合图形_python结合G2绘制精美图形

$.getJSON(top10.json, function (data) { var Frame G2.Frame; var frame new Frame(data); var chart new G2.Chart({ id: c1, width: 500, height: 400 }); chart.source(frame, { pct: {alias: 年化相对收益率(%)}, }); // 去除 X 轴标题 chart.axis(name, { title: nul…

edup无线网卡驱动安装linux,UBUNTU_15.0.4 usb无线网卡驱动安装方法

前言&#xff1a;为了摆脱网线的束缚&#xff0c;我买了个无线网卡&#xff1b; widnows 上好用&#xff0c;易安装。linux 上&#xff0c;按照自带教程上去做&#xff0c;没有成功。后来在搜索了多篇 解决问题的文章。 再加上自己的方法&#xff0c;终于完成了。貌似信号还可以…

LeetCode Golang 9.回文数

9. 回文数 第一种办法 &#xff1a;itoa 转换为字符串进行处理&#xff1a; package mainimport ("strconv""fmt" )//判断一个整数是否是回文数。回文数是指正序&#xff08;从左向右&#xff09;和倒序&#xff08;从右向左&#xff09;读都是一样的整数。…

关于使用JQ scrollTop方法进行滚动定位

没图我说个锤子&#xff0c;先来个自拍镇楼。 又到了每周周五总结时间。我广州刘德华又来讲故事了。这一周没啥任务&#xff0c;就一个任务&#xff0c;产品口头交代了两句&#xff0c;也没有psd没有设计图没有样式。自由发挥&#xff0c;你自己敲代码做个作品出来。 what&…

ssh密钥登录

方法一:使用下例中ssky-keygen和ssh-copy-id&#xff0c;仅需通过3个步骤的简单设置而无需输入密码就能登录远程Linux主机。 ssh-keygen 创建公钥和密钥。 ssh-copy-id 把本地主机的公钥复制到远程主机的authorized_keys文件上。ssh-copy-id 也会给远程主机的用户主目录&#x…

Spring REST:异常处理卷。 2

这是有关使用Spring进行REST异常处理的系列的第二篇文章。 在我以前的文章中&#xff0c;我描述了如何在REST服务中组织最简单的异常处理。 这次&#xff0c;我将更进一步&#xff0c;我将向您展示何时最好在ControllerAdvice级别上使用异常处理 。 介绍 在开始本文的技术部分…

python 装饰器有哪些_python装饰器有什么用

简言之&#xff0c;python装饰器就是用于拓展原来函数功能的一种函数&#xff0c;这个函数的特殊之处在于它的返回值也是一个函数&#xff0c;使用python装饰器的好处就是在不用更改原函数的代码前提下给函数增加新的功能。 一般而言&#xff0c;我们要想拓展原来函数代码&…

jQuery数据转换与提交

json2.js序列化,即JSON对象转换成String字符串&#xff1a; JSON.stringify({ id: 1, name: jsons });反序列化,即String转JSON对象&#xff1a; JSON.parse("{ id: 1, name: jsons }");jquery.json2xml.jsJSON对象转换为XML字符串&#xff1a; $.json2xml({ id: 1, …

linux中查看相关日志记录,linux重启查看日志及历史记录 查询原因

linux系统文件通常在/var/log中下面是对下面常出现的文件进行解释/var/log/message ---------------------------------------系统启动后的信息和错误日志/var/log/secure ------------------------------------------与安全相关的日志信息/var/log/maillog ------------------…

04,认证、权限、频率

认证组件 Django原生的authentic组件为我们的用户注册与登录提供了认证功能&#xff0c;十分的简介与强大。同样DRF也为我们提供了认证组件&#xff0c;一起来看看DRF里面的认证组件是怎么为我们工作的&#xff01;models.py# 定义一个用户表和一个保存用户Token的表 class Use…

sun.misc.Unsafe和堆外内存

sun.misc.Unsafe类允许您执行许多Java中不应该做的事情&#xff0c;但是在非常特殊的情况下仍然有用。 必须在99&#xff05;的时间避免这种情况&#xff0c;但是在极少数情况下&#xff0c;这是唯一有意义的解决方案。 这篇文章考虑了它在OpenHFT中的使用方式以及我希望在Jav…

jq获取input选取的文件名_tushare获取交易数据并可视化分析

获取数据是金融量化分析的第一步&#xff0c;找不到可靠、准确的数据&#xff0c;量化分析就无从谈起。随着信息技术的不断发展&#xff0c;数据获取渠道也越来越多&#xff0c;尤其是Python网络爬虫&#xff0c;近几年愈来愈火。然而&#xff0c;很多人毕竟精力有限&#xff0…