不会书头像
关注
Linux 线程池从零实现:pthread 与 C++11 std::thread 双版本实战封面图

Linux 线程池从零实现:pthread 与 C++11 std::thread 双版本实战

Linux 线程池从零实现:pthread 与 C++11 std::thread 双版本实战


1. 进程与线程:30 秒快速理解

1.1 两个「基本单位」

  • 进程:操作系统资源分配的基本单位,拥有独立的虚拟地址空间,进程之间相互隔离。

  • 线程CPU 调度的基本单位,寄生在进程内部,共享进程的地址空间和全局数据。

对比项进程 Process线程 Thread
本质资源分配的基本单位CPU 调度的基本单位
地址空间独立,互相隔离共享所属进程的地址空间
创建方式fork() / clone()pthread_create / std::thread
通信方式管道、信号、共享内存、Socket 等 IPC共享变量 + 互斥锁 / 条件变量
创建与切换开销
崩溃影响互不影响一个线程异常可能影响整个进程
适用场景隔离要求高、多进程架构大量并发、共享数据多

图 1 进程是资源分配的单位,线程是 CPU 调度的单位。

1.2 为什么要「线程池」

线程的创建和销毁是有开销的(内核栈、调度器数据结构、上下文切换)。如果每个任务都现场 pthread_createjoin,高频短任务场景下开销占比会很高。

线程池的思路是:提前创建好 N 个线程常驻,任务来了丢进队列,由空闲线程取走执行。 好处有三个:

  • 复用线程:减少创建 / 销毁开销;

  • 控制并发度:防止线程无限制增长拖垮系统;

  • 任务与执行解耦:提交方只关心「提交」,不关心「谁执行」。


2. 高频面试题速答(进程 / 线程基础)

Q1:进程和线程有什么区别?进程是资源分配的基本单位(独立地址空间、相互隔离);线程是 CPU 调度的基本单位(共享地址空间)。进程更重、隔离更好;线程更轻、同步成本更高。

Q2:fork() 为什么「调用一次,返回两次」?fork() 复制当前进程生成子进程:父进程返回子进程 PID(>0),子进程返回 0,失败返回 -1。靠返回值区分父子两条执行流。

Q3:线程同步有哪些方式?互斥锁(mutex)、条件变量(cond)、读写锁(rwlock)、自旋锁(spinlock)、信号量(semaphore)、原子操作(atomic)。嵌入式里最常用互斥锁 + 条件变量

Q4:死锁产生的四个必要条件?互斥、持有并等待、不可剥夺、循环等待。避免方法:所有线程按同一顺序加锁、一次性申请所有锁、用 trylock 超时放弃、尽量缩小锁粒度。

Q5:条件变量为什么要配合互斥锁使用?「检查条件 → 睡眠」必须是原子的:先加锁检查,不满足就 waitwait 内部会释放锁并睡眠,被唤醒后重新拿锁)。否则会出现「刚检查完条件、正要睡,任务恰好被提交」的丢失唤醒

Q6:等待条件为什么用 while 而不是 if防止虚假唤醒(spurious wakeup):即使条件不满足,线程也可能被唤醒。所以醒来后必须用 while 重新检查条件,不满足就继续等。


3. 线程池原理与设计要点

图 2 线程池三件套:任务队列 + N 个工作线程 + 互斥锁 / 条件变量。

一个线程池最少需要 5 个成员:

  1. 任务队列:存放待执行任务(链表 / 队列)。

  2. 工作线程数组:预先创建 N 个线程,循环取任务。

  3. 互斥锁:保护任务队列的入队 / 出队。

  4. 条件变量:队列为空时让工作线程睡眠,有任务时唤醒。

  5. 停止标志:用于优雅关闭线程池。

三个关键设计点:

  • 锁外执行任务:取出任务后立刻解锁,再执行任务函数。否则一个慢任务会阻塞所有线程取任务。

  • while 等待条件:防止虚假唤醒,pthread_cond_wait 必须用 while 循环包裹。

  • 优雅退出:置 shutdown 标志 → broadcast 唤醒所有线程 → 线程处理完剩余任务后自行退出 → join 回收。


4. Linux pthread 线程池实现(C 语言)

4.1 设计思路

  • 链表实现任务队列,节点保存函数指针 + 参数;

  • 每个工作线程是一个 while(1) 循环:没任务就睡在条件变量上,有任务就出队执行;

  • 提交任务 = 加锁 + 入队 + signal 唤醒一个线程 + 解锁;

  • 关闭线程池 = 置标志 + broadcast + join

4.2 完整代码

/* Linux 线程池实现:pthread 版本 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
​
/* 任务节点(链表) */
typedef struct task {
    void (*func)(void *arg);   /* 任务函数 */
    void *arg;                 /* 任务参数 */
    struct task *next;
} task_t;
​
/* 线程池结构 */
typedef struct threadpool {
    pthread_mutex_t lock;      /* 保护任务队列的互斥锁 */
    pthread_cond_t  not_empty; /* 队列非空条件变量 */
    pthread_t      *threads;   /* 工作线程数组 */
    task_t         *head;      /* 任务队列头 */
    task_t         *tail;      /* 任务队列尾 */
    int             thread_count;
    int             shutdown;  /* 1 = 停止 */
} threadpool_t;
​
/* 工作线程主函数 */
static void *worker(void *arg) {
    threadpool_t *pool = (threadpool_t *)arg;
​
    for (;;) {
        pthread_mutex_lock(&pool->lock);
​
        /* 队列空且未关闭 -> 挂起等待条件变量 */
        while (pool->head == NULL && !pool->shutdown) {
            pthread_cond_wait(&pool->not_empty, &pool->lock);
        }
​
        /* 关闭且队列已清空 -> 退出线程 */
        if (pool->shutdown && pool->head == NULL) {
            pthread_mutex_unlock(&pool->lock);
            pthread_exit(NULL);
        }
​
        /* 取出队头任务(在锁内出队,锁外执行) */
        task_t *t = pool->head;
        pool->head = t->next;
        if (pool->head == NULL) pool->tail = NULL;
        pthread_mutex_unlock(&pool->lock);
​
        t->func(t->arg);   /* 执行任务 */
        free(t);           /* 释放任务节点 */
    }
    return NULL;
}
​
/* 创建线程池 */
threadpool_t *threadpool_create(int thread_count) {
    threadpool_t *pool = calloc(1, sizeof(*pool));
    pool->thread_count = thread_count;
    pthread_mutex_init(&pool->lock, NULL);
    pthread_cond_init(&pool->not_empty, NULL);
    pool->threads = malloc(sizeof(pthread_t) * thread_count);
​
    for (int i = 0; i < thread_count; i++) {
        pthread_create(&pool->threads[i], NULL, worker, pool);
    }
    return pool;
}
​
/* 提交任务 */
int threadpool_add(threadpool_t *pool, void (*func)(void *), void *arg) {
    task_t *t = malloc(sizeof(*t));
    t->func = func;
    t->arg  = arg;
    t->next = NULL;
​
    pthread_mutex_lock(&pool->lock);
    if (pool->tail) {
        pool->tail->next = t;   /* 追加到队尾 */
    } else {
        pool->head = t;         /* 队列为空,作为第一个任务 */
    }
    pool->tail = t;
    pthread_cond_signal(&pool->not_empty);   /* 唤醒一个空闲工作线程 */
    pthread_mutex_unlock(&pool->lock);
    return 0;
}
​
/* 销毁线程池:先置关闭标志,再唤醒所有线程,等它们处理完剩余任务后退出 */
void threadpool_destroy(threadpool_t *pool) {
    pthread_mutex_lock(&pool->lock);
    pool->shutdown = 1;
    pthread_mutex_unlock(&pool->lock);
​
    pthread_cond_broadcast(&pool->not_empty);   /* 唤醒所有等待中的线程 */
​
    for (int i = 0; i < pool->thread_count; i++) {
        pthread_join(pool->threads[i], NULL);   /* 回收工作线程 */
    }
​
    /* 清理可能残留的任务节点(正常情况下应为空) */
    task_t *t;
    while (pool->head) {
        t = pool->head;
        pool->head = t->next;
        free(t);
    }
    free(pool->threads);
    pthread_mutex_destroy(&pool->lock);
    pthread_cond_destroy(&pool->not_empty);
    free(pool);
}
​
/* ---------------- 测试 ---------------- */
typedef struct {
    int id;
} job_t;
​
void my_task(void *arg) {
    job_t *j = (job_t *)arg;
    printf("task %2d 由线程 %lu 执行\n", j->id, (unsigned long)pthread_self());
    usleep(100000);   /* 模拟耗时 */
}
​
int main(void) {
    threadpool_t *pool = threadpool_create(4);   /* 4 个工作线程 */
​
    job_t jobs[20];
    for (int i = 0; i < 20; i++) {
        jobs[i].id = i;
        threadpool_add(pool, my_task, &jobs[i]);
    }
​
    sleep(2);                 /* 等待任务执行完 */
    threadpool_destroy(pool); /* 优雅关闭 */
    printf("线程池已关闭\n");
    return 0;
}

4.3 代码讲解

  • worker():工作线程的主体。加锁后先检查队列:空且未关闭就 pthread_cond_wait 睡眠;关闭且队列清空就退出;否则出队。出队后立刻解锁,在锁外执行任务,这是避免慢任务阻塞整个线程池的关键。

  • threadpool_create():初始化锁和条件变量,一次性创建 N 个线程。线程创建后立即进入 worker 循环等待任务。

  • threadpool_add():加锁 → 尾插链表 → signal 唤醒一个线程 → 解锁。signal 只唤醒一个,正好匹配「提交一个任务只需要一个线程来处理」。

  • threadpool_destroy():置 shutdown = 1broadcast 唤醒所有线程。worker 醒来发现「关闭且队列已空」就退出,join 逐个回收。这样能保证已提交的任务全部执行完,不会丢任务。

4.4 编译运行

gcc -o threadpool threadpool.c -pthread -std=c11
./threadpool

可能的输出(线程号因系统而异,20 个任务由 4 个线程并发消化):

task  0 由线程 140251943358528 执行
task  1 由线程 140251934965824 执行
task  2 由线程 140251926573120 执行
task  3 由线程 140251918180416 执行
task  4 由线程 140251934965824 执行
...
task 19 由线程 140251918180416 执行
线程池已关闭

5. C++11 std::thread 线程池实现

5.1 设计思路

  • std::function<void()> 抹平任务类型,任意可调用对象都能入队;

  • std::packaged_task + std::future,让任务可以取回返回值

  • 用 RAII:析构函数里自动关闭并 join,杜绝忘记回收线程;

  • enqueue 模板化,支持任意参数个数和类型的任务。

5.2 完整代码

/* C++11 线程池实现:std::thread + std::packaged_task + std::future */
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include <memory>
#include <stdexcept>
​
class ThreadPool {
public:
    explicit ThreadPool(size_t threads)
        : stop_(false) {
        for (size_t i = 0; i < threads; ++i) {
            workers_.emplace_back([this] {
                for (;;) {
                    std::function<void()> task;
                    {
                        /* 条件变量必须在持锁状态下等待 */
                        std::unique_lock<std::mutex> lock(queue_mutex_);
                        /* 用 while 防止虚假唤醒 */
                        cv_.wait(lock, [this] {
                            return stop_ || !tasks_.empty();
                        });
                        if (stop_ && tasks_.empty()) return;
                        task = std::move(tasks_.front());
                        tasks_.pop();
                    }
                    task();   /* 锁外执行任务 */
                }
            });
        }
    }
​
    /* 提交任务,返回 std::future 用于获取结果 */
    template <class F, class... Args>
    auto enqueue(F&& f, Args&&... args)
        -> std::future<typename std::result_of<F(Args...)>::type> {
        using return_type = typename std::result_of<F(Args...)>::type;
​
        /* packaged_task 包装可调用对象,通过 future 取返回值 */
        auto task = std::make_shared<std::packaged_task<return_type()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...));
​
        std::future<return_type> res = task->get_future();
        {
            std::unique_lock<std::mutex> lock(queue_mutex_);
            if (stop_) throw std::runtime_error("enqueue on stopped ThreadPool");
            /* 再包一层 lambda,统一抹平为无参无返回 */
            tasks_.emplace([task]() { (*task)(); });
        }
        cv_.notify_one();   /* 唤醒一个空闲工作线程 */
        return res;
    }
​
    /* 析构:置停止标志,唤醒全部线程并回收 */
    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex_);
            stop_ = true;
        }
        cv_.notify_all();
        for (std::thread &worker : workers_) {
            worker.join();
        }
    }
​
    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;
​
private:
    std::vector<std::thread> workers_;          /* 工作线程 */
    std::queue<std::function<void()>> tasks_;   /* 任务队列 */
    std::mutex queue_mutex_;                    /* 队列互斥锁 */
    std::condition_variable cv_;                /* 条件变量 */
    bool stop_;                                 /* 停止标志 */
};
​
int main() {
    ThreadPool pool(4);   /* 4 个工作线程 */
​
    /* 有返回值的任务:用 future 拿结果 */
    auto f1 = pool.enqueue([](int a, int b) { return a + b; }, 3, 4);
    auto f2 = pool.enqueue([](int a) { return a * a; }, 9);
​
    /* 无返回值的任务 */
    for (int i = 0; i < 10; ++i) {
        pool.enqueue([i] {
            std::cout << "task " << i << " in thread "
                      << std::this_thread::get_id() << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        });
    }
​
    std::cout << "3 + 4 = " << f1.get() << std::endl;
    std::cout << "9 * 9 = " << f2.get() << std::endl;
​
    /* 离开作用域时自动析构,优雅关闭 */
    return 0;
}

5.3 代码讲解

  • worker 循环:和 C 版本质相同——持锁等待 stop_ || !tasks_.empty(),条件满足后取任务、锁外执行

  • enqueue 模板F 是任意可调用对象,Args... 是参数包。先用 packaged_task<return_type()> 绑定参数,再用 lambda 包成 std::function<void()> 入队,同时返回 std::future 给调用方取结果。

  • RAII 析构:置 stop_ = truenotify_all() → 逐个 join()。哪怕调用方忘了手动关闭,对象销毁时也会自动优雅关闭。

  • 注意std::result_of 是 C++11 的写法(C++17 起可用 std::invoke_result 替代,语义相同)。C++ 标准库线程是跨平台的,同样的代码在 Linux / Windows / RTOS 上都能编译。

5.4 编译运行

g++ -o threadpool threadpool.cpp -std=c++11 -pthread
./threadpool

输出示例(多线程并发打印,顺序交错是正常现象):

task 0 in thread 2
task 1 in thread 3
3 + 4 = 7
9 * 9 = 81
task 2 in thread 4
...
task 9 in thread 2

生产环境中多线程写日志/控制台要加日志锁,否则输出会互相穿插。


6. 任务流转:条件变量如何驱动线程池

图 3 生产者和工作线程通过「互斥锁 + 条件变量」协作。

  • 生产者:加锁 → 任务入队 → signal / notify_one 唤醒一个线程 → 解锁;

  • 工作线程:加锁 → 条件不满足则 wait(释放锁并睡眠)→ 被唤醒后重新拿锁 → 出队 → 解锁 → 锁外执行

  • notify_one 只唤醒一个线程,适合「每提交一个任务就够一个线程处理」;notify_all / broadcast 唤醒所有线程,适合关闭线程池的场景。


7. 线程池面试题深入解析

Q1:什么是线程池?为什么用线程池?预先创建一组线程并复用,避免频繁创建 / 销毁线程的开销,同时限制最大并发数,防止系统资源被打满。

Q2:线程池有哪些核心参数?

  • 核心线程数:常驻线程数量;

  • 最大线程数:任务暴增时最多扩到多少;

  • 任务队列容量:有界还是无界;

  • 拒绝策略:队列满时怎么办(丢弃、阻塞、抛异常)。

Q3:线程数怎么定?

  • CPU 密集型:≈ CPU 核数,再多只会增加上下文切换开销;

  • IO 密集型:≈ 核数 × 2 或更多,线程大部分时间在等 IO、不占 CPU;

  • 最终结合压测调优,没有银弹。

Q4:怎么优雅关闭线程池?置停止标志 → 唤醒所有线程 → 等待队列中已提交的任务执行完 → 线程退出 → join 回收。不要直接 pthread_cancel 强杀线程,可能造成资源泄漏或数据不一致。

Q5:有界队列和无界队列的区别?无界队列实现简单,但任务堆积会吃光内存(触发 OOM);有界队列配合拒绝策略更可控,生产环境推荐。

Q6:线程池和进程池的区别?线程池共享地址空间、切换快,但一个线程崩溃可能影响整个进程;进程池隔离好,但创建 / 切换开销大、通信要走 IPC。嵌入式里常用「多进程 + 进程内多线程」的混合架构。


8. 两种实现对比

对比项pthread 线程池(C)std::thread 线程池(C++11)
语言CC++11
任务表示函数指针 + void *std::function + 模板
获取返回值不支持,需自行包装std::future 直接支持
资源管理手动 create / destroy / joinRAII,析构自动回收
可移植性POSIX 平台(Linux / Unix)标准库,跨平台
适用场景嵌入式 C 工程、系统编程C++ 工程、通用服务

9. 总结

  • 线程池 = 任务队列 + 工作线程 + 互斥锁 + 条件变量 + 停止标志;

  • 三个关键细节:锁外执行任务、while 防虚假唤醒、置标志优雅退出

  • C 版适合嵌入式 C 工程,C++ 版利用标准库更省心(future 拿结果、RAII 自动清理);

  • 面试重点:核心参数、线程数估算、关闭流程、有界 / 无界队列。

下一篇可以写:线程池进阶(动态扩容、任务优先级、超时取消)或进程池实现。欢迎评论区点菜 😄

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2301_80672443/article/details/163513694

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--