尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Linux C线程池实现:从pthread到任务队列的源码解析
简介这份资源面向具备一定C语言基础、希望深入理解Linux多线程编程与并发模型的学习者提供了一套可运行的简单线程池实现范例。内容围绕线程创建与管理、互斥锁与条件变量同步、任务队列设计、线程池初始化与销毁流程等核心知识点展开帮助读者掌握如何预先创建线程、复用空闲线程处理任务从而避免频繁创建销毁线程带来的系统开销。压缩包共17个文件以10个C源文件为主体辅以2个头文件、1个Makefile及若干说明文档整体约6KB结构紧凑便于直接编译调试与二次修改。目前已有146人学习关注。通过阅读与运行代码读者可理解线程池的工作流程与资源管理策略并在此基础上尝试任务优先级、超时机制等扩展优化适合作为多线程编程入门到进阶的实践参考。1. 从一份线程池源码包说起为什么面试和项目都绕不开它如果你正在 Linux 下写 C 语言服务端程序迟早会遇到一个绕不开的问题每来一个连接就pthread_create一次短连接场景下线程创建销毁的开销能把 CPU 吃满长连接场景下线程数又不可控内存和调度压力直接拉满。这份「Linux环境下C语言实现简单线程池.zip」就是冲着这个痛点来的——它把线程池最核心的骨架用纯 C 写了一遍没有花哨的封装也没有依赖第三方库适合拿来当模板改也适合拿来当面试八股文的实物对照。它解决的是「任务并发执行」这件事主线程把任务丢进队列一组预先创建好的工作线程从队列里取任务执行队列空就阻塞等待队列满就按策略处理。适合谁一是刚学完pthread想找个完整例子练手的同学二是需要给现有 C 项目加个轻量并发层但不想引入复杂框架的开发者三是准备面试时想把「线程池原理」从背答案变成能画图讲清楚的人。这份资源不是生产级框架但它的结构足够干净改起来不费劲。2. 线程池的骨架拆解从 pthread 到任务队列的四个关键结构2.1 为什么是「池」而不是「来一个开一个」线程创建是有成本的。在 Linux 上pthread_create底层会走clone系统调用内核要分配栈空间、初始化调度实体、维护 task_struct一次调用在普通 x86 机器上大概几微秒到几十微秒。单看不多但如果是高频短任务比如一个回显服务器每秒处理几千个连接创建销毁的开销就非常可观。线程池的思路是「空间换时间」启动时一次性创建 N 个线程让它们阻塞在任务队列上任务来了直接唤醒执行省掉反复创建销毁的环节。另一个好处是可控。线程数固定内存占用和上下文切换次数就能预估。你可以在配置文件里写死THREAD_POOL_SIZE 8也可以根据 CPU 核数动态算但不管怎样上限是你定的不会因为突发流量把机器拖垮。常见做法是线程数 CPU 核数 * 2或CPU 核数 1前者适合 I/O 密集型后者适合计算密集型但这不是铁律得看任务里阻塞操作的比例。2.2 四个核心结构体任务、队列、线程池、锁与条件变量这份源码包里的结构体设计很典型基本是教科书式的拆分。我把它抽象成四个部分来看结构体作用关键字段task_t描述一个待执行任务函数指针、参数指针、next 指针task_queue_t管理任务队列head、tail、size、mutex、condthread_pool_t管理线程池整体线程数组、队列指针、shutdown 标志锁与条件变量同步队列访问pthread_mutex_t、pthread_cond_t任务队列用链表实现head和tail分别指向队首和队尾入队从tail插出队从head取这样是 FIFO。队列自带一把互斥锁和一个条件变量工作线程发现队列为空时pthread_cond_wait挂起主线程入队后pthread_cond_signal唤醒一个等待的线程。这里有个细节pthread_cond_wait必须放在while循环里判断队列是否为空不能用if否则可能被虚假唤醒骗过去。线程池结构体里通常还会有一个shutdown标志用来优雅退出。主线程调用销毁函数时先把shutdown置 1然后广播条件变量唤醒所有线程线程醒来发现shutdown为真且队列为空就退出。这个顺序不能反否则线程可能永远卡在cond_wait上。2.3 任务入队与出队的代码骨架下面这段代码是入队和出队的核心逻辑我按常见实现整理了一下你可以对照源码包里的文件看// 入队主线程调用把任务挂到队列尾部 int task_queue_push(task_queue_t *q, task_t *task) { pthread_mutex_lock(q-mutex); // 锁住队列防止并发修改 if (q-size q-max_size) { // 队列满按策略处理 pthread_mutex_unlock(q-mutex); return -1; // 这里简单返回失败也可以阻塞等待 } task-next NULL; if (q-tail NULL) { // 空队列head 和 tail 都指向新任务 q-head task; q-tail task; } else { q-tail-next task; // 非空队列挂到尾部 q-tail task; } q-size; pthread_cond_signal(q-cond); // 唤醒一个等待的工作线程 pthread_mutex_unlock(q-mutex); return 0; } // 出队工作线程调用从队列头部取任务 task_t *task_queue_pop(task_queue_t *q) { pthread_mutex_lock(q-mutex); while (q-size 0 !q-shutdown) { // 必须用 while防止虚假唤醒 pthread_cond_wait(q-cond, q-mutex); } if (q-shutdown q-size 0) { // 关闭且无任务返回 NULL 让线程退出 pthread_mutex_unlock(q-mutex); return NULL; } task_t *task q-head; // 从头部取 q-head task-next; if (q-head NULL) { q-tail NULL; // 队列空了tail 也要置空 } q-size--; pthread_mutex_unlock(q-mutex); return task; }逻辑说明入队时先加锁检查队列容量然后修改head/tail指针和size最后signal唤醒一个线程。出队时先加锁用while循环等待队列非空或关闭标志取走头部任务后更新指针。参数方面max_size控制队列上限防止任务无限堆积shutdown标志由销毁函数设置配合pthread_cond_broadcast使用。注意pthread_cond_signal只唤醒一个线程如果队列里一次入了多个任务最好用pthread_cond_broadcast否则可能只有一个线程被唤醒其他任务要等下一次 signal。但 broadcast 有惊群效应实际用哪个要看任务到达频率。2.4 工作线程的主循环长什么样工作线程的入口函数通常是一个while(1)循环里面调用task_queue_pop拿到任务就执行拿不到就退出void *worker_thread(void *arg) { thread_pool_t *pool (thread_pool_t *)arg; while (1) { task_t *task task_queue_pop(pool-queue); // 阻塞取任务 if (task NULL) { // 返回 NULL 说明要退出了 break; } task-func(task-arg); // 执行任务函数 free(task); // 任务结构体由线程池分配这里释放 } return NULL; }这里有个容易翻车的地方task-func是用户传进来的函数指针它的参数task-arg也是用户传的。如果任务函数里要释放arg指向的内存那free(task)之前不能碰arg如果arg是栈上变量任务函数执行完之前调用者不能返回。这些边界在源码包里没有强制约束得自己注意。3. 编译与运行从 Makefile 到可执行文件的完整链路3.1 源码包的文件布局与编译命令拿到 zip 后先解压常见的文件布局是thread_pool/ ├── Makefile ├── thread_pool.c ├── thread_pool.h ├── main.c └── README.mdthread_pool.c里是线程池的实现thread_pool.h是结构体声明和函数原型main.c是测试用例。编译直接用make就行如果 Makefile 写得简单等价于gcc -o thread_pool main.c thread_pool.c -lpthread -Wall -g-lpthread必须加否则链接阶段会报undefined reference to pthread_create。-Wall打开所有警告-g保留调试符号方便用 gdb 跟。如果源码包里用了-stdc99或-stdgnu99注意pthread相关函数在 c99 下可能需要_GNU_SOURCE宏常见做法是在文件开头加#define _GNU_SOURCE。3.2 测试用例怎么跑提交任务与观察输出main.c里一般会创建一个线程池然后循环提交一批任务每个任务打印自己的线程 ID 和任务编号。跑起来大概是这个效果./thread_pool thread 1 start task 0 thread 2 start task 1 thread 3 start task 2 ...如果你看到任务编号不是严格递增的别慌这是正常的——多个线程并发取任务谁先抢到锁谁先执行顺序本来就不确定。想验证线程池是否真的复用了线程可以在任务函数里打印pthread_self()观察同一个线程 ID 是否出现在多个任务里。如果每个任务都是新线程 ID那说明线程池没生效可能每次都在创建新线程。提示测试时把任务函数里加个usleep(100000)模拟耗时操作这样能明显看到多个线程交替执行。如果任务瞬间完成输出会挤在一起不好观察。3.3 参数调优线程数、队列长度和任务粒度线程数不是越多越好。我一般会先用nproc看 CPU 核数然后按任务类型定纯计算任务线程数 核数 1多了反而增加上下文切换带 I/O 阻塞的任务线程数 核数 * 2 到 核数 * 4具体看阻塞比例混合型先按核数 * 2 跑用top -H看 CPU 利用率如果没跑满就加线程队列长度也要设上限。不设上限的话任务生产速度远大于消费速度时内存会被任务结构体吃光。常见做法是设一个max_size比如 1024 或 4096入队时如果满了就返回失败让调用方决定是丢弃还是重试。源码包里如果没实现队列满的处理你可以自己加一个pthread_cond_wait在入队侧等队列有空位再入。任务粒度也影响性能。如果每个任务只是做一次加法那线程池的调度开销比任务本身还大。这种场景应该把多个小任务合并成一个大任务或者干脆不用线程池直接串行跑。线程池适合的是「任务执行时间远大于调度开销」的场景比如每个任务要读文件、发网络包、做矩阵运算。4. 避坑与排查线程池跑不起来时先看这五条4.1 现象程序卡死gdb 显示所有线程都在 cond_wait原因销毁线程池时只设置了shutdown标志但没有调用pthread_cond_broadcast导致工作线程还在cond_wait上睡着主线程在pthread_join上等它们醒来互相等死锁。解决销毁函数里先加锁设置shutdown 1然后pthread_cond_broadcast(q-cond)再解锁最后pthread_join每个线程。顺序不能乱。4.2 现象任务执行到一半程序崩溃报 double free原因任务结构体task_t在入队时由调用方malloc出队后由工作线程free但调用方可能也free了一次。或者任务参数arg指向的内存被提前释放。解决明确所有权。我一般约定task_t由线程池内部管理入队时拷贝一份出队执行完由线程池释放arg指向的内存由调用方负责任务函数执行完之前不能释放。如果arg是动态分配的可以在任务函数末尾自己free但要在文档里写清楚。4.3 现象队列里任务堆积但线程都在闲着原因入队时用了pthread_cond_signal只唤醒一个线程但队列里一次入了多个任务被唤醒的线程取走一个后其他线程还在睡。如果后续没有新的 signal剩下的任务就没人处理。解决入队时如果队列长度从 0 变成 1用signal如果一次入多个用broadcast。或者简单点每次入队都broadcast代价是惊群效应但实现简单任务量不大时够用。4.4 现象编译报 undefined reference to pthread_create原因链接时没加-lpthread或者 Makefile 里LDFLAGS没写对。解决在编译命令末尾加-lpthread注意顺序库要放在源文件后面。如果用的是 CMake加find_package(Threads REQUIRED)和target_link_libraries(thread_pool PRIVATE Threads::Threads)。4.5 现象线程数设了 8但 top 里只看到 1 个线程在跑原因任务函数里加了全局锁或者任务本身是串行的多个线程抢同一把锁实际还是单线程执行。解决检查任务函数里有没有pthread_mutex_lock保护共享资源如果有考虑缩小锁的粒度或者把共享资源改成线程局部存储。另外确认任务之间没有依赖关系如果任务 B 必须等任务 A 完成那并发度自然上不去。5. 进阶玩法把线程池改成带优先级的任务调度器5.1 从单队列到多队列按优先级拆分基础线程池只有一个 FIFO 队列所有任务平等。但实际场景里有些任务要优先处理比如心跳包比日志写入更紧急。改法很简单把task_queue_t复制成三份分别对应高、中、低优先级工作线程取任务时按优先级顺序检查task_t *task_queue_pop_priority(thread_pool_t *pool) { task_t *task NULL; pthread_mutex_lock(pool-mutex); while (pool-total_size 0 !pool-shutdown) { pthread_cond_wait(pool-cond, pool-mutex); } if (pool-shutdown pool-total_size 0) { pthread_mutex_unlock(pool-mutex); return NULL; } // 按高、中、低顺序取哪个非空取哪个 if (pool-high_queue.size 0) { task task_queue_pop_nolock(pool-high_queue); } else if (pool-mid_queue.size 0) { task task_queue_pop_nolock(pool-mid_queue); } else { task task_queue_pop_nolock(pool-low_queue); } pool-total_size--; pthread_mutex_unlock(pool-mutex); return task; }这里把三把锁合并成一把池级别的锁简化同步逻辑。total_size是三个队列长度之和用来判断是否有任务可取。入队时根据优先级选择对应队列并更新total_size。5.2 动态扩缩容根据队列长度调整线程数固定线程数在流量波动大时不够灵活。可以加一个管理线程每隔几秒检查一次队列长度如果队列长度持续大于阈值且线程数没到上限就pthread_create加线程如果队列长期为空且线程数超过下限就设置一个exit_flag让多余线程退出。void *manager_thread(void *arg) { thread_pool_t *pool (thread_pool_t *)arg; while (!pool-shutdown) { sleep(1); // 每秒检查一次 pthread_mutex_lock(pool-mutex); int queue_len pool-queue-size; int active pool-active_threads; pthread_mutex_unlock(pool-mutex); if (queue_len pool-max_queue_threshold active pool-max_threads) { // 扩容创建新线程 pthread_t tid; pthread_create(tid, NULL, worker_thread, pool); pool-active_threads; } else if (queue_len 0 active pool-min_threads) { // 缩容设置退出标志让空闲线程自己退出 pool-shrink_flag 1; pthread_cond_broadcast(pool-cond); } } return NULL; }缩容比扩容麻烦因为要确保退出的线程没有正在执行的任务。常见做法是给每个线程一个idle标志管理线程只让idle的线程退出。这块代码量不小建议先把基础版跑稳再动。5.3 验证方法用压力测试看线程池是否真的省了资源改完之后怎么验证我一般写个简单的压测程序提交 10 万个空任务分别用「每次创建线程」和「线程池」两种方式跑用time命令看耗时用valgrind --toolmassif看内存峰值。线程池版本应该明显快于每次创建线程的版本内存也应该平稳。如果线程池版本反而更慢检查是不是锁竞争太激烈或者任务粒度太小导致调度开销占了大头。还有一个土办法在worker_thread入口和出口加计数器统计每个线程执行了多少任务。如果线程数设了 8但只有一个线程执行了 90% 的任务说明任务分配不均可能是锁竞争或者任务本身耗时差异太大。从那以后我每次拿到一个线程池实现都会先跑一遍「空任务压测 耗时任务观察线程 ID」这两步确认它真的在复用线程、真的在并发执行再往里面加业务逻辑。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

AFSim 2.9.0 Linux源码编译指南:CMake配置与环境变量避坑全流程

AFSim 2.9.0 Linux源码编译指南:CMake配置与环境变量避坑全流程

简介:面向Linux平台源码编译AFSim2.9.0仿真工具集的实操指南,由作者依据自身完整编译经验整理,适合需要在Ubuntu等环境下搭建AFSim的仿真开发与研究人员参考。文档为单个PDF文件,体积约694KB,核心内容涵盖编译环境要求…

📅 2026/10/15 18:29:11
Java单测自动生成利器Diffblue Cover:原理、实践与避坑指南

Java单测自动生成利器Diffblue Cover:原理、实践与避坑指南

简介:这是一份面向 Java 开发者的 Diffblue Cover Community Edition 离线插件包,专门适配 IntelliJ IDEA 2022.2 集成开发环境。它利用智能代码分析,自动生成遵循 JUnit 或 TestNG 规范的单元测试类与方法,能够覆盖多条执行路径&…

📅 2026/10/15 18:29:11
TDesign Button 按钮组件(Uniapp)完整指南:引入方式、属性 API、事件与样式定制

TDesign Button 按钮组件(Uniapp)完整指南:引入方式、属性 API、事件与样式定制

前端UI组件小程序设计系统移动开发 【免费下载链接】tdesign-miniprogram Wechat MiniProgram and Uniapp UI components lib for TDesign 项目地址: https://gitcode.com/gh_mirrors/tde/tdesign-miniprogram 点击查看 免费下载 TDesign 的 Button 按钮组件用于开…

📅 2026/10/15 18:24:10
MORE NEWS

更多资讯

📰

计算机组成原理课设:从指令集到数据通路的CPU设计实战指南

简介:面向山东大学20级计算机专业学生的《计算机组成原理》课程设计资料,围绕电路图设计与命令集定义展开,内容涵盖数据表示、运算器、控制器、存储系统等核心知识点,并涉及原理图绘制、指令集设计、实验报告撰写等完整环节&#…

📰

两串锂电池均衡实战:MP2642GR-Z与PIC32MZ1024EFK144的软硬协同设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

多个传感器如何协同作战?World2Agent多传感器组合与fanout传输完整指南

【免费下载链接】world2agent World2Agent(W2A) is an open protocol that standardizes how Al agents perceive the real world. 项目地址: https://gitcode.com/gh_mirrors/wo/world2agent 点击查看 免费下载 World2Agent(W2A)是一个开放…

📰

PASCAL-5i 小样本语义分割数据集复现要点与踩坑指南

简介:Pascal-5i数据集是面向小样本学习研究的标准化数据集,源自PASCAL VOC,将20个类别划分为基础类与新类,支持5-way 1-shot和5-way 5-shot任务,适合需要验证原型网络、元学习或知识迁移方法的科研人员与算法工程师使用…

📰

c9-python-getting-started 系列:用 asyncio 与 async/await 编写 Python 异步操作

示例工程教程 【免费下载链接】c9-python-getting-started Sample code for Channel 9 Python for Beginners course 项目地址: https://gitcode.com/gh_mirrors/c9/c9-python-getting-started 点击查看 免费下载 本指南围绕 more-python-for-beginners 课程中「As…

📰

SAP固定资产减值与增值配置实操指南

简介:本资源是一份面向SAP财务顾问与企业资产管理人员的实操型配置手册,聚焦固定资产减值与增值在SAP系统中的合规落地,解决市场价值波动下账面价值调整难、账务处理逻辑不清、后台配置易出错等实际问题。手册系统梳理三种主流实现方式&#…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬