从零手写线程池:把多线程、锁、同步、日志讲透

经常写 Linux 多线程代码就会发现:线程开多了扛不住,反复创建销毁太浪费,任务来了再临时起线程又太慢。

真正能打的服务,几乎都离不开线程池

但很多人一上手就被一堆概念绕晕:互斥、临界区、条件变量、生产者消费者、单例、线程安全...... 看着就头大。

其实线程池一点都不玄乎,它就是一套 **"固定工人 + 任务排队 + 有人睡觉有人干活 + 安全退出"** 的工作模式。

今天我就用最通俗、最接地气的方式,带你从零撸一个带完整源码、带日志、带单例、能直接上线用 的 C++ 线程池。所有代码都是我亲手实现的,可直接编译运行。


一、先把话说明白:线程池到底是个啥?

你可以把线程池想象成一个小工厂

  • 工厂里提前雇好 5 个固定工人(线程)
  • 门口放一个 任务架子(任务队列)
  • 老板不断把任务放上去(生产者)
  • 工人轮流过来拿任务、做完再回来拿(消费者)
  • 没任务时,工人就在旁边睡觉等,不瞎转悠浪费电
  • 要关门下班时,先把架子上的任务做完,再统一下班

这就是线程池最朴素的本质:提前开好一批线程,循环等待任务,来了就做,没有就等,用完不销毁,反复利用。

它解决三件事:

  1. 避免频繁创建 / 销毁线程的巨大开销
  2. 控制线程数量,防止把系统拖崩
  3. 让任务处理更快、更稳定

二、整体架构:我写了哪些核心模块?

我把线程池拆成 5 个完全独立的头文件,结构清晰、低耦合、方便移植:

  1. Mutex.hpp ------ 互斥锁 + RAII 自动锁
  2. Cond.hpp ------ 条件变量(等待 / 唤醒)
  3. Thread.hpp ------ 线程类封装(创建、等待、分离、停止)
  4. Log.hpp ------ 策略模式日志系统(控制台 / 文件双输出)
  5. ThreadPool.hpp ------ 线程池本体(单例、任务队列、消费逻辑)

下面我会带着源码,一行一行讲清楚设计思路。


三、模块一:Mutex.hpp ------ 锁,就是线程的 "红绿灯"

多线程最容易乱的根源:大家同时抢一个共享资源 。我的解决方案:封装原生锁 + RAII 自动管理

复制代码
#pragma once
#include <iostream>
#include <pthread.h>

namespace yzq
{
    // 封装原生互斥锁
    class Mutex
    {
    public:
        Mutex() { pthread_mutex_init(&_mutex, nullptr); }
        void Lock() { pthread_mutex_lock(&_mutex); }
        void UnLock() { pthread_mutex_unlock(&_mutex); }
        pthread_mutex_t *Get() { return &_mutex; }
        ~Mutex() { pthread_mutex_destroy(&_mutex); }

    private:
        pthread_mutex_t _mutex;
    };

    // RAII 自动锁:创建加锁,析构解锁,绝对不会忘
    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex) : _mutex(mutex) { _mutex.Lock(); }
        ~LockGuard() { _mutex.UnLock(); }
    private:
        Mutex &_mutex;
    };
};

人话讲解

  • Mutex :对 pthread_mutex_t 做面向对象封装,提供加锁、解锁。
  • LockGuard :最关键的设计!代码块开始 → 自动加锁 代码块结束 → 自动解锁
  • 再也不用担心忘记解锁导致死锁

四、模块二:Cond.hpp ------ 让线程 "会睡觉、会被叫醒"

只有锁还不够:没任务时,线程不能一直while(1)空转,CPU 会直接 100%!

正确做法:没任务睡觉,有任务被唤醒

复制代码
#pragma once
#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"

using namespace yzq;

namespace yzq
{
    class Cond
    {
    public:
        Cond() { pthread_cond_init(&_cond, nullptr); }
        
        // 等待:会自动释放锁,被唤醒后重新持有锁
        void Wait(Mutex &mutex) { pthread_cond_wait(&_cond, mutex.Get()); }
        
        // 唤醒一个
        void Signal() { pthread_cond_signal(&_cond); }
        
        // 唤醒全部
        void Broadcast() { pthread_cond_broadcast(&_cond); }
        
        ~Cond() { pthread_cond_destroy(&_cond); }

    private:
        pthread_cond_t _cond;
    };
};

关键点

Wait 是线程同步的灵魂:

  • 调用时自动释放锁
  • 休眠等待
  • 被唤醒后自动重新获取锁

这一步错了,你的线程池永远写不对。


五、模块三:Thread.hpp ------ 把线程包装成 "好用的工具"

我不想每次都写 pthread_create,太麻烦。我直接封装一个线程类,支持:创建、启动、停止、分离、等待、设置名字。

复制代码
static uint32_t number = 1;
    class Thread
    {
        using func_t = std::function<void()>;
        void EnableDetach()
        {
            std::cout << "线程被分离了" << std::endl;
            _isdetach = true;
        }

        void EnableRunning()
        {
            _isrunning = true;
        }

        static void *Routine(void *args) // 用static修饰,因为如果是类内函数其第一个参数为this
        {
            Thread *self = static_cast<Thread *>(args);
            self->EnableRunning(); // 创建后直接运行
            if (self->_isdetach)
            {
                self->Detach();
            }
            pthread_setname_np(self->_tid, self->_name.c_str());
            self->_func(); // 回调
            return nullptr;
        }

    public:
        Thread(func_t func)
            : _tid(0), _isdetach(false), _isrunning(false), _res(nullptr), _func(func)
        {
            _name = "thread-" + std::to_string(number++);
        }

        // 启动线程
        bool Start()
        {
            if (_isrunning)
            {
                return false;
            }
            int n = pthread_create(&_tid, nullptr, Routine, this);
            if (n != 0) // 创建失败
            {
                std::cerr << "create thread error: " << strerror(n) << std::endl;
                return false;
            }
            else
            {
                std::cout << _name << " create success" << std::endl;
                return true;
            }
        }

        // 分离线程
        bool Detach()
        {
            if (_isdetach)
                return false;
            if (_isrunning)
                pthread_detach(_tid);
            EnableDetach();
            return true;
        }

        // 中止线程
        bool Stop()
        {
            if (!_isrunning)
                return false;
            int n = pthread_cancel(_tid);
            if (n != 0)
            {
                std::cerr << "create thread error: " << strerror(n) << std::endl;
                return false;
            }
            else
            {
                _isrunning = false;
                std::cout << _name << " stop" << std::endl;
                return true;
            }
        }

        // 回收线程
        bool Join()
        {
            if (_isdetach)
            {
                std::cout << "你的线程已经是分离的了,不能进行join" << std::endl;
                return false;
            }
            int n = pthread_join(_tid, &_res);
            if (n != 0)
            {
                std::cerr << "create thread error: " << strerror(n) << std::endl;
                return false;
            }
            else
            {
                std::cout << "join success" << std::endl;
                return true;
            }
        }

        std::string Name()
        {
            return _name;
        }

        ~Thread() {}

    private:
        pthread_t _tid;    // 线程号
        std::string _name; // 线程名
        bool _isdetach;    // 分离状态
        bool _isrunning;   // 运行状态
        void *_res;        // 线程返回值
        func_t _func;      // 线程执行的函数
    };

作用

  • 支持直接传入 lambda 表达式
  • 自动命名线程:thread-1、thread-2...
  • 状态管理:是否运行、是否分离

六、模块四:Log.hpp ------ 线程池的 "黑匣子"

没有日志的多线程程序 = 玄学调试。我用策略模式写了一套极简、强大、线程安全的日志。

核心设计

  • 日志等级:DEBUG / INFO / WARNING / ERROR / FATAL

  • 双输出策略:控制台 / 文件

  • 自动携带信息:时间、PID、文件名、行号

  • 使用超级简单LOG(INFO) << "线程启动成功";

    // 策略基类
    class LogStrategy {
    public:
    virtual void SyncLog(const std::string message) = 0;
    };

    // 控制台输出
    class ConsoleLogStrategy : public LogStrategy;
    // 文件输出
    class FileLogStrategy : public LogStrategy;

    // 全局日志对象
    Logger logger;

    // 宏定义,一行打印
    #define LOG(level) logger(level, FILE, LINE)

日志效果

复制代码
[2026-04-04 17:22:33] [INFO] [1234] [ThreadPool.hpp] [30] - start new thread success: thread-1

七、核心模块:ThreadPool.hpp ------ 真正的线程池

7.1 设计亮点

  1. 固定 5 个线程
  2. 任务队列std::queue
  3. 生产者 - 消费者模型
  4. 线程安全(锁 + 条件变量)
  5. 单例模式(全局唯一)
  6. 优雅退出(处理完任务再退出)
  7. 休眠线程计数(精准唤醒)

7.2 核心逻辑:工人线程怎么干活?

复制代码
void HandlerTask()
{
    while (true)
    {
        T t;
        {
            LockGuard lockguard(_mutex); // 自动加锁

            // 没任务 && 还在运行 → 睡觉
            while (_taskq.empty() && _isrunning)
            {
                _sleepernum++;
                _cond.Wait(_mutex); // 等任务
                _sleepernum--;
            }

            // 没任务 && 要退出 → 下班
            if (_taskq.empty() && !_isrunning)
            {
                break;
            }

            // 取任务
            t = _taskq.front();
            _taskq.pop();
        }

        t(); // 【关键】在锁外执行任务!
    }
}

为什么要在锁外执行任务?

锁只保护队列访问 ,不保护任务执行 。如果锁内执行,就变成串行执行,并发效率直接归零!


7.3 提交任务:老板放任务

复制代码
bool Enqueue(const T &in)
{
    LockGuard lockguard(_mutex);
    if (_isrunning)
    {
        _taskq.push(in);
        // 如果所有线程都在睡觉,叫醒一个来干活
        if (_threads.size() == _sleepernum)
            WakeUpOne();
        return true;
    }
    return false;
}

7.4 单例模式:全局只有一个线程池

复制代码
static ThreadPool *GetInstance()
{
    LockGuard lockguard(_lock);
    if (inc == nullptr)
    {
        inc = new ThreadPool<T>();
        inc->Start();
    }
    return inc;
}

保证:

  • 全局唯一
  • 线程安全
  • 懒加载(用到才创建)

八、运行效果(日志实录)

复制代码
[2026-04-04 17:30:00] [INFO] [14567] [ThreadPool.hpp] [30] - start new thread success: thread-1
[2026-04-04 17:30:00] [INFO] [14567] [ThreadPool.hpp] [30] - start new thread success: thread-2
[2026-04-04 17:30:01] [DEBUG] [14567] [main.cpp] [20] - 获取单例
[2026-04-04 17:30:01] [INFO] [14567] [ThreadPool.hpp] [50] - 唤醒单个休眠线程
[2026-04-04 17:30:12] [INFO] [14567] [ThreadPool.hpp] [90] - thread-5 退出了, 线程池退出&&任务队列为空

线程池从来不是什么高大上的黑魔法。

它就是:一堆线程 + 一个队列 + 一把锁 + 一个唤醒机制 + 一套日志 + 一个单例。

想要亲手尝试去深入理解看前往我的gitee:https://gitee.com/fantasy55/linux/tree/master/thread_pool了解具体过程

相关推荐
_MyFavorite_5 小时前
JAVA重点基础、进阶知识及易错点总结(28)接口默认方法与静态方法
java·开发语言·windows
取码网5 小时前
最新在线留言板系统PHP源码
开发语言·php
环黄金线HHJX.5 小时前
龙虾钳足启发的AI集群语言交互新范式
开发语言·人工智能·算法·编辑器·交互
旖-旎5 小时前
分治(快速选择算法)(3)
c++·算法·leetcode·排序算法·快速选择
不写八个5 小时前
PHP教程006:ThinkPHP项目入门
开发语言·php
_MyFavorite_5 小时前
JAVA重点基础、进阶知识及易错点总结(31)设计模式基础(单例、工厂)
java·开发语言·设计模式
xiaoye-duck5 小时前
【C++:哈希表封装】哈希表封装 myunordered_map/myunordered_set 实战:底层原理 + 完整实现
数据结构·c++·散列表
A.A呐6 小时前
【C++第二十三章】C++11
开发语言·c++
智算菩萨6 小时前
【Pygame】第8章 文字渲染与字体系统(支持中文字体)
开发语言·python·pygame
014-code6 小时前
Java SPI 实战:ServiceLoader 的正确打开方式(含类加载器坑)
java·开发语言