Linux C++ 高并发编程:从原理到手撕,线程池全链路深度解析


🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:


文章目录

  • 前言:
  • [一. 池化技术与线程池:为什么我们需要线程池?](#一. 池化技术与线程池:为什么我们需要线程池?)
    • [1.1 池化技术的核心思想](#1.1 池化技术的核心思想)
    • [1.2 线程池的核心定义](#1.2 线程池的核心定义)
    • [1.3 线程池的核心优势](#1.3 线程池的核心优势)
    • [1.4 典型应用场景](#1.4 典型应用场景)
  • [二. 线程池的核心设计原理:本质是生产者消费者模型](#二. 线程池的核心设计原理:本质是生产者消费者模型)
    • [2.1 线程池的核心组成](#2.1 线程池的核心组成)
    • [2.2 线程池的核心运行流程](#2.2 线程池的核心运行流程)
    • [2.3 核心设计要点](#2.3 核心设计要点)
  • [三. 手撕线程池:C++ 源码深度解析](#三. 手撕线程池:C++ 源码深度解析)
    • [3.1 基础组件:RAII 风格的互斥锁与条件变量封装](#3.1 基础组件:RAII 风格的互斥锁与条件变量封装)
      • [3.1.1 互斥锁与锁守卫封装](#3.1.1 互斥锁与锁守卫封装)
      • [3.1.2 条件变量封装](#3.1.2 条件变量封装)
    • [3.2 线程池核心类实现](#3.2 线程池核心类实现)
    • [3.3 线程池使用示例(ThreadPool_v1完整版)](#3.3 线程池使用示例(ThreadPool_v1完整版))
  • [四. 进阶优化:线程安全的单例模式线程池(ThreadPool_v2)](#四. 进阶优化:线程安全的单例模式线程池(ThreadPool_v2))
    • [4.1 单例模式的核心要求](#4.1 单例模式的核心要求)
    • [4.2 饿汉模式 vs 懒汉模式](#4.2 饿汉模式 vs 懒汉模式)
    • [4.3 线程安全的懒汉单例线程池(双检锁 DCL)](#4.3 线程安全的懒汉单例线程池(双检锁 DCL))
    • [4.4 单例线程池使用示例](#4.4 单例线程池使用示例)
  • [五. 线程池背后的核心安全问题](#五. 线程池背后的核心安全问题)
    • [5.1 线程安全与函数可重入](#5.1 线程安全与函数可重入)
    • [5.2 死锁:多线程编程的头号杀手](#5.2 死锁:多线程编程的头号杀手)
    • [5.3 STL 容器与智能指针的线程安全](#5.3 STL 容器与智能指针的线程安全)
    • [5.4 常见锁概念拓展](#5.4 常见锁概念拓展)
  • 结尾:

前言:

在 Linux 后端高并发开发场景中,我们经常会遇到这样的问题:WEB 服务器每秒要处理上千次客户端请求,日志系统需要异步写入海量数据,批量计算任务需要并行执行。如果每次处理任务都临时创建线程,不仅会带来巨大的线程创建 / 销毁系统开销,还可能因峰值期创建大量线程导致 CPU 调度过载、甚至系统 OOM。池化技术正是为了解决这类问题而生,而线程池就是池化思想在多线程编程中最经典的工程落地。它通过提前创建一批固定数量的工作线程,统一管理任务队列,让用户任务被复用的线程异步执行,从根本上解决了频繁创建线程的开销问题,同时实现了对并发数的精准控制。本文将从线程池的核心原理出发,带你手撕工业级 C++ 线程池的完整实现,再到单例模式的进阶优化,最后深入拆解线程池背后的线程安全、死锁、锁机制等核心知识点,帮你彻底掌握 Linux 高并发编程的这一核心技能。


一. 池化技术与线程池:为什么我们需要线程池?

1.1 池化技术的核心思想

池化技术的本质是 「提前申请、重复利用、统一管理」,和我们生活中的「预制菜」「共享单车」逻辑完全一致:

  • 提前申请资源,避免临时申请的开销;
  • 资源重复利用,最大化资源利用率;
  • 统一管理资源,避免无节制申请导致系统过载。

除了线程池,我们熟知的进程池、内存池、连接池、对象池,都是池化思想的落地实现。

1.2 线程池的核心定义

线程池是一种线程使用模式:程序启动时提前创建一批固定数量的工作线程,这些线程循环从任务队列中获取用户投递的任务并执行;用户无需关心线程的管理细节,只需将任务投递到线程池即可异步执行。

1.3 线程池的核心优势

优势 详细说明
降低系统开销 避免了线程频繁创建和销毁带来的 CPU、内存开销,尤其适合短任务场景
提升响应速度 任务到达时直接复用已有线程执行,无需等待线程创建的耗时,大幅降低任务延迟
控制并发上限 限制工作线程的最大数量,避免大量线程抢占 CPU 导致的调度颠簸,保证系统稳定性
统一线程管理 对工作线程进行统一的分配、调优、监控和异常处理,降低业务代码的复杂度

1.4 典型应用场景

  • 短任务高并发场景:WEB 服务器请求处理、网关接口转发、RPC 调用处理
  • 异步非核心任务:日志异步写入、数据统计上报、消息推送
  • 批量并行计算:大数据处理、图片 / 视频批量处理、模型推理批量任务
  • 低延迟响应服务:对客户端请求延迟敏感的后端服务,如交易系统、即时通讯服务

二. 线程池的核心设计原理:本质是生产者消费者模型

线程池的底层逻辑,就是一个标准的多生产者 - 多消费者模型

  • 生产者:用户线程,向任务队列投递待执行的任务;
  • 消费者:线程池内的工作线程,循环从任务队列中获取任务并执行;
  • 交易场所:任务队列,是整个模型的核心临界资源,必须保证并发访问的线程安全。


2.1 线程池的核心组成

一个完整的线程池,由四大核心模块构成:

  1. 任务队列:存储用户投递的待执行任务,通常用队列实现,是线程池的核心临界资源;
  2. 工作线程组:提前创建的固定数量的工作线程,循环竞争任务队列中的任务执行;
  3. 同步互斥机制:互斥锁保护任务队列的并发访问,条件变量实现线程的等待与唤醒,解决生产者与消费者的同步问题;
  4. 线程池状态管理:控制线程池的初始化、运行、停止状态,实现优雅退出,避免任务丢失。


2.2 线程池的核心运行流程

  1. 初始化阶段:创建固定数量的工作线程,初始化互斥锁、条件变量、任务队列,设置线程池为运行状态;
  2. 任务投递阶段:用户线程加锁后将任务推入任务队列,若有线程处于等待状态,则唤醒对应的工作线程;
  3. 任务执行阶段:工作线程循环竞争任务队列,队列为空时进入条件变量休眠;被唤醒后加锁获取任务,解锁后在临界区外执行任务;
  4. 优雅退出阶段:设置线程池为停止状态,唤醒所有等待的工作线程;工作线程处理完任务队列中剩余的所有任务后,正常退出;主线程等待所有工作线程回收后,释放线程池资源。

2.3 核心设计要点

  1. 任务执行必须在临界区外:工作线程取到任务后立即释放锁,任务执行是线程私有行为,不占用临界区,最大化提升并发度;
  2. 条件变量必须用 while 循环判断:防止操作系统的伪唤醒,保证线程被唤醒后一定会重新检查任务队列是否有任务,避免程序异常;
  3. 优雅退出的双条件判断:只有当「线程池停止运行」且「任务队列为空」时,工作线程才能退出,保证所有已投递的任务都会被执行完毕,不会出现任务丢失;
  4. 唤醒逻辑优化:只有当有线程处于等待状态时,才发送唤醒信号,避免无效的系统调用,提升程序性能。

三. 手撕线程池:C++ 源码深度解析

我们将基于 Linux 原生 pthread 库,用 C++ 实现工业级线程池,先封装基础的同步互斥组件,再实现线程池核心逻辑,保证代码的可复用性、健壮性和高性能。

3.1 基础组件:RAII 风格的互斥锁与条件变量封装

RAII(资源获取即初始化)是 C++ 管理资源的核心思想,利用对象的生命周期自动管理资源的申请与释放,彻底避免资源泄漏。

3.1.1 互斥锁与锁守卫封装

cpp 复制代码
#ifndef MUTEX_HPP
#define MUTEX_HPP

#include <iostream>
#include <pthread.h>

/**
 * @brief 互斥锁封装类 (The Wrapper Pattern)
 * 将原生 pthread_mutex_t 及其相关操作封装进 C++ 类中
 * 优点:利用构造/析构函数自动初始化资源,降低直接调用底层接口的心智负担
 */
class Mutex
{
public:
    // 构造函数:初始化互斥锁
    Mutex()
    {
        // 这里的 nullptr 表示使用默认的互斥锁属性(非递归、不检测死锁等)
        pthread_mutex_init(&_lock, nullptr);
    }
    
    // 析构函数:销毁互斥锁
    ~Mutex()
    {
        /** * 注意:销毁一个正处于加锁状态或仍有线程在等待的锁会导致未定义行为
         * 封装在析构函数中可以确保当 Mutex 对象生命周期结束时,相关资源被内核正确回收
         */
        pthread_mutex_destroy(&_lock);
    }
    
    // 加锁操作
    void Lock()
    {
        // 若锁已被占用,调用线程将阻塞在此处,进入等待队列
        pthread_mutex_lock(&_lock);
    }
    
    // 解锁操作
    void UnLock()
    {
        // 唤醒在该互斥锁上等待的线程
        pthread_mutex_unlock(&_lock);
    }
    
    // 获取原始互斥锁指针,用于需要原生 pthread_mutex_t 的接口
    // 常见场景:作为 pthread_cond_wait 的参数使用
    pthread_mutex_t* Origin()
    {
        return &_lock;
    }

private:
    pthread_mutex_t _lock;  // POSIX 互斥锁,临界资源访问控制的核心引擎
    
    /** * 补充建议:在实际工程中,通常需要禁用 Mutex 的拷贝构造和赋值
     * 因为物理意义上的"锁"在系统中应该是唯一的,不应被克隆。
     * Mutex(const Mutex&) = delete; 
     */
};

/**
 * @brief RAII 风格的锁守卫类 (LockGuard)
 * 核心逻辑:Resource Acquisition Is Initialization (资源获取即初始化)
 * 作用:解决"忘记解锁"的问题。无论是正常退出、函数 return,还是抛出异常,
 * 只要局部变量 LockGuard 出了作用域,其析构函数必然会被调用,从而自动解锁。
 */
class LockGuard
{
public:
    // 构造函数:接收一个 Mutex 指针,并立即加锁
    // 使用指针是为了能在类外部灵活地管理由同一个 Mutex 保护的不同临界区
    LockGuard(Mutex* lockptr) : _lockptr(lockptr)
    {
        // 实现"一构造就加锁"的自动化语义
        _lockptr->Lock();
    }
    
    // 析构函数:自动解锁
    ~LockGuard()
    {
        // 实现"一销毁就解锁"的自动化语义,保障了代码的异常安全性 (Exception Safety)
        _lockptr->UnLock();
    }

private:
    Mutex* _lockptr;  // 维护一个指向 Mutex 的指针,负责在生命周期结束时调用其接口
};
#endif

代码解析

  • Mutex类完整封装了 pthread 互斥量的初始化、加锁、解锁、销毁全生命周期,禁用拷贝避免未定义行为;
  • LockGuard是 RAII 的核心实现,利用栈对象的生命周期自动管理锁,彻底避免了手动解锁的遗漏,即使临界区内代码抛出异常,也能保证锁被正确释放。

3.1.2 条件变量封装

cpp 复制代码
#ifndef COND_HPP
#define COND_HPP

#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"

/**
 * @brief 条件变量封装类
 * 核心逻辑:提供线程间的通知机制。
 * 它允许线程在某些条件不满足时挂起,并在其他线程改变条件并发送信号时被唤醒。
 */
class Cond 
{
public:
    // 构造函数:初始化条件变量
    Cond()
    {
        // nullptr 表示使用操作系统默认的条件变量属性
        pthread_cond_init(&cond, nullptr);
    }

    /**
     * @brief 等待条件满足
     * @param mutex 必须是当前线程已经持有的互斥锁
     * * 底层逻辑"三步跳":
     * 1. 自动释放传入的 mutex 锁(这样其他线程才能修改临界资源)。
     * 2. 将当前线程挂起并加入到该条件变量的等待队列中。
     * 3. 当被唤醒返回时,会自动尝试重新竞争并持有该 mutex 锁。
     */
    void Wait(Mutex &mutex)
    {
        // 调用封装好的 Mutex 类的 Origin() 接口,配合底层 C 接口使用
        pthread_cond_wait(&cond, mutex.Origin());
    }

    // 唤醒一个在此条件变量下等待的线程
    void NotifyOne()
    {
        // 唤醒队列中的第一个线程(如果存在)
        pthread_cond_signal(&cond);
    }

    // 唤醒所有在此条件变量下等待的线程
    void NotifyAll()
    {
        // 广播通知,常用于多个消费者或复杂的资源变动场景
        pthread_cond_broadcast(&cond);
    }

    // 析构函数:销毁条件变量资源
    ~Cond()
    {
        /**
         * 注意事项:
         * 销毁一个仍有线程在等待的条件变量是危险行为。
         * 在线程池销毁前,通常需要先调用 NotifyAll 并回收所有线程。
         */
        pthread_cond_destroy(&cond);
    }

private:
    pthread_cond_t cond; // POSIX 线程库提供的底层条件变量结构
};

#endif

代码解析

  • 条件变量的核心作用是实现线程间的同步,避免任务队列为空时工作线程 CPU 空转;
  • Wait函数必须和互斥锁配合使用,因为「条件判断」和「进入等待」必须是原子操作,避免解锁后、等待前信号丢失导致线程永久阻塞;
  • NotifyOne用于任务入队时唤醒单个工作线程,NotifyAll用于线程池退出时唤醒所有等待线程。

3.2 线程池核心类实现

我们用模板类实现线程池,支持任意可调用对象作为任务,先定义任务类型,封装线程再实现线程池核心逻辑。其中我们还使用了日志类,这个我们上一篇刚封装完,并且有点长,这里就不再次展示了。

3.2.1 线程类定义

cpp 复制代码
#ifndef __THREAD_HPP
#define __THREAD_HPP

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>

// 定义线程执行的任务类型,使用包装器增强灵活性
using func_t = std::function<void()>;

// 线程状态枚举:用于构建简单的状态机,确保护法操作
enum class TSTAYUS
{
    THREAD_NEW,     // 新建状态
    THREAD_RUNNING, // 运行状态
    THREAD_STOPPED, // 停止/退出状态
};

// 这个是有点bug的:全局静态变量在多线程并发创建对象时存在"竞态条件"
// 多个线程可能同时执行 gunm++,导致线程编号重复,生产环境下建议使用 std::atomic<int>
static int gunm = 1;

class Thread
{
private:
    // 获取所属进程的 PID
    void get_pid()
    {
        _pid = getpid();
    }
    // 获取内核级线程 ID (LWP ID),这才是 Linux 系统监控(如 top -H)看到的真正 ID
    void get_lwid()
    {
        // 原生 pthread 库没有直接获取 LWP 的接口,必须通过系统调用
        _lwid = syscall(SYS_gettid);
    }

    /**
     * @brief 静态成员函数作为线程入口点
     * 关键逻辑:pthread_create 要求回调函数必须是 void* (*)(void*)
     * 类的普通成员函数隐含 this 指针,参数不匹配,故必须设为 static。
     * 通过传入 args (this 指针) 重新找回对象上下文。
     */
    static void* routine(void* args)
    {
        Thread* ts = static_cast<Thread*>(args);
        ts->get_pid();
        ts->get_lwid();
        
        // 为线程设置名字,方便在调试器(如 gdb)中识别
        pthread_setname_np(pthread_self(), ts->Name().c_str());
        
        // 执行用户真正传入的任务
        ts->_func();
        return nullptr;
    }

public:
    // 构造函数:完成任务绑定与命名,此时线程尚未在内核中创建
    Thread(func_t f) : _func(f), _joinable(true), _status(TSTAYUS::THREAD_NEW)
    {
        _name = "Worker-" + std::to_string(gunm++);
    }

    // 启动线程:正式调用底层接口
    void start()
    {
        if(_status == TSTAYUS::THREAD_RUNNING)
        {
            std::cerr << "thread is already running" << std::endl;
            return;
        }
        
        // 传入 this 作为 routine 的参数,实现 C 到 C++ 的跨越
        int n = pthread_create(&_tid, nullptr, routine, this);
        if(n != 0)
        {
            std::cerr << "pthread_create failed" << std::endl;
        }
        _status = TSTAYUS::THREAD_RUNNING;
    }

    // 停止线程:通过发送取消请求
    void stop()
    {
        if(_status == TSTAYUS::THREAD_RUNNING)
        {
            // pthread_cancel 是比较暴力的退出方式,依赖线程内部是否存在取消点
            int n = pthread_cancel(_tid);
            if(n != 0)
            {
                std::cerr << "pthread_cancel failed" << std::endl;
            }
            _status = TSTAYUS::THREAD_STOPPED;
        }
        else 
        {
            std::cerr << "thread status is : THREAD_STOPPED or THREAD_NEW" << std::endl;
            return;
        }
    }

    // 资源回收:阻塞等待线程结束
    void join()
    {
        if(_joinable)
        {
            // 只有处于 joinable 状态的线程才需要被 join,否则会产生资源泄露
            int n = pthread_join(_tid, nullptr);
            if(n != 0)
            {
                std::cerr << "pthread_join failed" << std::endl;
            }
            printf("lwp: %d, name: %s, join success\n", _lwid, _name.c_str());
        }
        else {
            printf("lwp: %d, name: %s, join failed, because thread is detached\n", _lwid, _name.c_str());
        }
    }

    // 线程分离:将线程设置为由系统自动回收
    void detach()
    {
        if(_joinable && _status == TSTAYUS::THREAD_RUNNING)
        {
            _joinable = false;
            // 分离后,该线程退出时会自动释放所有资源,无需 join
            int n = pthread_detach(_tid);
            if(n != 0)
            {
                std::cerr << "pthread_detach failed" << std::endl;
            }
        }
    }

    // 获取线程名称接口
    std::string Name()
    {
        return _name;
    }

    ~Thread()
    {
        // 析构函数中未做强制 join,这是为了给使用者留出控制权
        // 但要注意,如果对象销毁时线程还在跑且未 detach,可能会导致程序崩溃
    }

private:
    pthread_t _tid;      // 线程库层面的 ID (用户层 ID)
    pid_t _pid;          // 所属进程 ID
    pid_t _lwid;         // 轻量级进程 ID (内核层真正的线程 ID)
    std::string _name;   // 线程可读性名称
    func_t _func;        // 线程执行的任务包装器
    bool _joinable;      // 是否允许被等待标记
    TSTAYUS _status;     // 当前线程状态机
};

#endif

3.2.2 任务类型定义

cpp 复制代码
#pragma once

#include <iostream>
#include <functional>
#include <pthread.h>
#include "Logger.hpp"

/**
 * @brief 任务类型包装器
 * 使用 std::function 实现类型擦除 (Type Erasure)
 * 优点:线程池不需要知道具体任务的细节,只要是"无参无返回值"的调用对象(函数指针、Lambda、仿函数)
 * 都可以被封装进 task_t,这极大增强了线程池的通用性。
 */
using task_t = std::function<void()>;

using namespace LogModule;

// 1. 全局函数
/**
 * @brief 示例任务1:模拟 IO/打印型任务
 * 重点在于展示如何在任务内部识别当前正在干活的线程。
 */
void task1()
{
    char name[64];
    // pthread_getname_np 是 Linux 特有的接口,用于获取线程的别名(在 Thread.hpp 中通过 pthread_setname_np 设置)
    // 这在多线程调试时非常关键,能帮你确定任务是否在预期的 Worker 线程中执行。
    pthread_getname_np(pthread_self(), name, sizeof(name));
    
    LOG(LogLevel::DEBUG) << "执行任务1: 打印消息 |" << name << "|";
}

/**
 * @brief 示例任务2:模拟计算型任务
 * 模拟一个简单的算术逻辑处理。
 */
void task2()
{
    char name[64];
    // 每一个任务被执行时,实际上都是在某个 Worker 线程的调用栈中运行。
    pthread_getname_np(pthread_self(), name, sizeof(name));
    
    LOG(LogLevel::DEBUG) << "执行任务2: 计算 1+1 = " << 1 + 1 << " |" << name << "|";
}

3.2.3 线程池核心类框架(大致接口有那些)

cpp 复制代码
#ifndef THREADPOOL_HPP
#define THREADPOOL_HPP

#include <vector>
#include <queue>
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"

/**
 * @brief 线程池核心类 (基于生产者-消费者模型设计)
 * 采用模板类 T,以支持不同类型的任务逻辑(通常为 std::function<void()>)
 */
template<typename T>
class ThreadPool 
{
private: 
    // 【私有接口:内部逻辑支撑】
    
    // 检查任务队列是否为空
    bool IsEmptyQueue();

    // 辅助函数:从队列中获取一个任务(封装 pop 动作)
    T PopHelper();

    /**
     * @brief 线程执行流入口 (核心死循环)
     * 内部包含:加锁、条件变量等待、任务获取、任务执行、状态检测
     */
    void ThreadRoutine();

public:
    // 【公有接口:外部操作指南】

    /**
     * @brief 构造函数
     * @param num 预创线程的数量,默认为 5
     * 职责:初始化成员变量,并预分配 Thread 对象
     */
    ThreadPool(int num = 5);

    /**
     * @brief 启动线程池
     * 职责:将状态改为运行中,并真正调用每个 Thread 的 start() 方法创建内核线程
     */
    void Start();

    /**
     * @brief 生产者接口:提交任务
     * @param task 待执行的任务对象
     * 职责:加锁入队,并唤醒(Notify)正在休眠的消费者线程
     */
    void Enqueue(const T& task);

    /**
     * @brief 温和关闭线程池
     * 职责:修改运行状态,并广播(NotifyAll)所有线程,确保积压任务处理完后线程能正常退出
     */
    void Stop();

    /**
     * @brief 资源回收接口
     * 职责:循环调用线程对象的 join(),确保主线程在子线程彻底回收后再退出
     */
    void Wait();

    // 析构函数
    ~ThreadPool();

private:
    // 【核心资源:状态与同步控制】
    
    std::vector<Thread> _threads; // 线程"工人"管理数组
    int _num;                    // 预设线程规模
    bool _isrunning;             // 运行状态标识位(核心状态机)
    int _sleeper_cnt;            // 统计当前处于 Wait 状态的线程数

    std::queue<T> _queue;        // 任务队列:充当生产者与消费者之间的"交易场所"
    Mutex _mutex;                // 互斥锁:保证队列操作的原子性
    Cond _cond;                  // 条件变量:实现线程间的同步通知
};

#endif

成员变量解析

  • _queue:任务队列,是生产者和消费者的核心共享资源,所有访问必须加锁保护;
  • _mutex:互斥锁,保护任务队列、_sleeper_cnt_isrunning所有共享资源的并发访问;
  • _sleeper_cnt:记录等待线程数量,用于优化唤醒逻辑,只有当有线程等待时才发送唤醒信号,避免无效系统调用;
  • _isrunning:线程池运行状态标志,控制工作线程的运行与退出,实现优雅关闭。

3.2.4 核心成员函数实现

线程池初始化与启动
cpp 复制代码
public:
    /**
     * @brief 线程池构造函数
     * @param num 指定线程池中初始线程的数量
     * 职责:完成成员变量初始化,并预建 Thread 对象容器。
     */
    ThreadPool(int num = gDefaultCnt): _num(num), _isrunning(false), _sleeper_cnt(0)
    {
        for(int i = 0; i < num; i++)
        {
            // 利用lambda表达式捕捉this指针,不然会出现参数不匹配的问题
            // 跟Thread.hpp中有关系
            /**
             * 深度解析:
             * 1. 桥接作用:Thread 类期待一个 func_t (void()),而 ThreadRoutine 是成员函数。
             * 2. 闭包特性:通过 [this] 捕获当前对象的地址,使得 lambda 体内可以访问私有成员 ThreadRoutine。
             * 3. 性能优化:emplace_back 直接在 vector 内存中构造 Thread 对象,避免了额外的拷贝或移动开销。
             */
            _threads.emplace_back([this](){
                this->ThreadRoutine();
            });
        }
    }

    /**
     * @brief 启动线程池
     * 职责:将池子状态设为运行,并让底层的每一个 pthread 真正跑起来。
     */
    void Start()
    {
        // 使用 LockGuard 确保 Start 操作的原子性
        // 防止在多线程环境下该线程池被多次重复调用 Start() 导致逻辑混乱
        LockGuard lockGuard(&_mutex);

        // 幂等性检查:如果当前处于运行状态,直接返回,确保 Start 只能成功执行一次
        if(_isrunning)
            return;

        // 状态翻转:标记池子已进入服务状态
        _isrunning = true;

        // 遍历管理容器,逐个调用 Thread 类的 start() 封装,触发 pthread_create
        for(auto& thread: _threads)
            thread.start();
            
        // 启发:此时所有子线程将竞相进入 ThreadRoutine 的 while(true) 循环
    }

代码解析

  • 类的非静态成员函数有隐式的 this 指针,无法直接作为 pthread 的回调函数,因此用 lambda 表达式捕获 this 指针,将类实例传入回调,再调用成员函数;
  • 构造函数内仅创建线程对象,Start设置运行状态,分离初始化和启动逻辑,方便线程池的生命周期管理。
工作线程核心例程(线程池灵魂)
cpp 复制代码
private: 
    /**
     * @brief 检查队列状态
     * 注意:该函数虽为私有,但被 ThreadRoutine 调用时必须处于互斥锁的保护下。
     */
    bool IsEmptyQueue()
    {
        return _queue.empty();
    }

    /**
     * @brief 提取任务辅助函数
     * 职责:封装从 STL 队列中获取并移除任务的动作。
     * 底层细节:STL 容器非线程安全,调用此函数前必须确保当前线程已持有锁。
     */
    T PopHelper()
    {
        T t = _queue.front();
        _queue.pop();
        return t;
    }

    /**
     * @brief 线程的核心执行回路 (Worker Loop)
     * 每个线程启动后,都会在这个 while(true) 中度过余生,直到池子关闭。
     */
    void ThreadRoutine()
    {
        char name[64];
        // 获取线程名,用于日志输出,方便追踪是哪个"工人"在干活
        pthread_getname_np(pthread_self(), name, sizeof(name));

        while(true)
        {
            T task; // 定义局部任务对象,用于从队列中拷贝任务到本地执行流
            
            // 临界区作用域开始:保证对任务队列的操作是原子的
            {
                LockGuard lockGuard(&_mutex); // 加锁保护,RAII 机制确保出了这个花括号自动解锁
                
                // 1. 任务队列为空 && 线程处于运行状态(不退出) -- 允许休眠
                /**
                 * 深度解析:
                 * 为什么要用 while 而不是 if?
                 * 答:为了应对"虚假唤醒"。即便被唤醒,醒来第一件事必须是再次检查条件,
                 * 确保真的有任务可领,否则继续睡。
                 */
                while(IsEmptyQueue() && _isrunning) // 防止伪唤醒
                {
                    LOG(LogLevel::DEBUG) << "没有任务,线程休眠: " << "|" << name << "|";
                    _sleeper_cnt++;    // 进入休眠状态前,计数器自增
                    _cond.Wait(_mutex); // 核心动作:原子解锁并挂起;被唤醒后自动重新加锁
                    _sleeper_cnt--;    // 被唤醒后,计数器自减
                    LOG(LogLevel::DEBUG) << "有任务,线程唤醒: " << "|" << name << "|";
                }

                // 2. 任务队列为空 && 线程不处于运行状态(要退出) -- 允许退出
                /**
                 * 优雅退出的关键判断:
                 * 只有当"不想跑了"且"活儿都干完了",线程才 break 跳出循环。
                 * 这样保证了即使调用了 Stop,队列里的存量任务依然能被处理完。
                 */
                if(IsEmptyQueue() && !_isrunning)
                {
                    LOG(LogLevel::INFO) << "Thread: " << name << " quit";
                    break; 
                }

                // 3. 任务队列不为空 && 线程处于运行状态(不退出)   -- 要先处理完任务
                //    任务队列不为空 && 线程不处于运行状态(要退出) -- 要先处理完任务
                // 到这里了肯定是有任务:执行真正的"领任务"动作
                task = PopHelper();
            } // 临界区作用域结束,lockGuard 析构,释放互斥锁

            /**
             * 核心性能考量点:
             * task() 任务处理放在临界区外面来执行。
             * 理由:任务执行通常很耗时,如果持锁执行,其他线程将无法领任务,
             * 整个线程池将退化为串行执行。释放锁后再处理,才是真正的并发。
             */
            task(); 
        }
    }

核心设计深度解析

  • while 循环防伪唤醒:操作系统可能会无故唤醒等待的线程(伪唤醒),用 while 循环会在唤醒后重新检查任务队列是否有任务,不满足则继续等待,保证程序健壮性,这是 pthread 条件变量的标准使用规范;
  • 优雅退出双条件判断 :只有当「线程池停止」且「任务队列为空」时,线程才会退出,保证所有已投递的任务都会被执行完毕,绝对不能用pthread_cancel强制终止线程,会导致任务执行中断、资源泄漏;
  • 任务执行在临界区外 :取出任务后,锁会在离开作用域时自动释放,耗时的任务执行完全不占用临界区,其他线程可以正常投递和获取任务,最大化并发度,这是线程池高性能的核心设计。

任务投递接口
cpp 复制代码
/**
     * @brief 生产者接口:下发任务
     * @param task 外部提交的任务对象(通常是一个回调包装器)
     * 职责:将任务推入队列,并按需唤醒等待中的"工人"线程。
     */
    void Enqueue(const T& task)
    {
        // 1. 加锁保护:任务队列 (_queue) 是临界资源,必须保证 push 操作的原子性
        LockGuard lockGuard(&_mutex);
        
        // 2. 状态判定:这不仅是逻辑检查,更是安全防线
        if(!_isrunning) // 如果当前处于停止状态,禁止继续加任务
            return;

        // 3. 任务入队:将任务拷贝/移动到 STL 队列中
        _queue.push(task);

        // 4. 唤醒机制:
        // 唤醒一个来线程来执行任务
        /**
         * 性能优化点:按需通知 (Selective Notification)
         * 只有当确实有线程在条件变量下挂起 (_sleeper_cnt > 0) 时,才调用 NotifyOne。
         * 理由:如果所有线程都在忙碌处理任务,调用 Notify 会产生无谓的内核系统调用开销。
         */
        if(_sleeper_cnt > 0)
            _cond.NotifyOne();

        // 进阶思考注释:
        // if(_sleeper_cnt > 0 && _queue.size() > _num)
        //     _cond.NotifyOne();
        /**
         * 关于你注释掉的这两行:
         * 这通常用于"批量唤醒"或"负载调节"尝试。
         * 在高并发场景下,有时候会积压一定数量的任务后再统一唤醒,或者根据队列长度决定唤醒频率,
         * 但对于基础线程池,目前的 NotifyOne 已经能保证最优的实时响应。
         */
    }

代码解析

  • 任务队列是临界资源,投递任务必须加锁,保证多线程并发投递的线程安全;
  • 线程池停止后禁止投递新任务,避免任务入队后线程已退出导致任务丢失;
  • 仅当有线程处于等待状态时才发送唤醒信号,避免无意义的系统调用,提升性能。


程池停止与资源回收
cpp 复制代码
/**
     * @brief 优雅关闭接口 (Graceful Shutdown)
     * 职责:向所有执行线程发布"下班"信号,并确保已有的存量任务有机会被处理。
     */
    void Stop()
    {
        // 加锁进入临界区,修改状态位和发送通知必须是原子的,防止错失信号
        LockGuard lockGuard(&_mutex);
        
        // 处于运行状态才有停止的必要
        if(_isrunning)
        {
            LOG(LogLevel::DEBUG) << "关闭线程池";
            
            // 状态翻转:这是逻辑上的"关门",Enqueue 接口将不再接受新任务
            _isrunning = false; // 将状态改为false;

            // 唤醒所有的去执行,因为可能停止了但是任务还没做完
            /**
             * 深度解析:
             * 为什么是 NotifyAll 而不是 NotifyOne?
             * 答:此时所有在 Wait 队列中的线程都必须醒来检查 _isrunning 状态。
             * 只有全部唤醒,它们才能意识到池子已经关闭,从而打破 while 循环走向退出路径。
             */
            if(_sleeper_cnt > 0)
                _cond.NotifyAll();

            // 这样的做法不好
            // for(auto& thread: _threads)
            //     thread.stop();
            /**
             * 补充注释:
             * 为什么手动调用 thread.stop (pthread_cancel) 不好?
             * 1. 暴力中断:可能导致线程正在处理的任务执行一半被强杀,造成数据不一致。
             * 2. 资源泄露:如果线程持有某些非 RAII 资源(如堆内存、文件描述符),直接 cancel 会导致这些资源无法释放。
             * 3. 阻塞风险:cancel 依赖取消点,不一定能立即见效。
             */
        }
    }

    /**
     * @brief 线程回收接口
     * 职责:主执行流阻塞等待所有子线程干完活并彻底退出。
     */
    void Wait()
    {
        // 这里就不加锁了,防止阻塞
        /**
         * 架构逻辑:
         * Wait() 通常紧随 Stop() 之后。
         * 不加锁的原因:pthread_join 本身就是阻塞式等待。如果此时持锁等待线程退出,
         * 而子线程在退出逻辑中恰好也需要这把锁(例如 ThreadRoutine 里的判断),就会产生死锁。
         */
        for(auto& thread: _threads)
            thread.join();
    }

    /**
     * @brief 析构函数
     * 在这个版本中为空,因为资源的释放逻辑被显式地放在了 Stop 和 Wait 中。
     */
    ~ThreadPool()
    {
        /**
         * 生产环境建议:
         * 可以在析构函数中检查 _isrunning。
         * 如果用户忘记调用 Stop/Wait,析构函数应主动介入,防止产生"僵尸线程"或对象销毁后的野指针访问。
         */
    }

代码解析

  • Stop函数修改线程池运行状态后,必须用NotifyAll唤醒所有等待的线程,让所有线程都能检查退出条件,避免部分线程永久休眠;
  • Wait函数循环调用pthread_join等待所有工作线程退出,保证主线程不会提前终止,导致进程退出、任务未执行完毕。


  • 我们没使用图中这种Wait()调用的方式,后面可以在单例模式中尝试一下

3.3 线程池使用示例(ThreadPool_v1完整版)

  • Threadpool.hpp
cpp 复制代码
#ifndef THREADPOOL_HPP
#define THREADPOOL_HPP

// 可以看到我们直接使用了很多之前自己造的轮子
#include <iostream>
#include <pthread.h>
#include <vector>
#include <queue>
#include "Thread.hpp"
#include "Logger.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
using namespace LogModule;

// 默认线程池大小,通常根据 CPU 核心数进行调整
const static int gDefaultCnt = 5;

/**
 * @brief 通用线程池模板类
 * @tparam T 任务类型,通常是一个可调用对象(如 std::function)
 */
template<typename T>
class ThreadPool 
{
private: 
    // 内部检查工具:判断队列是否为空,需在加锁环境下调用
    bool IsEmptyQueue()
    {
        return _queue.empty();
    }

    // 内部辅助工具:封装出队动作,减少主循环中的代码冗余
    T PopHelper()
    {
        T t = _queue.front();
        _queue.pop();
        return t;
    }

    /**
     * @brief 线程的核心工作循环 (Worker Routine)
     * 每个线程启动后都会陷入此函数的死循环中,直到池子关闭
     */
    void ThreadRoutine()
    {
        char name[64];
        // 获取线程名称,用于区分不同的 Worker 日志输出
        pthread_getname_np(pthread_self(), name, sizeof(name));
        
        while(true)
        {
            T task; // 线程本地的任务包装器
            
            // 临界区作用域:确保锁的粒度尽可能小,仅保护对共享队列的操作
            {
                LockGuard lockGuard(&_mutex); // RAII 自动加锁保护
                
                // 1. 任务队列为空 && 线程处于运行状态(不退出) -- 允许休眠
                /**
                 * 关键点:为什么用 while 而不是 if?
                 * 答:为了应对"虚假唤醒 (Spurious Wakeup)"。线程被唤醒后必须重新
                 * 检查条件,确保队列里真的有数据,否则必须继续休眠。
                 */
                while(IsEmptyQueue() && _isrunning) // 防止伪唤醒
                {
                    LOG(LogLevel::DEBUG) << "没有任务,线程休眠: " << "|" << name << "|";
                    _sleeper_cnt++;    // 记录进入休眠的线程数,优化生产者的通知策略
                    _cond.Wait(_mutex); // 核心动作:释放锁 -> 挂起 -> 被唤醒 -> 重新竞争锁
                    _sleeper_cnt--;    // 被唤醒并抢到锁后,计数自减
                    LOG(LogLevel::DEBUG) << "有任务,线程唤醒: " << "|" << name << "|";
                }

                // 2. 任务队列为空 && 线程不处于运行状态(要退出) -- 允许退出
                /**
                 * 优雅退出的逻辑:
                 * 只有当"池子关门"且"存量活儿干完"时,线程才正式退出。
                 * 如果队列还有任务,即使 _isrunning 为 false,也会走到下面的 PopHelper 继续干活。
                 */
                if(IsEmptyQueue() && !_isrunning)
                {
                    LOG(LogLevel::INFO) << "Thread: " << name << " quit";
                    break; 
                }

                // 3. 任务队列不为空 && 线程处于运行状态(不退出)   -- 要先处理完任务
                //    任务队列不为空 && 线程不处于运行状态(要退出) -- 要先处理完任务
                // 到这里了肯定是有任务
                task = PopHelper();
            } // 临界区结束:LockGuard 析构,释放互斥锁
            
            /**
             * 性能优化的核心:
             * task() 的处理放在临界区外部。
             * 理由:任务执行通常很耗时,如果持锁运行,线程池会退化为单线程。
             * 让其他线程能在该任务执行期间去竞争锁领新任务,实现真正的并发。
             */
            task(); 
        }
    }

public:
    /**
     * @brief 构造函数:初始化管理资源
     * 注意:此时内核线程尚未真正创建,只是在容器中预置了 Thread 对象
     */
    ThreadPool(int num = gDefaultCnt): _num(num), _isrunning(false), _sleeper_cnt(0)
    {
        for(int i = 0; i < num; i++)
        {
            // 利用lambda表达式捕捉this指针,不然会出现参数不匹配的问题
            // 跟Thread.hpp中有关系:将成员函数 ThreadRoutine 转换为 func_t 类型
            _threads.emplace_back([this](){
                this->ThreadRoutine();
            });
        }
    }

    /**
     * @brief 启动线程池服务
     * 职责:开启控制开关,并逐一触发线程创建
     */
    void Start()
    {
        LockGuard lockGuard(&_mutex);
        // 幂等性保护:防止线程池被多次重复 Start
        if(_isrunning)
            return;
        _isrunning = true; // 翻转运行状态
        for(auto& thread: _threads)
            thread.start(); // 封装了 pthread_create
    }

    /**
     * @brief 生产者接口:将任务加入队列
     */
    void Enqueue(const T& task)
    {
        LockGuard lockGuard(&_mutex);
        if(!_isrunning) // 如果当前处于停止状态,禁止继续加任务(拒绝服务策略)
            return;
        _queue.push(task);
        
        /**
         * 唤醒策略优化:
         * 唤醒一个来线程来执行任务。
         * 只有当确实有线程在睡觉时才发信号,减少无效的内核系统调用开销。
         */
        if(_sleeper_cnt > 0)
            _cond.NotifyOne();

        // 进阶优化提示:如果任务堆积过多,可以考虑 NotifyAll 或 动态增加线程
        // if(_sleeper_cnt > 0 && _queue.size() > _num)
        //     _cond.NotifyOne();
    }

    /**
     * @brief 停止线程池服务 (优雅停止)
     */
    void Stop()
    {
        LockGuard lockGuard(&_mutex);
        // 处于运行状态才有停止的必要
        if(_isrunning)
        {
            LOG(LogLevel::DEBUG) << "关闭线程池";
            _isrunning = false; // 1. 先修改状态位,切断 Enqueue 的入口

            // 2. 唤醒所有正在休眠的线程。
            // 它们醒来后会因为 _isrunning 为 false 且队列为空而 break。
            if(_sleeper_cnt > 0)
                _cond.NotifyAll();

            // 这样的做法不好:直接 cancel 会导致任务处理一半被强杀,产生不可控后果
            // for(auto& thread: _threads)
            //     thread.stop();
        }
    }

    /**
     * @brief 等待线程回收
     */
    void Wait()
    {
        // 这里不加锁:pthread_join 本身是阻塞的。
        // 如果在此加锁,会导致正在尝试退出的子线程因为竞争不到锁而无法完成逻辑。
        for(auto& thread: _threads)
            thread.join();
    }

    ~ThreadPool()
    {}

private:
    // 线程管理资源
    std::vector<Thread> _threads; // 管理线程对象的容器
    int _num;                     // 线程池设定的初始线程规模
    bool _isrunning;              // 线程池存活状态标识
    // int _status;               // 进阶:可标识 RUNNING, PAUSE, STOPPED 等精细状态
    int _sleeper_cnt;             // 实时记录正在等待条件变量的空闲线程数

    // 生产/消费的核心组件
    std::queue<T> _queue;         // 任务缓冲池
    Mutex _mutex;                 // 保护队列的互斥锁
    Cond _cond;                   // 协调生产者与消费者的条件变量
};

#endif
  • Main.cc
cpp 复制代码
#include "Logger.hpp"
#include "Task.hpp"
#include "Threadpool.hpp"
#include <memory>
#include <unistd.h>

/**
 * @brief 线程池应用示例 (The Driver Program)
 * 职责:作为"生产者"线程,负责初始化环境、下发任务并控制整体生命周期。
 */
int main()
{
    // 0. 初始化日志系统:开启控制台输出策略,这是我们观察多线程并发行为的"眼睛"
    ENABLE_CONSOLE_LOG_STRATEGY();

    // 1. 创建线程池对象:
    // 使用 std::unique_ptr 管理线程池,体现了现代 C++ 的 RAII 资源管理思想
    // task_t 是在 Task.hpp 中定义的 std::function<void()> 类型擦除包装器
    std::unique_ptr<ThreadPool<task_t>> tp = std::make_unique<ThreadPool<task_t>>();
    
    // 2. 启动线程池:
    // 此时底层会真正创建 5 个(默认值)Worker 线程,并让它们进入空闲休眠状态,等待任务
    tp->Start();

    // 3. 生产过程:主线程充当生产者角色
    int cnt = 10;
    while(cnt--)
    {
        // 打印当前循环状态,方便追踪生产进度
        LOG(LogLevel::DEBUG) << "-----------------------: " << cnt;
        
        // 模拟生产间隔:每秒投放一个任务,让日志打印不至于瞬间刷屏,方便观察
        sleep(1);
        
        // 向线程池投喂任务1:打印消息任务
        // Enqueue 会自动唤醒一个正在休眠的 Worker 线程来处理
        tp->Enqueue(task1); 

        sleep(1);
        
        // 向线程池投喂任务2:计算任务
        tp->Enqueue(task2);
    }

    /**
     * 4. 优雅停机协议:
     * 这是最能体现代码鲁棒性的地方
     */
    
    // 发出停止指令:
    // 将池子的 _isrunning 设为 false,并广播唤醒所有休眠线程。
    // 注意:此时队列里可能还有没做完的任务,线程会坚持把活儿干完再退出。
    tp->Stop();
    
    // 等待回收:
    // 主线程阻塞于此,直到所有 Worker 线程处理完残余任务并正常 join。
    // 这保证了程序退出时,没有任何"僵尸执行流"存在。
    tp->Wait();
    
    // unique_ptr 离开作用域,自动析构 ThreadPool 对象,内存安全释放。
    return 0;
}




四. 进阶优化:线程安全的单例模式线程池(ThreadPool_v2)

在实际的后端开发中,线程池通常是进程内全局唯一的资源,需要用单例模式保证整个程序中只有一个线程池实例,避免资源浪费和管理混乱。

4.1 单例模式的核心要求

  1. 构造函数私有化,外部无法直接创建对象;
  2. 禁用拷贝构造和赋值运算符,防止对象拷贝破坏单例;
  3. 提供全局唯一的实例获取接口,保证实例只被创建一次;
  4. 多线程环境下保证线程安全,避免并发创建多个实例。

4.2 饿汉模式 vs 懒汉模式

饿汉模式 :吃完饭立刻洗碗,程序启动时就创建实例,用的时候直接拿。优点是实现简单、天然线程安全;缺点是实例初始化耗时时会拖慢程序启动速度,即使不用也会占用资源。

懒汉模式 :吃完饭先不洗碗,下一顿用的时候再洗,核心是延时加载,第一次使用时才创建实例。优点是不影响程序启动速度,按需加载;缺点是多线程环境下需要解决线程安全问题。

工业级开发中,懒汉模式的使用更广泛,它不会影响服务的启动速度,符合后端服务的设计规范。


4.3 线程安全的懒汉单例线程池(双检锁 DCL)

双检锁(Double-Check Locking, DCL)是工业界最常用的线程安全懒汉单例实现,完美平衡了安全性和性能。旧版是一个"随用随建的任务工具",单例版是一个"全局唯一的任务调度中心"

cpp 复制代码
#ifndef THREADPOOL_HPP
#define THREADPOOL_HPP

// 可以看到我们直接使用了很多之前自己造的轮子
#include <iostream>
#include <memory>
#include <pthread.h>
#include <vector>
#include <queue>
#include "Thread.hpp"
#include "Logger.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
using namespace LogModule;

const static int gDefaultCnt = 5;

/**
 * @brief 线程池单例模板类
 * 采用了"懒汉模式"实现,即在第一次调用 GetInstance 时才进行实例化。
 */
template<typename T>
class ThreadPool 
{
private: 
    // 内部逻辑:判定队列状态
    bool IsEmptyQueue()
    {
        return _queue.empty();
    }
    
    // 内部逻辑:原子化提取任务(调用前需持有锁)
    T PopHelper()
    {
        T t = _queue.front();
        _queue.pop();
        return t;
    }

    /**
     * @brief 消费者核心执行流
     * 运行于子线程栈中,通过条件变量实现高效的任务等待与唤醒。
     */
    void ThreadRoutine()
    {
        char name[64];
        pthread_getname_np(pthread_self(), name, sizeof(name));
        while(true)
        {
            T task; // 任务对象
            // 临界区作用域:确保锁的持有时间最短化
            {
                LockGuard lockGuard(&_mutex); // 加锁保护
                
                // 1. 任务队列为空 && 线程处于运行状态(不退出) -- 允许休眠
                /**
                 * 深度解析:
                 * 此处的 while 循环不仅解决了"虚假唤醒",还配合单例模式
                 * 确保了多个子线程在竞争唯一任务队列时的逻辑严密性。
                 */
                while(IsEmptyQueue() && _isrunning) // 防止伪唤醒
                {
                    LOG(LogLevel::DEBUG) << "没有任务,线程休眠: " << "|" << name << "|";
                    _sleeper_cnt++;
                    _cond.Wait(_mutex); // 核心:释放锁 -> 挂起 -> 被唤醒 -> 重获锁
                    _sleeper_cnt--;
                    LOG(LogLevel::DEBUG) << "有任务,线程唤醒: " << "|" << name << "|";
                }

                // 2. 任务队列为空 && 线程不处于运行状态(要退出) -- 允许退出
                if(IsEmptyQueue() && !_isrunning)
                {
                    LOG(LogLevel::INFO) << "Thread: " << name << "quit";
                    break; 
                }

                // 3. 任务队列不为空,无论运行状态如何,都要提取任务处理
                task = PopHelper();
            }
            // 任务执行放在锁外,这是实现真正并发、避免线程池退化为单线程的关键
            task(); 
        }
    }

// 单例模式防御:私有化构造函数,杜绝外部随意创建对象
private:
    ThreadPool(int num = gDefaultCnt): _num(num), _isrunning(false), _sleeper_cnt(0)
    {
        for(int i = 0; i < num; i++)
        {
            // 利用lambda表达式捕捉this指针,不然会出现参数不匹配的问题
            // 跟Thread.hpp中有关系
            _threads.emplace_back([this](){
                this->ThreadRoutine();
            });
        }
    }

    // 将拷贝和赋值语句去掉
    /**
     * 补充建议:
     * 虽然这里用了 = default,但在标准的单例模式中,
     * 拷贝构造和赋值运算符通常应该设为 = delete,以防止实例被"克隆"。
     */
    ThreadPool(const ThreadPool<T>& ) = default;
    ThreadPool<T>& operator =(const ThreadPool<T>&) = default;

public:
    // 定义成静态的:全局唯一访问点
    /**
     * @brief 获取单例对象的静态接口
     * 采用了"双检查锁 (Double-Checked Locking)"机制。
     */
    static ThreadPool<T>* GetInstance()
    {
        // 第一层判断:为了提高性能。如果实例已存在,直接返回,避免不必要的加锁开销。
        if(_instance ==  nullptr)
        {
            // 加锁:保证创建实例过程的原子性,防止多个线程同时执行 new 操作
            LockGuard lockGuard(&_signalton_lock);
            
            // 第二层判断:为了保证唯一性。在获得锁后再次检查,
            // 确认在此期间没有其他线程提前创建了实例。
            if(_instance == nullptr)
            {
                LOG(LogLevel::DEBUG) << "首次创建,创建成功" ;
                _instance = new ThreadPool<T>(); // 只会创建一次
            }
        }
        return _instance;
    }

    // 启动线程服务
    void Start()
    {
        LockGuard lockGuard(&_mutex);
        if(_isrunning)
            return;
        _isrunning = true;
        for(auto& thread: _threads)
            thread.start();
    }

    // 生产者下发任务
    void Enqueue(const T& task)
    {
        LockGuard lockGuard(&_mutex);
        if(!_isrunning) // 如果当前处于停止状态,禁止继续加任务
            return;
        _queue.push(task);
        
        // 唤醒一个来线程来执行任务
        // 优化策略:只有存在正在睡觉的工人才发通知
        if(_sleeper_cnt > 0)
            _cond.NotifyOne();
    }

    // 优雅停止线程池
    void Stop()
    {
        LockGuard lockGuard(&_mutex);
        if(_isrunning)
        {
            LOG(LogLevel::DEBUG) << "关闭线程池";
            _isrunning = false; // 改变状态,作为 ThreadRoutine 退出的触发信号
            
            // 唤醒所有的去执行, 保证所有线程都能意识到状态改变并正确 break
            if(_sleeper_cnt > 0)
                _cond.NotifyAll();
        }
    }

    // 阻塞式资源回收
    void Wait()
    {
        // join 操作本身阻塞,且不涉及临界资源修改,故无需加锁
        for(auto& thread: _threads)
            thread.join();
    }

    ~ThreadPool()
    {}

private:
    // 线程池管理组件
    std::vector<Thread> _threads; // 管理线程对象的容器
    int _num;                    // 线程池规模
    bool _isrunning;             // 全局生命周期开关
    int _sleeper_cnt;            // 记录当前空闲工人的数量

    std::queue<T> _queue;        // 共享任务队列
    Mutex _mutex;                // 保护任务队列的互斥锁
    Cond _cond;                  // 协调生产/消费节奏的条件变量

    // 单例模式静态成员
    static ThreadPool<T> *_instance;    // 全局唯一实例指针
    static Mutex _signalton_lock;       // 保护单例实例化的静态锁
};

// 静态成员变量在类外初始化:
// 静态指针在 main 运行前初始化为 null,保证 GetInstance 的逻辑起点正确。
template<typename T>
ThreadPool<T>* ThreadPool<T>::_instance = nullptr;

template <typename T>
Mutex ThreadPool<T>::_signalton_lock;

#endif

双检锁核心设计解析

  • 第一重 if 判断:实例创建完成后,所有获取实例的操作都不会进入加锁逻辑,直接返回实例,避免了每次获取实例都加锁的性能开销,这是双检锁的核心优化点;
  • 加锁保护:只有实例为空时才会加锁,保证同一时间只有一个线程能进入实例创建代码块;\
  • 第二重 if 判断:防止多个线程同时通过第一重 if 判断,比如线程 A 和 B 同时判断实例为空,A 先拿到锁创建了实例,B 拿到锁后如果没有第二重判断,会再次创建实例,破坏单例模式;
  • 注意事项 :C++11 之前需要给_instance加上volatile关键字,防止编译器指令重排导致实例未初始化完成就被使用;C++11 及之后,静态局部变量的初始化是天然线程安全的,还有更简洁的单例实现方式。

4.4 单例线程池使用示例

cpp 复制代码
#include "Logger.hpp"
#include "Task.hpp"
#include "Threadpool.hpp"
#include <memory>
#include <unistd.h>

/**
 * @brief 单例线程池实战演示
 * 核心变化:从"局部管理"变为"全局单例"。
 * 这种模式下,程序的任何角落都可以通过 GetInstance() 随时随地提交任务。
 */
int main()
{
    // 初始化日志配置
    ENABLE_CONSOLE_LOG_STRATEGY();

    // std::unique_ptr<ThreadPool<task_t>> tp = std::make_unique<ThreadPool<task_t>>(); // 这个就不行了
    /**
     * @note 为什么 unique_ptr/make_unique 不行了?
     * 答:因为在 ThreadPool_v2 中,我们将构造函数设为了 private。
     * make_unique 内部需要调用 new 来触发构造函数,而外部没有访问权限。
     * 这正是单例模式的"护城河",防止了程序员在外部不小心创建出第二个池子。
     */

    // ThreadPool<task_t>::GetInstance()->Start();
    /**
     * @brief 获取全局唯一实例
     * 第一次调用时,会在堆上申请内存并初始化;
     * 后续调用直接返回同一个对象的指针,确保全局只有一套任务队列和线程组。
     */
    auto tp = ThreadPool<task_t>::GetInstance();
    
    // 启动线程池:让预创的线程进入待命状态
    tp->Start();

    // 模拟生产者行为
    int cnt = 10;
    while(cnt--)
    {
        // 打印分界线,标识每一次生产循环
        LOG(LogLevel::DEBUG) << "-----------------------: " << cnt;
        
        // 间隔 1 秒投喂任务,模拟低频持续的业务流量
        sleep(1);
        tp->Enqueue(task1); // 派发打印任务

        sleep(1);
        tp->Enqueue(task2); // 派发计算任务
    }

    /**
     * @brief 优雅停机
     * 即使是单例,在主流程结束前也建议显式调用 Stop 和 Wait。
     * Stop():阻止新任务进入,并唤醒所有 Worker 准备下班。
     * Wait():主线程在此等待,确保 Worker 线程处理完队列里的"存量工作"后被安全 join。
     */
    tp->Stop();
    tp->Wait();
    
    return 0;
}
  • 可以看到首次创建成功,剩下的其实跟之前的输出结果看不出啥区别

  • 补充和优化

五. 线程池背后的核心安全问题

线程池的底层是多线程的同步与互斥,只有彻底理解线程安全、死锁等核心问题,才能写出健壮的高并发代码。

5.1 线程安全与函数可重入

核心概念

  • 线程安全:多个线程并发访问共享资源时,程序能正确执行,不会出现数据竞争、结果异常,就称这个程序 / 函数是线程安全的。
  • 可重入:同一个函数被不同的执行流调用,前一个调用还未执行完,就有其他执行流再次进入,运行结果不会出现任何问题,这个函数就是可重入函数。

联系与区别

  • 可重入函数一定是线程安全的,线程安全的函数不一定是可重入的;
  • 线程安全描述的是多线程并发访问的运行特性,可重入描述的是函数被重复调用的代码特性;
  • 函数不可重入,大概率会导致线程安全问题。


常见不安全场景

  1. 不保护共享全局 / 静态变量的函数;
  2. 调用了 malloc/free、标准 I/O 库函数的函数(内部使用全局数据结构,不可重入);
  3. 返回静态变量指针的函数;
  4. 函数状态随调用发生变化的函数。

5.2 死锁:多线程编程的头号杀手

死锁是指一组线程各自持有不会释放的资源,又互相申请对方持有的资源,导致所有线程永久阻塞等待的状态。

死锁的四个必要条件

死锁发生时,这四个条件必须同时满足,破坏其中任意一个,就能避免死锁:

  1. 互斥条件:一个资源同一时间只能被一个线程使用,锁的基本特性,无法破坏;

  2. 请求与保持条件 :线程申请新资源阻塞时,不释放已经持有的资源;

  3. 不剥夺条件 :线程已持有的资源,在使用完之前不能被其他线程强行剥夺;

  4. 循环等待条件 :多个线程形成头尾相接的循环等待资源的关系。

避免死锁的核心方法(破坏上面的四个条件中任意一个即可,互斥最简单,不用就行。其他的有的我没详细说,大概看看了解一下,面试问的比较少,可能会问)

  • 破坏循环等待条件 :最常用的方式,保证所有线程加锁顺序完全一致;一次性申请所有需要的资源;用std::lock一次性锁定多个互斥锁;
  • 破坏请求与保持条件 :申请锁失败时,立即释放已持有的所有锁,使用非阻塞的trylock接口;
  • 代码规范:使用 RAII 风格的锁管理,避免忘记解锁;避免临界区内嵌套加锁;避免临界区内执行耗时操作、调用阻塞函数。


5.3 STL 容器与智能指针的线程安全

这是 C++ 多线程编程中最容易踩坑的点:

  • STL 容器默认不是线程安全的:STL 的设计初衷是极致的性能,加锁会带来巨大的性能开销,因此所有 STL 容器(vector、queue、map 等)都不是线程安全的。多线程并发读写同一个容器时,必须由开发者自行加锁保护,否则会出现迭代器失效、数据损坏、程序崩溃等问题。
  • 智能指针的线程安全
    • unique_ptr:所有权唯一,只在当前代码块内生效,天然线程安全;
    • shared_ptr:标准库用原子操作 (CAS) 保证了引用计数的增减是原子的,因此引用计数操作是线程安全的;但指向的对象的并发访问,不是线程安全的,需要自行加锁保护。


5.4 常见锁概念拓展

  • 悲观锁 vs 乐观锁
    • 悲观锁:我们使用的互斥锁就是典型的悲观锁,每次访问数据前都先加锁,认为数据一定会被修改,适用于写多读少的场景;
    • 乐观锁:访问数据时不加锁,更新时判断数据是否被修改,通过版本号和 CAS 操作实现,适用于读多写少的场景,性能远高于悲观锁。
  • CAS 操作:Compare-And-Swap,比较并交换,是乐观锁和无锁编程的核心。更新数据时,先判断当前内存值和之前读取的值是否相等,相等则更新,否则重试。现代 CPU 都提供了 CAS 原子指令。
  • 自旋锁:申请锁失败时,线程不会被阻塞挂起,而是循环轮询尝试获取锁。适用于临界区执行时间极短的场景,避免线程切换的开销,缺点是长时间自旋会浪费 CPU 资源。
  • 读写锁:针对读多写少场景优化的锁,读 - 读共享,写 - 写 / 读 - 写互斥。多个线程可以同时持有读锁,写锁同一时间只能被一个线程持有,大幅提升读多写少场景的并发度。



结尾:

html 复制代码
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:本文从池化技术的核心思想出发,完整讲解了线程池的设计原理,手撕了工业级 C++ 线程池的完整实现,再到单例模式的进阶优化,最后深入拆解了线程安全、死锁、锁机制等底层核心问题,完整覆盖了 Linux C++ 高并发编程中线程池的全链路知识点。线程池是高并发后端开发的基石,它的本质是生产者消费者模型的工程化落地,核心思想是用预申请换响应速度,用统一管理换系统稳定性,用资源复用换系统开销。而线程池的底层,离不开互斥锁、条件变量这些同步互斥原语,更离不开对线程安全、死锁等问题的深刻理解。在实际的工业级开发中,线程池还会有更多进阶优化,比如动态调整线程数量的浮动线程池、任务优先级队列、线程异常处理、监控统计等功能,但核心的设计原理永远不会变。希望这篇文章能帮你彻底掌握线程池,解锁 Linux C++ 高并发编程的核心能力。

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど

相关推荐
栗少2 小时前
Python 入门教程(面向有 Java 经验的开发者)
java·开发语言·python
小王师傅662 小时前
【Java结构化梳理】泛型-上
java·开发语言
大龄码农-涵哥2 小时前
MySQL SQL调优详解:explain执行计划、索引失效、慢查询优化一条龙
数据库·sql·mysql
万法若空2 小时前
TCP网络编程基础
服务器·网络·tcp/ip
m0_613856292 小时前
mysql如何使用IF函数_mysql简单二元逻辑转换
jvm·数据库·python
齐潇宇2 小时前
Kubectl命令指南
linux·运维·云原生·容器·kubernetes
_F_y2 小时前
SQLite3的基础使用
jvm·数据库·sqlite
小周技术驿站2 小时前
Docker服务详解
运维·docker·容器