从0到1实现线程池(C语言版)


目录
🌤️1. 基础知识
⛅1.1 线程概述
⛅1.2 linux下线程相关函数
🌥️1.2.1 线程ID
🌥️1.2.2 线程创建
🌥️1.2.3 线程回收
🌥️1.2.4 线程分离
🌤️2. 线程池概述
⛅2.1 线程池的定义
⛅2.2 为什么要有线程池(线程池的作用)
⛅2.3 线程池的构成
⛅2.4 线程数量的确定
🌤️3、实现线程池
⛅3.1 接口设计
⛅3.2 结构体设计
⛅3.3 关键函数书写
🌥️3.3.1 __threads_create & __threads_terminate
🌥️3.3.2 __taskqueue_create
🌥️3.3.3 __add_task & __pop_task
🌤️1. 基础知识
⛅1.1 线程概述
要实现线程池,首先要了解什么是线程?要说线程就不得不提进程,以 Windows 下 QQ 为例,当我们双击打开 QQ,便打开了一个 QQ 进程,进程可以简单的认为是程序的一次执行过程,是操作系统分配基本运行资源的基本单位,可以通过任务管理器查看每一个进程的资源(如 CPU、内存、磁盘、网络)使用情况。在 QQ 中,我们可以同时打字聊天、语音通话、下载文件等,在同一个进程 QQ 中,这些同时进行的任务就是由不同的线程来执行的。线程是进程内的一个执行单元,它与其他线程共享该进程的资源,每个线程都拥有自己的执行序列、程序计数器、寄存器集和堆栈,线程是操作系统调度执行的最小单位。在 Linux 中,其实进程和线程不做区分,关于这些本质细节后面会出专门的专栏进行讲解,现在只需要知道线程就是来执行某个特定任务的。
⛅1.2 linux下线程相关函数
为了实现线程池,还需要掌握一些常见的线程函数。
🌥️1.2.1 线程ID
每一个线程都有一个唯一的线程ID,类型为pthread_t,一个无符号长整形数,可以调用如下函数获得:
pthread_t pthread_self(void); // 返回当前线程的线程ID
🌥️1.2.2 线程创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
// thread:线程ID;attr: 线程属性, 一般写NULL;start_routine:函数指针;arg:函数实参
// 返回值:成功返回0
给大家举个例子就明白怎么创建一个线程了。

可以这样来理解线程创建函数:tid作为标记值管理线程,就像用fd来管理文件描述符一样,学号来管理学生一样,作为唯一不可重复的标识,线程是用来处理任务的,所以要传一个函数指针,这个任务函数可能有参数,所以还要传入参数。
🌥️1.2.3 线程回收
#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞,直到子线程退出, 函数解除阻塞, 回收对应的子线程资源
int pthread_join(pthread_t thread, void **retval);
// thread:线程ID;retval:一般写NULL
// 返回值:成功返回0
有线程创建就必有线程回收,这个函数调用之后,主线程(一般为main函数)要阻塞等待所有的子线程完成,所以效率较低。
🌥️1.2.4 线程分离
在很多情况下,主线程有属于自己的业务处理流程,调用pthread_join()只要子线程不退出主线程就会一直被阻塞。为了解决这种效率低的问题,线程库函数为我们提供了线程分离函数pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,占用的内核资源就被系统的其他进程接管并回收了。
#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);
🌤️2. 线程池概述
在正式实现线程池之前,先来了解一下线程池的基础概念,线程池是一个框架中的基础轮子,在几年前有些同学会把线程池作为简历中的一个项目,其实线程池不能是一个项目,因为并没有实现任何业务功能,线程池是用来优化一个框架或者项目的。

⛅2.1 线程池的定义
线程池就是维持管理固定数量线程的池式结构,除了线程池之外,还有大家比较熟悉的内存池、数据库连接池等池式结构,他们都有一个共同点,就是复用资源,为什么要复用,因为这些资源的创建和销毁都是比较耗时且繁琐的;什么叫维持?就是反复的去利用线程池里的线程,没有任务就休眠(就绪态),有任务就去调度执行。
⛅2.2 为什么要有线程池(线程池的作用)
当主线程在执行的过程中,遇到了耗时任务,会严重耽误主线程的运行效率的时候,主线程将这些任务发布到线程池,让线程池来完成,主线程继续执行核心业务逻辑,下面来举个生活中的例子便于大家理解。
想象一下你是一家餐厅的经理(主线程)。餐厅面对的顾客(任务)是多种多样的,包括点餐、上菜、收钱等等。如果餐厅只有你一个人来做所有的事情,显然效率会非常低下,特别是在顾客高峰期,你可能会不堪重负。这时,你可以雇佣几位员工(线程池中的线程)来帮忙。你可以将任务分配给这些员工:比如一个专门接待顾客点餐,一个负责上菜,另一个负责收银。这样,每个员工都有明确的责任,可以同时处理不同的任务,大大提高了整体的运作效率。
线程池的工作方式也类似。当有多个任务需要处理时,线程池可以提供多个线程来同时执行任务,主线程则可以继续执行其他的核心业务逻辑,而不必等待每一个任务完成,从而提高了程序的性能和响应速度。这样的机制减少了线程创建和销毁的开销,并可以有效地管理线程的生命周期。
⛅2.3 线程池的构成
有了上面的讲述,我们知道线程池需要有生产者线程在发布耗时任务,有消费者线程(线程池)来负责执行任务,然后就来思考一下生产者线程和消费者线程怎么进行交互?想一下身为经理的你怎么和员工交互呢?可以使用手机、口头表述等形式,在计算机中就需要一种数据结构,并且在线程池中该数据结构还对应着一种多进程环境,这里我们采用任务队列,或者有朋友会想,可不可以用其他数据结构?几乎在所有线程池开源代码中,队列是唯一的选择,也可以用和队列类似的结构如数组。
队列是一种先进先出的顺序数据结构,生产者线程push任务,消费者线程pop任务,并且队列加锁十分方便,不会占用太长时间,同时链表插入有序。
这里再来补充一个线程三状态转移图:

⛅2.4 线程数量的确定
首先,为什么要维护固定数量的线程呢?主要是为了避免线程频繁的创建和销毁。对于不同的程序来说,所适合维持的线程数量一般是固定的,主要由以下经验公式确认,对于 CPU 密集型程序,一般设置为 CPU 核心数较为合适,对于 IO 密集型程序,一般设置为两倍的 CPU 核心数较为合适。在实际的测试过程中,是在上述线程数量附近寻找最合适的数量。
🌤️3、实现线程池
⛅3.1 接口设计
在正式写代码之前,首先要确定要给用户暴露哪些接口,一般将接口函数放在头文件中(.h),将具体的实现放在.c文件中,用户并不需要知道线程池实现的细节,所以这里就给用户暴露必要的三个接口,分别是创建线程池、销毁线程池和提交任务。
thrdpool_t *thrdpool_create(int thrd_count);
void thrdpool_terminate(thrdpool_t * pool);
typedef void (*handler_pt)(void * /* ctx */);
int thrdpool_post(thrdpool_t *pool, handler_pt func, void *arg);
⛅3.2 结构体设计
这里一共需要三个结构体,分别是线程池、任务队列和任务,定义如下:
struct thrdpool_s { // 线程池task_queue_t *task_queue; // 阻塞队列,让线程状态变更的操作应该封装在队列内部atomic_int quit; // 是否退出的标记,原子变量int thrd_count; // 线程数量pthread_t *threads; // 线程
};
这里要特别注意职责的划分,有不少开源线程池的实现把互斥锁这些本应该放在队列的变量放在了线程池中,应该要让线程状态变更的操作封装在队列内部,线程池结构体中包含了指向任务队列的指针,队列结构体定义如下:
typedef struct task_queue_s { // 队列void *head;void **tail; int block;spinlock_t lock; // 可以单独加锁pthread_mutex_t mutex; // 也可以复用互斥锁pthread_cond_t cond;
} task_queue_t;
任务结构体定义如下:
typedef struct task_s { // 任务void *next;handler_pt func;void *arg;
} task_t;
总体结构体的关系如图所示:

队列的head指针指向队头任务,tail指针指向队尾任务的next指针,要注意这里void **(指向指针的指针)的用法,不少优秀的开源代码都喜欢这样设计,可以简化后续链表操作。其中生产者从队列头部添加任务,消费者从队列尾部取出任务。
⛅3.3 关键函数书写
🌥️3.3.1 __threads_create & __threads_terminate
首先来看看创建线程的代码,像这种复杂资源创建要使用对称式的接口设计,有创建(__threads_create )就有销毁(__threads_terminate),并且建议采用回滚式的代码书写方式,先假设成功,然后回滚,代码呈现如下图的结构:

static int __threads_create(thrdpool_t *pool, size_t thrd_count) {pthread_attr_t attr;int ret;ret = pthread_attr_init(&attr);if (ret == 0) { // 1、假设pthread_attr_init成功pool->threads = (pthread_t *)malloc(sizeof(pthread_t) * thrd_count);if (pool->threads) { // 2、假设malloc成功int i = 0;for (; i < thrd_count; i++) {if (pthread_create(&pool->threads[i], &attr, __thrdpool_worker, pool) != 0) {break;}}pool->thrd_count = i;pthread_attr_destroy(&attr);if (i == thrd_count) // 3、假设pthread_create成功return 0; // 最终成功返回,下面都是回滚代码__threads_terminate(pool); // 3、回滚free(pool->threads); // 2、回滚}ret = -1; // 1、回滚}return ret;
}
数字对应的地方为代码书写顺序,可以看到回滚式代码书写还是非常优雅的。
销毁线程的代码如下:
static void __threads_terminate(thrdpool_t * pool) {atomic_store(&pool->quit, 1);__nonblock(pool->task_queue);int i;for (i=0; i<pool->thrd_count; i++) {pthread_join(pool->threads[i], NULL);}
}
🌥️3.3.2 __taskqueue_create
然后来看看任务队列的创建函数,同样使用回滚式代码书写方式
static task_queue_t *__taskqueue_create() {int ret;task_queue_t *queue = (task_queue_t *)malloc(sizeof(task_queue_t));if (queue) { // 1、假设malloc成功ret = pthread_mutex_init(&queue->mutex, NULL);if (ret == 0) { // 2、假设pthread_mutex_init成功ret = pthread_cond_init(&queue->cond, NULL);if (ret == 0) { // 3、假设pthread_cond_init成功spinlock_init(&queue->lock);queue->head = NULL;queue->tail = &queue->head;queue->block = 1;return queue; // 成功返回}pthread_mutex_destroy(&queue->mutex); // 2、回滚 }free(queue); // 1、回滚}return NULL;
}
🌥️3.3.3 __add_task & __pop_task
最后来看看添加任务和取任务的函数,这两个流程逻辑函数的书写不适合用回滚式书写方式,适合用防御式代码书写方式,即先把不满足条件的 return 掉,然后写满足条件的,这两个操作函数需要有较好的链表算法能力,并且在这里会体会到之前 void **tail 的好处,__add_task 函数如下:
static inline void __add_task(task_queue_t *queue, void *task) {// 不限定任务类型,只要该任务的结构起始内存是一个用于链接下一个节点的指针void **link = (void**)task;*link = NULL; // task->next = NULL; 不限定链接方式 spinlock_lock(&queue->lock);*queue->tail /* 等价于 queue->tail->next */ = link;queue->tail = link;spinlock_unlock(&queue->lock);pthread_cond_signal(&queue->cond);
}
可以看到使用了指针的指针之后,代码清爽简洁,然后没有用 void **tail,书写的代码量会变大,并且这种方式不会用到 next 指针,不限定链接方式,适用性更强。
__pop_task 函数如下:
static inline void * __pop_task(task_queue_t *queue) {spinlock_lock(&queue->lock);if (queue->head == NULL) {spinlock_unlock(&queue->lock);return NULL;}task_t *task;task = queue->head;void **link = (void**)task;queue->head = *link;if (queue->head == NULL) {queue->tail = &queue->head;}spinlock_unlock(&queue->lock);return task;
}
🪐4. 结束语
本文较为简单的阐述了线程池的实现方法,后续还会完善代码,并用gtest做完整的测试,本人目前还是个在校生,还比较小白,也刚刚开始写 CSDN 博客不久,可能写的也不是很好,如果有任何疑问或者发现我有哪里写的不对的地方,欢迎大家留言告诉我!我都会一一改正的。

如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持小占!

相关文章:
从0到1实现线程池(C语言版)
目录 🌤️1. 基础知识 ⛅1.1 线程概述 ⛅1.2 linux下线程相关函数 🌥️1.2.1 线程ID 🌥️1.2.2 线程创建 🌥️1.2.3 线程回收 🌥️1.2.4 线程分离 🌤️2. 线程池概述 ⛅2.1 线程池的定义 ⛅2.2 为…...
Visual studio自动添加头部注释
记事本打开VS版本安装目录下的Class.cs文件 增加如下内容:...
【C#生态园】提升性能效率:C#异步I/O库详尽比较和应用指南
优化异步任务处理:C#异步I/O库全面解析 前言 在C#开发中,异步I/O是一个重要的主题。为了提高性能和响应速度,开发人员需要深入了解各种异步I/O库的功能和用法。本文将介绍几个常用的C#异步I/O库,包括Task Parallel Library、Asy…...
管理医疗AI炒作的三种方法
一个人类医生和机器人医生互相伸手。 全美的医院、临床诊所和医疗系统正面临重重困难。他们的员工队伍紧张且人员短缺,运营成本不断上升,服务需求常常超过其承受能力,限制了医疗服务的可及性。 人工智能应运而生。在自ChatGPT推出将AI推向聚…...
VMware Workstation Pro Download 个人免费使用
参考 VMware Workstation Pro Download...
DevOps平台搭建过程详解--Gitlab+Jenkins+Docker+Harbor+K8s集群搭建CICD平台
一、环境说明 1.1CI/CD CI即为持续集成(Continue Integration,简称CI),用通俗的话讲,就是持续的整合版本库代码编译后制作应用镜像。建立有效的持续集成环境可以减少开发过程中一些不必要的问题、提高代码质量、快速迭代等;(Jenkins) CD即持续交付Con…...
Nginx之日志切割,正反代理,HTTPS配置
1 nginx日志切割 1.1 日志配置 在./configure --prefixpath指定的path中切换进去,找到log文件夹,进去后找到都是对应的日志文件 其中的nginx.pid是当前nginx的进程号,当使用ps -ef | grep nginx获得就是这个nginx.pid的值 在nginx.conf中…...
Mysql数据量大,如何拆分Mysql数据库(垂直拆分)
垂直拆分(Vertical Partitioning)是一种将数据库按照业务模块或功能进行拆分的方法,目的是将不同模块的数据放到不同的数据库中,从而减少单个数据库的压力,提高系统的性能和可扩展性。垂直拆分适用于数据量大且业务模块…...
机器人可能会在月球上提供帮助
登月是我们这个时代最具标志性的事件之一,这可能还算轻描淡写了:这是我们迄今为止在物理上探索得最远的一次。我听过一些当时的老广播,它们可以让你想象出这次航行的重要性。 现在,研究人员表示,我们可能很快就能重返…...
真实案例分享:零售企业如何避免销售数据的无效分析?
在零售业务的数据分析中,无效分析不仅浪费时间和资源,还可能导致错误的决策。为了避免这种情况,企业必须采取策略来确保他们的数据分析工作能够产生实际的商业价值。本文将通过行业内真实的案例,探讨零售企业如何通过精心设计的数…...
ctfshow-文件包含
web78 <?phpif(isset($_GET[file])){$file $_GET[file];include($file); }else{highlight_file(__FILE__); } 判断是否存在file参数 如果存在 将包含这个参数值 文件 php://filter可以获取指定文件源码。当它与包含函数结合时,php://filter流会被当作php文件执…...
Qt事件处理机制
用qt实现简单闹钟 widget.h #ifndef WIDGET_H #define WIDGET_H #include<QPushButton> #include<QTextEdit> #include<QLabel> #include <QWidget> #include<QMouseEvent> #include<QPoint> #include<QTime> #include<QTimer&…...
vue axios 如何读取项目下的json文件
在 Vue 项目中,使用 axios 读取本地的 JSON 文件可以通过将 JSON 文件放置在 public 目录中,然后通过 axios 发起请求读取。 步骤: 将 JSON 文件放置在 public 目录下: Vue 项目中的 public 目录是静态资源目录,项目编…...
燃气涡轮发动机性能仿真程序GSP12.0.4.2使用经验(二):使用GSP建立PG9351FA燃气轮机性能仿真模型
目录 一、PG9351FA燃气轮机简介及热力循环参数二、基于GSP的性能仿真模型设置环境参数设置进气道参数设置压气机参数设置燃烧室参数设置透平(涡轮)参数设置转子负载参数燃油流量外部控制 三、仿真结果四、其它 一、PG9351FA燃气轮机简介及热力循环参数 …...
迟滞比较器/施密特触发器
功能 从下面原理图像看来,只有在达到上下阈值才会出现输出电平的转换,这样防止信号的杂波跳变。而且每次的阈值是随着输出而变化的,当输出高时,阈值如下图中,V_PV_N V_R*( RF/(R1RF) )VH*( R1/(R1RF) );当输出低时&a…...
LeetCode_sql_day22(1112.每位学生的最高成绩)
描述:1112.每位学生的最高成绩 表:Enrollments ------------------------ | Column Name | Type | ------------------------ | student_id | int | | course_id | int | | grade | int | ------------------------ (st…...
OFDM信号PARP的CCDF图
文章目录 引言代码代码疑难解答参考文献 引言 本书主要参考了文献1,但实际上该书中符号和表述的错误非常多(只能说棒子是这样的);同时因为发表时间的关系,很多MATLAB代码进行了更新,原书提供的代码已经无法…...
LeetCode之高频SQL50题
查询 1757. 可回收且低脂的产品 584. 寻找用户推荐人 595. 大的国家 1148. 文章浏览 I 1683. 无效的推文 连接 1378. 使用唯一标识码替换员工ID 1068. 产品销售分析 I 1581. 进店却未进行过交易的顾客 197. 上升的温度 1661. 每台机器的进程平均运行时间 577. 员工…...
echarts多组堆叠柱状图
一、效果图 二、代码实现 1、创建容器 <el-card class"box-card"><div slot"header" class"clearfix"><span>课堂学习</span></div><div id"class-learning" style"height: 360px">&l…...
打造安心宠物乐园:EasyCVR平台赋能猫咖/宠物店的智能视频监控解决方案
随着宠物经济的蓬勃发展,宠物店与猫咖等场所对顾客体验、宠物安全及健康管理的需求日益提升。然而,如何确保这些场所的安全与秩序,同时提升顾客体验,成为了经营者们关注的焦点。引入高效、智能的视频监控方案,不仅能够…...
电脑插入多块移动硬盘后经常出现卡顿和蓝屏
当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时,可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案: 1. 检查电源供电问题 问题原因:多块移动硬盘同时运行可能导致USB接口供电不足&#x…...
HBuilderX安装(uni-app和小程序开发)
下载HBuilderX 访问官方网站:https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本: Windows版(推荐下载标准版) Windows系统安装步骤 运行安装程序: 双击下载的.exe安装文件 如果出现安全提示&…...
如何理解 IP 数据报中的 TTL?
目录 前言理解 前言 面试灵魂一问:说说对 IP 数据报中 TTL 的理解?我们都知道,IP 数据报由首部和数据两部分组成,首部又分为两部分:固定部分和可变部分,共占 20 字节,而即将讨论的 TTL 就位于首…...
Reasoning over Uncertain Text by Generative Large Language Models
https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...
Git常用命令完全指南:从入门到精通
Git常用命令完全指南:从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...
Chrome 浏览器前端与客户端双向通信实战
Chrome 前端(即页面 JS / Web UI)与客户端(C 后端)的交互机制,是 Chromium 架构中非常核心的一环。下面我将按常见场景,从通道、流程、技术栈几个角度做一套完整的分析,特别适合你这种在分析和改…...
【Veristand】Veristand环境安装教程-Linux RT / Windows
首先声明,此教程是针对Simulink编译模型并导入Veristand中编写的,同时需要注意的是老用户编译可能用的是Veristand Model Framework,那个是历史版本,且NI不会再维护,新版本编译支持为VeriStand Model Generation Suppo…...
aurora与pcie的数据高速传输
设备:zynq7100; 开发环境:window; vivado版本:2021.1; 引言 之前在前面两章已经介绍了aurora读写DDR,xdma读写ddr实验。这次我们做一个大工程,pc通过pcie传输给fpga,fpga再通过aur…...
Android Framework预装traceroute执行文件到system/bin下
文章目录 Android SDK中寻找traceroute代码内置traceroute到SDK中traceroute参数说明-I 参数(使用 ICMP Echo 请求)-T 参数(使用 TCP SYN 包) 相关文章 Android SDK中寻找traceroute代码 设备使用的是Android 11,在/s…...
从0开始学习R语言--Day17--Cox回归
Cox回归 在用医疗数据作分析时,最常见的是去预测某类病的患者的死亡率或预测他们的结局。但是我们得到的病人数据,往往会有很多的协变量,即使我们通过计算来减少指标对结果的影响,我们的数据中依然会有很多的协变量,且…...
