• 正文
  • 相关推荐
申请入驻 产业图谱

一个嵌入式线程池的极简实现!

11/10 09:03
462
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

一、项目简介

嵌入式系统开发中,我们经常遇到这样的场景:需要并发处理多个任务,但频繁创建和销毁线程会带来显著的性能开销。线程池技术应运而生——预先创建一组工作线程,通过任务队列分发工作,实现线程的复用。

线程池的线程复用:线程池中的线程可以被重复利用,用于执行多个任务,避免了频繁创建和销毁线程的性能开销。提高响应速度。假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间。

https://github.com/Pithikos/C-Thread-Pool

C-Thread-Pool是一个基于POSIX标准的轻量级线程池实现,代码量不到600行,却提供了完整的线程池功能。它的核心价值在于:

POSIX兼容:可在Linux、BSD等类Unix系统运行,无需依赖特定平台API

接口简洁:仅7个API函数,学习成本低

功能完备:支持动态添加任务、等待任务完成、暂停/恢复执行、查询工作状态

资源可控:线程数量在初始化时确定,避免线程爆炸

对于嵌入式工程师而言,这个项目是学习多线程编程和同步机制的绝佳范例。它展示了如何用最少的代码实现一个生产级的线程池,其设计思想可直接应用到资源受限的嵌入式环境。

二、整体架构设计

2.1 核心架构

C-Thread-Pool的架构可以概括为"生产者-消费者"模型:主线程作为生产者向任务队列添加工作,工作线程作为消费者从队列取出任务并执行。

2.2 数据结构设计

线程池的精髓在于其数据结构设计,它们环环相扣,共同实现了高效的任务调度。

数据结构解析:

thpool_ - 线程池的主控结构

threads:线程指针数组,管理所有工作线程

num_threads_alive/num_threads_working:使用volatile修饰,确保多线程可见性

thcount_lock:保护线程计数的互斥锁

threads_all_idle:条件变量,用于实现thpool_wait()的高效等待

jobqueue - 任务队列(单向链表实现)

front/rear:队首队尾指针,支持O(1)时间复杂度的入队出队

rwmutex:读写互斥锁,保护队列的并发访问

has_jobs:二值信号量,用于线程的阻塞和唤醒

job - 任务节点

prev:指向前一个任务,构成链表

function:函数指针,指向要执行的工作函数

arg:void指针,传递任意类型的参数

bsem - 二值信号量的自实现

为什么不用POSIX信号量?因为项目追求POSIX兼容性,而sem_t在某些系统上不可用用互斥锁+条件变量+整型值模拟信号量行为

2.3 工作流程

线程池的核心是工作线程的执行循环。一个任务从提交到完成的全过程:

任务提交阶段

thpool_add_work()

      • 分配job结构并初始化函数指针和参数通过

jobqueue_push()

      • 将任务加入队列尾部调用

bsem_post()

      • 唤醒一个等待的工作线程

任务获取阶段

工作线程调用bsem_wait()阻塞在信号量上当有任务到来时,一个线程被pthread_cond_signal()唤醒线程调用jobqueue_pull()从队首取出任务

任务执行阶段:增加num_threads_working计数执行用户提供的函数释放job内存减少计数,如果所有线程都空闲则发信号通知主线程

2.4 关键设计细节

1. 为什么用二值信号量而不是计数信号量?

观察jobqueue_pull()的实现,当队列中有多个任务时,它会在取出任务后再次调用bsem_post()。这是一个巧妙的设计:

这种"链式唤醒"机制保证了只要队列非空,就会有线程持续工作,同时避免了惊群效应(所有线程同时被唤醒争抢一个任务)。

2. volatile关键字的使用

这两个全局变量用于控制所有线程的行为。volatile告诉编译器这些变量可能被其他线程修改,不要优化掉对它们的重复读取。在thread_do()的主循环中:

双重检查确保即使线程被唤醒,也能及时响应销毁信号。

3. 线程销毁的优雅处理

thpool_destroy()的实现展示了如何优雅地终止线程池:

先给1秒的宽限期让空闲线程退出,然后持续唤醒直到所有线程结束。这避免了使用pthread_cancel()的强制终止,保证了线程有机会清理资源。

三、实战:构建一个图像处理应用

在嵌入式系统中,线程池技术可以应用于多种场景,如数据处理、网络通信、传感器数据采集等。

网络服务器

用途:在网络服务器中,使用线程池处理多个客户端的请求。每个客户端的请求可以被视为一个任务,线程池中的线程可以并发地处理这些任务。

优势:使用线程池可以提高服务器的并发处理能力,减少因频繁创建和销毁线程而带来的开销,从而提高服务器的响应速度和整体性能。

数据处理

用途:在数据处理场景中,使用线程池用于并行处理大量数据。例如,对大量数据进行排序、搜索或分析时,可以将数据分成多个小块,每个小块作为一个任务交给线程池处理。

优势:通过并行处理,可以显著缩短数据处理时间,提高数据处理的效率。

数据采集

用途:在实时系统中,使用线程池可以用于处理周期性或突发性的任务。例如,在嵌入式实时操作系统中,可以使用线程池来管理传感器数据的采集和处理任务。

优势:线程池可以提供稳定的响应时间,确保任务在预定的时间内完成,从而满足实时系统的要求。

让我们通过一个实际场景来使用线程池:并行处理图像数据。假设我们有一批传感器数据需要进行平方运算(模拟图像滤波)。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include "thpool.h"

typedefstruct {
    int *data;        // 输入数据数组
    int index;        // 当前处理的索引
    long result;      // 计算结果
} task_data_t;

void compute_task(void *arg) {
    task_data_t *task = (task_data_t *)arg;
    
    // 模拟复杂计算:平方运算
    task->result = (long)task->data[task->index] * task->data[task->index];
    
    printf("Thread #%u processed data[%d] = %d, result = %ldn", 
           (unsignedint)pthread_self(), 
           task->index, 
           task->data[task->index], 
           task->result);
    
    free(task);  // 释放任务内存
}

int main() {
    int data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int num_elements = sizeof(data) / sizeof(data[0]);
    
    // 创建4线程池(通常设为CPU核心数)
    threadpool thpool = thpool_init(4);
    if (!thpool) {
        fprintf(stderr, "Failed to create thread pooln");
        return1;
    }
    
    printf("Submitting %d tasks to thread pooln", num_elements);
    
    // 提交所有任务
    for (int i = 0; i < num_elements; i++) {
        task_data_t *task = malloc(sizeof(task_data_t));
        task->data = data;
        task->index = i;
        
        if (thpool_add_work(thpool, compute_task, task) == -1) {
            fprintf(stderr, "Failed to add workn");
            free(task);
        }
    }
    
    // 等待所有任务完成
    thpool_wait(thpool);
    printf("All tasks completedn");
    
    // 查询工作线程数(此时应为0)
    printf("Working threads: %dn", thpool_num_threads_working(thpool));
    
    // 销毁线程池
    thpool_destroy(thpool);
    
    return0;
}

代码要点:

内存管理:每个任务的task_data_t在主线程中分配,在工作线程中释放。这是线程池的常见模式——谁使用谁释放。

参数传递:通过结构体传递多个参数,避免了全局变量的使用,使代码更加线程安全。

等待机制thpool_wait()会阻塞主线程直到所有任务完成。它的实现基于条件变量,不会浪费CPU:

编译运行:

gcc example1.c thpool.c -pthread -o example1
./example1

预期输出:

Submitting 10 tasks to thread pool
Thread #123456 processed data[0] = 1, result = 1
Thread #234567 processed data[1] = 2, result = 4
Thread #345678 processed data[2] = 3, result = 9
...
All tasks completed
Working threads: 0

四、总结

C-Thread-Pool项目蕴含了多线程编程的精髓,值得深入学习的设计亮点包括:

4.1 极简主义的API设计

7个函数覆盖了线程池的完整生命周期,这种克制体现了"少即是多"的哲学。对嵌入式系统而言,简单的API意味着更小的二进制体积和更低的学习成本。

4.2 精巧的同步机制

用不到100行代码实现了二值信号量,展示了如何用基础同步原语(互斥锁+条件变量)组合出复杂功能。"链式唤醒"避免惊群,条件变量实现零CPU开销的等待,这些技巧在资源受限的嵌入式环境中尤为重要。

4.3 防御性编程实践

双重检查threads_keepalive防止竞态条件优雅的销毁流程避免资源泄漏volatile确保多线程可见性返回值检查(如thpool_add_work()返回-1表示失败)

4.4 可移植性设计

通过条件编译支持Linux、BSD,没有使用任何非标准扩展。这种POSIX友好的设计使代码可以轻松移植到不同的嵌入式Linux平台。

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

本公众号专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,公众号内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!