非常精简的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系统文件,嵌入式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。第二种是人们去发布认可…

关于使用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;我们要想拓展原来函数代码&…

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…

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

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

原来游戏技术行业最大的秘密竟然是...

欢迎大家前往腾讯云社区&#xff0c;获取更多腾讯海量技术实践干货哦~ 本文由腾讯游戏云发表于云社区专栏 本篇文章主要是分享游戏业务面临的安全风险场景&#xff0c;以及基于这些场景的特点&#xff0c;我们应该如何做好对应的防护。 【一、背景&#xff1a;游戏行业DDoS攻击…

指定Gradle构建属性

属性是用于轻松自定义Gradle构建和Gradle环境的宝贵工具。 我将在本文中演示一些用于指定Gradle构建中使用的属性的方法。 Gradle支持项目属性和系统属性 。 这篇文章中有趣的是两者之间的主要区别是如何访问它们。 通过常规Java / Groovy系统属性访问方法访问系统属性时&…

python数字转中文字符_Python实现中文数字转换为阿拉伯数字的方法示例

本文实例讲述了Python实现中文数字转换为阿拉伯数字的方法。分享给大家供大家参考&#xff0c;具体如下&#xff1a; 一、需求 今天写了三千二百行代码。 今天写了3200行代码。 两行意思相同&#xff0c;只是表达方式不太能够&#xff0c;统一掉。 二、原理 数字的特征是 数字 …

高级cmd攻击命令_一步一步学习DVWA渗透测试(Command Injection命令行注入)-第七次课...

各位小伙伴&#xff0c;今天我们继续学习Command Injection&#xff0c;翻译为中文就是命令行注入。是指通过提交恶意构造的参数破坏命令语句结构&#xff0c;从而达到执行恶意命令的目的。在OWASP TOP 10中一种存在注入漏洞&#xff0c;最常见的就是SQL和命令行注入。PHP开发的…

IDEA插件之 CodeGlance

在编辑代码最右侧&#xff0c;显示一块代码小地图 这款插件使用效果图如下&#xff0c;个人感觉还是有点用处&#xff0c;滚动条太小&#xff0c;有这个地图&#xff0c;拖动起来更加方便一点 原文地址:http://tengj.top/2017/02/22/idea1-1/转载于:https://www.cnblogs.com/al…

移动端图片上传方法

移动端图片上传方法 实现效果 文件下载 http://files.cnblogs.com/files/sntetwt/移动端图片上传.rar 实现步骤 一、隐藏<input type"file" id"file" name"Filedata" style"display:none;" accept"image/*" /> 二、…

c语言最大公约数和最小公倍数_五年级奥数课堂之七:公因数和公倍数

乘积尾0的个数公因数和公倍数的基本概念公因数的释义给定若干个整数&#xff0c;如果有一个(些)数是它们共同的因数&#xff0c;那么这个(些)数就叫做它们的公因数。而全部公因数中最大的那个&#xff0c;称为这些整数的最大公因数。公约数与公倍数相反&#xff0c;就是既是A的…

设计模式(五)--工厂模式汇总

LZ想把简单工厂模式、工厂方法模式和抽象工厂模式整理到一篇博文当中&#xff0c;由浅入深&#xff0c;应该能方便理解和记忆&#xff0c;话不多说&#xff0c;进入正题。 一、简单工厂模式 定义&#xff1a;从设计模式的类型上来说&#xff0c;简单工厂模式是属于创建型模式&a…

如何估算内存消耗?

这个故事可以追溯到至少十年之前&#xff0c;当时我第一次接触PHB时遇到一个问题&#xff1a;“在生产部署中&#xff0c;我们需要购买多大服务器”。 我们正在构建的新的&#xff0c;闪亮的系统距离生产开始还有9个月的时间&#xff0c;显然该公司已承诺提供包括硬件在内的整个…

python爬取b站403_Python如何爬取b站热门视频并导入Excel

代码如下 #encoding:utf-8 import requests from lxml import etree import xlwt import os # 爬取b站热门视频信息 def spider(): video_list [] url "https://www.bilibili.com/ranking?spm_id_from333.851.b_7072696d61727950616765546162.3" html requests.g…