《Linux工程实践篇(二):线程池实战——从线程安全单例到完整源码实现》

本文要聊的,是线程池。

先从核心概念入手,把它的典型应用场景和选型分类铺开,让你对线程池有个整体的判断框架。然后往深里挖,把饿汉式和线程安全懒汉式这两种单例模式的实现原理逐一拆开,看清它们各自的门道。最后落到代码上,结合单例模式,给出一个完整可用的线程池实现。

目录

一、认识线程池:为什么需要线程池

[1.1 线程池的基本概念与核心价值](#1.1 线程池的基本概念与核心价值)

[1.2 哪些场景适合使用线程池](#1.2 哪些场景适合使用线程池)

[1.2.1 高并发下的短任务处理](#1.2.1 高并发下的短任务处理)

[1.2.2 对响应速度要求较高的应用](#1.2.2 对响应速度要求较高的应用)

[1.2.3 面对突发流量时的线程资源控制](#1.2.3 面对突发流量时的线程资源控制)

[1.3 常见线程池类型与基本选型思路](#1.3 常见线程池类型与基本选型思路)

二、线程安全的单例模式:为什么线程池需要它

[2.1 单例模式是什么:从生活场景理解](#2.1 单例模式是什么:从生活场景理解)

[2.2 单例模式的实现与逐步演进](#2.2 单例模式的实现与逐步演进)

[2.2.1 饿汉模式:程序启动时完成实例创建](#2.2.1 饿汉模式:程序启动时完成实例创建)

[2.2.2 懒汉模式:按需创建实例及其隐患](#2.2.2 懒汉模式:按需创建实例及其隐患)

[2.2.3 加锁实现线程安全的懒汉模式](#2.2.3 加锁实现线程安全的懒汉模式)

三、用单例模式实现一个线程池

[3.1 基础组件:线程池运行所需的基础设施](#3.1 基础组件:线程池运行所需的基础设施)

[3.1.1 Cond.hpp:条件变量封装](#3.1.1 Cond.hpp:条件变量封装)

[3.1.2 Mutex.hpp:互斥锁封装](#3.1.2 Mutex.hpp:互斥锁封装)

[3.1.3 Log.hpp:日志功能封装](#3.1.3 Log.hpp:日志功能封装)

[3.1.4 Task.hpp:任务对象定义](#3.1.4 Task.hpp:任务对象定义)

[3.2 核心组件:线程与线程池](#3.2 核心组件:线程与线程池)

[3.2.1 Thread.hpp:线程封装](#3.2.1 Thread.hpp:线程封装)

[3.2.2 ThreadPool.hpp:线程池核心实现](#3.2.2 ThreadPool.hpp:线程池核心实现)

[3.3 从编译到运行:构建与测试](#3.3 从编译到运行:构建与测试)

[3.3.1 Makefile:项目编译规则](#3.3.1 Makefile:项目编译规则)

[3.3.2 Main.cpp:线程池测试入口](#3.3.2 Main.cpp:线程池测试入口)


一、认识线程池:为什么需要线程池

1.1 线程池的基本概念与核心价值

线程池,说白了就是一种线程使用模式。多线程编程里,如果任由线程无节制地创建,数量一多,上下文切换和调度开销就会大得惊人,CPU的缓存局部性被破坏,系统整体性能跟着往下掉。

线程池的做法,是在内部预先养着一批线程,让它们处于等待状态,随时准备接住由管理者派发下来的并发任务。这套机制带来的核心收益,主要有两点:

  • 资源消耗和开销极低:处理短任务时,不必反复创建、销毁线程,省去了那笔巨大的开销。

  • 资源利用高效,还能防过载:既让系统内核和硬件资源得到充分利用,又能有效防止线程被过度调度。

系统里到底能同时跑多少线程,不是拍脑袋定的。它通常受制于硬件和系统资源的瓶颈,可用的并发处理器数量、处理器内核数、内存容量,还有网络socket句柄数量,都是天花板。

1.2 哪些场景适合使用线程池

线程池这东西,特性摆在那儿,现代服务端和高并发系统里,到处都能看到它的身影。

1.2.1 高并发下的短任务处理

任务量巨大,但单个任务跑起来没一会儿就结束,这种场景,线程池简直是为它量身定做的。最典型的例子,就是热门Web服务器扛住海量用户的点击请求。

反过来,如果是那种一跑就很久的长任务,比如Telnet连接请求,情况就不一样了。Telnet会话持续的时间,远远超过线程本身创建的开销,这时候再用线程池,性能提升就微乎其微,没什么意思。

1.2.2 对响应速度要求较高的应用

有些系统,对性能的要求苛刻到近乎偏执,客户端的请求必须秒回,比如实时交易系统、高频 RPC 服务。线程池在这里的价值,就是提前把工作线程备好。任务一到,立刻就能上手,响应近乎"零等待"。临时抱佛脚地创建线程?根本来不及。

1.2.3 面对突发流量时的线程资源控制

系统时不时会撞上突发流量,一瞬间,大量请求涌进来。没有线程池的时候,系统会慌不择路地疯狂创建线程。理论上,大部分操作系统支持的线程上限确实不低,但短时间内的爆发式创建,极容易把内存逼到极限,一不小心就引发严重错误。

线程池往那一挡,事情就稳了:该接的流量照接,线程总量却始终被摁在安全线以内。扛得住冲击,也守得住底线。

1.3 常见线程池类型与基本选型思路

实际工程里落地线程池,常见的主要是两种模式。

固定数量线程池:预先创建好固定数量的工作线程。这些线程一出生就进入循环,不断从任务队列里捞任务对象。捞到一个,立刻执行它身上的接口方法。线程数量定死,不增不减。

浮动线程池:线程数量不写死,而是根据系统当前的实际负载,动态地增减。核心调度逻辑跟固定线程池大差不差,区别只在于"要不要多开几个线程、要不要裁掉几个闲人"这一层。

本系列的设计与实现里,我们选的是固定个数的线程池。逻辑更清晰,落地也更好把控,拿来把线程池的原理吃透,正合适。

二、线程安全的单例模式:为什么线程池需要它

2.1 单例模式是什么:从生活场景理解

有些类,在整个系统里只应该有一个对象,这种设计模式,就叫单例模式。就像现实里,一个男人只能有一个媳妇,天然带着唯一性。

服务器开发里,这事儿特别常见。程序启动时,往往要把海量数据,上百G的配置、缓存,一把加载进内存。这时候要是反复创建对象,内存分分钟被撑爆,数据还容易各说各话,对不上号。所以,像这种需要统一管理的数据,特别适合交给单例模式的类来管。

单例模式有两种常见写法,饿汉方式 和懒汉方式。为了把它们讲得直观,拿生活中的"洗碗"打个比方:

饿汉方式:刚吃完饭,立刻把碗洗干净。好处是下一顿要吃的时候,随手就能拿起干净的碗用,加载迅速,随用随有。

懒汉方式:吃完饭先把碗搁着,不急着洗。直到下一顿真要用了,才临时抱佛脚去洗。

懒汉方式最核心的思想,就是延迟加载。服务端启动阶段,如果暂时用不上这些资源,那就先不初始化。启动速度,就这么被大幅优化出来了。

2.2 单例模式的实现与逐步演进

思路其实很简单:只要通过Singleton这个包装类去碰目标对象T,就能保证在一个进程里,T的实例有且只有一个。

2.2.1 饿汉模式:程序启动时完成实例创建

饿汉方式,主打一个"急脾气",类加载或者程序启动的时候,静态实例就已经初始化完毕,什么都不等。

cpp 复制代码
template <typename T>
class Singleton {
    static T data;
public:
    static T* GetInstance() {
        return &data;
    }
};

data是静态成员,程序一启动它就位。谁来要实例,直接把地址甩过去。不用判断、不用加锁,省心。

2.2.2 懒汉模式:按需创建实例及其隐患

懒汉方式正好相反,不急着造,等第一次有人来要了,才动手创建:

cpp 复制代码
template <typename T>
class Singleton {
    static T* inst;
public:
    static T* GetInstance() {
        if (inst == NULL) {
            inst = new T();
        }
        return inst;
    }
};

看着挺聪明,实则埋着一颗雷:多线程安全。

设想多个线程同时第一次调用GetInstance()。它们齐刷刷跑到if (inst == NULL)跟前,一看,都是空的,于是个个都觉得"该我出手了"。结果,你new 一个,我new 一个,他再new 一个,好几份T对象的实例,就这么凭空冒了出来。单例约束,碎了一地。

当然,这颗雷只在"首次创建"那一瞬间引爆。等实例一旦造好,后面再多的并发调用,也只是读读指针,不会出问题。可问题恰恰就在那一瞬间,越是高并发,越容易撞上。

2.2.3 加锁实现线程安全的懒汉模式

想让懒汉模式在多线程环境下安全落地,光靠一个if显然不够。得请出互斥锁,再配上经典的**双重检查锁(Double-Checked Locking)**机制:

cpp 复制代码
// 懒汉模式,线程安全
template <typename T>
class Singleton {
    volatile static T* inst; // 需要 volatile 关键字,防止指令重排与编译器优化
    static std::mutex lock;
public:
    static T* GetInstance() {
        // 外层判定:先看一眼,实例是不是已经造好了
        if (inst == NULL) {
            lock.lock();
            // 内层判定:加锁之后,再确认一次,防止多个线程同时闯入
            if (inst == NULL) {
                inst = new T();
            }
            lock.unlock();
        }
        return inst;
    }
};

两层判断,各司其职,配合得天衣无缝。

外层判断,负责"偷懒"。实例一旦创建完成,后续每一次调用都直接从这里返回,根本不用去碰锁。锁冲突的概率被压到最低,性能自然上来了。

内层判断,负责"把关"。首次并发调用的那一刻,多个线程可能同时挤进外层判断。但锁一上,就只剩一个幸运儿能往下走。它再确认一次inst == NULL,然后执行new。等它释放锁,后面排队的线程冲进来,一看inst已经有值了,乖乖退出去,各回各家。new操作,有且仅有一次。

volatile关键字,则是给编译器提个醒:别自作聪明。它能防止编译器过度优化,也能挡住指令重排。否则,inst 指针有可能在对象还没完全构造好之前就被暴露出去,别的线程一读,拿到的就是一个半成品,后患无穷。

三、用单例模式实现一个线程池

3.1 基础组件:线程池运行所需的基础设施

线程池不是凭空冒出来的,它得靠几个基础件搭起来:互斥锁、条件变量、日志,还有任务定义。先把这几块砖备好,后面砌墙才顺手。

3.1.1 Cond.hpp:条件变量封装

条件变量的薄封装,把pthread_cond_t的初始化、等待、唤醒、销毁,包成好用的方法。

cpp 复制代码
#ifndef __CONDMODULE__
#define __CONDMODULE__
#include <pthread.h>
#include "Mutex.hpp"

namespace ConModule
{
    class Cond
    {
    public:
        Cond()
        {
            int n = pthread_cond_init(&_cond, nullptr);
            (void)n;
        }

        void Wait(MutexModule::Mutex &mutex)
        {
            int n = pthread_cond_wait(&_cond, mutex.Get());
            (void)n;
        }

        void Signal()
        {
            int n = pthread_cond_signal(&_cond);
            (void)n;
        }

        void Broadcast()
        {
            int n = pthread_cond_broadcast(&_cond);
            (void)n;
        }

        ~Cond()
        {
            int n = pthread_cond_destroy(&_cond);
            (void)n;
        }

    private:
        pthread_cond_t _cond;
    };
}
#endif

Wait接收一个Mutex引用,通过mutex.Get()拿到原始的pthread_mutex_t指针,再交给pthread_cond_wait。条件变量的等待,天生就得跟互斥锁绑在一起,这层封装把这层关系顺理成章地接住了。

3.1.2 Mutex.hpp:互斥锁封装

互斥锁和RAII守卫的组合,一个管锁本体,一个管自动加解锁。

cpp 复制代码
#ifndef __MUTEXMODULE__
#define __MUTEXMODULE__
#include <pthread.h>
#include <filesystem>
#include <iostream>
#include <ctime>
#include <string>
#include <sys/types.h>
#include <unistd.h>
#include <fstream>
#include <sstream>
#include <memory>
#include <cstdio>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            int n = pthread_mutex_init(&_mutex, nullptr);
            (void)n;
        }

        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }

        void UnLock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }

        ~Mutex()
        {
            int n = pthread_mutex_destroy(&_mutex);
            (void)n;
        }

        pthread_mutex_t* Get()
        {
            return &_mutex;
        }

    private:
        pthread_mutex_t _mutex;
    };

    class MutexGuard
    {
    public:
        MutexGuard(Mutex& mutex)
            : _mutex(mutex)
        {
            _mutex.Lock();
        }

        ~MutexGuard()
        {
            _mutex.UnLock();
        }

    private:
        Mutex& _mutex;
    };
}
#endif

Mutex把pthread_mutex_t包起来,Get()方法把原始指针暴露出去,方便条件变量那边调用。MutexGuard是RAII守卫,构造即加锁、析构即解锁,这次传的是引用,不会再把锁拷成两份了。

3.1.3 Log.hpp:日志功能封装

日志模块,策略模式撑骨架,RAII消息管输出。

cpp 复制代码
#ifndef __LOG_MODULE__
#define __LOG_MODULE__
#include "Mutex.hpp"

namespace LogModule
{
    const std::string CAGE = "\n";

    // 刷盘策略抽象基类
    class LogFlashStrategy
    {
    public:
        ~LogFlashStrategy() = default;
        virtual void SyncLog(const std::string& message) = 0;
    };

    // 控制台输出策略
    class ConsoleLogStrategy : public LogFlashStrategy
    {
    public:
        void SyncLog(const std::string& logmessage) override
        {
            MutexModule::MutexGuard _MutexGuard(_mutex);
            std::cout << logmessage << CAGE;
        }
    private:
        MutexModule::Mutex _mutex;
    };

    const std::string default_filename = "log.txt";
    const std::string default_filepath = "./log";

    // 文件输出策略
    class FileLogStrategy : public LogFlashStrategy
    {
    public:
        FileLogStrategy(const std::string file = default_filename,
                        const std::string path = default_filepath)
            : _filename(file), _filepath(path)
        {
            MutexModule::MutexGuard _MutexGuard(_mutex);
            if (std::filesystem::exists(_filepath))
                return;
            try
            {
                std::filesystem::create_directories(_filepath);
            }
            catch (const std::filesystem::filesystem_error& _exception)
            {
                std::cout << _exception.what() << CAGE;
            }
        }

        void SyncLog(const std::string& logmessage) override
        {
            MutexModule::MutexGuard _MutexGuard(_mutex);
            std::string pathfile = _filepath +
                (_filepath.back() == '/' ? "" : "/") + _filename;
            std::ofstream out(pathfile, std::ios::app);
            if (!out.is_open())
                return;
            out << logmessage << CAGE;
            out.close();
        }
    private:
        MutexModule::Mutex _mutex;
        std::string _filename;
        std::string _filepath;
    };

    // 日志等级
    enum class LEVEL
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    // 获取当前时间字符串
    std::string GetCurrentTime()
    {
        time_t cur_time = time(nullptr);
        struct tm result;
        localtime_r(&cur_time, &result);
        char timebuffer[128];
        snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
                 result.tm_year + 1900,
                 result.tm_mon + 1,
                 result.tm_mday,
                 result.tm_hour,
                 result.tm_min,
                 result.tm_sec);
        return timebuffer;
    }

    // 等级转字符串
    std::string LevelToString(LEVEL level)
    {
        switch (level)
        {
        case LEVEL::DEBUG:   return "DEBUG";
        case LEVEL::INFO:    return "INFO";
        case LEVEL::WARNING: return "WARNING";
        case LEVEL::ERROR:   return "ERROR";
        case LEVEL::FATAL:   return "FATAL";
        default:             return "UNKNOWN";
        }
    }

    class Logger
    {
    public:
        Logger()
        {
            EnableConsolLogStrategy();
        }

        void EnableConsolLogStrategy()
        {
            _current_flash_tsrategy = std::make_unique<ConsoleLogStrategy>();
        }

        void EnableFileLogStrategy()
        {
            _current_flash_tsrategy = std::make_unique<FileLogStrategy>();
        }

        // 一条日志消息,RAII 风格
        class LogMessage
        {
        public:
            LogMessage(LEVEL& level, std::string& file, size_t line, LogModule::Logger& logger)
                : _current_time(GetCurrentTime()),
                  _level(level),
                  _pid(getpid()),
                  _src_file(file),
                  _line(line),
                  _logger(logger)
            {
                std::stringstream ret;
                ret << "[" << _current_time << "]"
                    << "[" << LevelToString(_level) << "]"
                    << "[" << _pid << "]"
                    << "[" << _src_file << "]"
                    << "[" << _line << "] -";
                _hole_message = ret.str();
            }

            template<class T>
            LogMessage& operator<<(const T& info)
            {
                std::stringstream ss;
                ss << info;
                _hole_message += ss.str();
                return *this;
            }

            ~LogMessage()
            {
                if (_logger._current_flash_tsrategy)
                    _logger._current_flash_tsrategy->SyncLog(_hole_message);
            }
        private:
            std::string _current_time;
            LEVEL _level;
            pid_t _pid;
            std::string _src_file;
            size_t _line;
            std::string _hole_message;
            LogModule::Logger& _logger;
        };

        LogMessage operator()(LEVEL level, std::string filename, size_t line)
        {
            return LogMessage(level, filename, line, *this);
        }
    private:
        std::unique_ptr<LogFlashStrategy> _current_flash_tsrategy;
    };

    Logger logger;

#define LOG(level) logger(level, __FILE__, __LINE__)
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.EnableConsolLogStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.EnableFileLogStrategy()
}
#endif
3.1.4 Task.hpp:任务对象定义

任务定义,简单直白,就一个函数类型别名和一个示例任务。

cpp 复制代码
#ifndef __TASK__
#define __TASK__
#include <functional>
#include <iostream>

using task_t = std::function<void(void)>;

void DownLoadTask(void)
{
    std::cout << "这是一个下载任务" << std::endl;
}
#endif

task_t就是std::function<void(void)>,任务被抽象成可调用对象。任何签名匹配的函数、lambda、仿函数,都能往这个框里装。线程池接到的任务,就是这一份份等待被执行的 task_t。

四个基础组件到这就备齐了:Cond管等待与唤醒,Mutex管互斥与RAII,Log管日志输出,Task管任务抽象。下一步,就是拿它们拼出线程池的本体。

3.2 核心组件:线程与线程池

基础件备齐了,现在轮到主角登场。Thread.hpp负责把线程封成对象,ThreadPool.hpp负责把线程管起来、把任务派下去。两块拼在一起,线程池的本体就立起来了。

3.2.1 Thread.hpp:线程封装
cpp 复制代码
#include "Mutex.hpp"
#include "Task.hpp"
#include "Log.hpp"

namespace ThreadModule
{
    int count = 1;

    class Thread
    {
    private:
        static void* Routine(void* args)
        {
            Thread* self = static_cast<Thread*>(args);
            pthread_setname_np(pthread_self(), self->_name);
            if (!self->_isrunning)
                self->_isrunning = true;
            self->_func();   // 转成 this->_func()
            return nullptr;
        }

    public:
        Thread(task_t task)
            : _tid(0),
              _func(task),
              _isrunning(false)
        {
            snprintf(_name, sizeof(_name), "Thread-%d", count);
            count++;
        }

        void Start()
        {
            if (_isrunning)
                return;
            int n = pthread_create(&_tid, nullptr, &Routine, this);
            if (n == 0)
                LogModule::LOG(LogModule::LEVEL::DEBUG) << "线程创建成功" << LogModule::CAGE;
            else
                LogModule::LOG(LogModule::LEVEL::ERROR) << "线程创建失败" << LogModule::CAGE;
        }

        void join()
        {
            pthread_join(_tid, nullptr);
        }

        char* Name()
        {
            return _name;
        }

    private:
        pthread_t _tid;
        bool _isrunning;
        task_t _func;
        char _name[128];
    };
}

这个类把pthread那套裸接口包成了一个对象。几个关键点:

Routine是静态函数。 pthread_create只认void*(*)(void*)这个签名,成员函数自带隐式this,对不上。所以Routine必须是static,通过args把this传进来,再在里面调_func()。

线程名带编号。 用全局count给每个线程起名Thread-1、Thread-2......调试日志里一眼就能分辨谁是谁。

_isrunning在Routine里置位。 这一点很讲究,pthread_create成功,只代表线程被创建了,不代表它已经拿到CPU开始跑。_isrunning在Routine内部才被置为true,状态更新跟真实执行时机对得上。

Start里做了防重入。 已经跑起来的线程,不会再被Start一次。

3.2.2 ThreadPool.hpp:线程池核心实现
cpp 复制代码
#ifndef __THREAD_POOL__
#define __THREAD_POOL__
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Task.hpp"
#include <queue>

namespace ThreadPoolModuleBasedOnSingletonPattern
{
    using namespace LogModule;
    using namespace MutexModule;
    using namespace ConModule;
    using namespace ThreadModule;

    static const size_t DEFULT_SIZE = 5;

    template <typename T>
    class ThreadPool
    {
    private:
        void WakeUpAllThread()
        {
            if (_ThreadSleepSize)
                _cond.Broadcast();
            LOG(LEVEL::INFO) << "唤醒所有休眠的线程" << CAGE;
        }

        void WakeUpOneThread()
        {
            _cond.Signal();
            LOG(LEVEL::INFO) << "唤醒一个休眠线程" << CAGE;
        }

        ThreadPool()
            : _ThreadSleepSize(0),
              _isrunning(false),
              _Size(DEFULT_SIZE)
        {
            for (int i = 0; i < DEFULT_SIZE; i++)
            {
                _Thread.emplace_back(
                    [this]()
                    {
                        HanderTask();
                    });
            }
        }

        void Start()
        {
            if (_isrunning)
                return;
            _isrunning = true;
            for (auto& e : _Thread)
            {
                e.Start();
                LOG(LEVEL::INFO) << "创建线程池成功" << e.Name();
            }
        }

        ThreadPool<T>(const ThreadPool<T>& threadpool) = delete;
        ThreadPool<T>& operator=(const ThreadPool<T>& threadpool) = delete;

    public:
        static ThreadPool<T>* GetInstance()
        {
            if (_SingPtr == nullptr)
            {
                MutexGuard mutexguard(_lock);
                LOG(LEVEL::INFO) << "获取单例。。。" << CAGE;
                {
                    if (_SingPtr == nullptr)
                    {
                        LOG(LEVEL::INFO) << "首次使用单例,创建单例" << CAGE;
                        _SingPtr = new ThreadPool<T>;
                        _SingPtr->Start();
                    }
                }
            }
            return _SingPtr;
        }

        void HanderTask()
        {
            LOG(LEVEL::INFO) << "进入线程处理函数";
            char name[64];
            pthread_getname_np(pthread_self(), name, sizeof(name));

            while (true)
            {
                T task;
                {
                    MutexGuard mutexguard(_mutex);
                    while (_isrunning && _TaskQueue.empty())
                    {
                        LOG(LEVEL::DEBUG) << "进入等待";
                        _ThreadSleepSize++;
                        _cond.Wait(_mutex);
                        _ThreadSleepSize--;
                        LOG(LEVEL::DEBUG) << "被唤醒";
                    }

                    LOG(LEVEL::INFO) << "退出等待 queue=" << _TaskQueue.size();

                    if (!_isrunning && _TaskQueue.empty())
                    {
                        LOG(LEVEL::INFO) << "线程池退出且任务队列为空" << CAGE;
                        break;
                    }

                    task = _TaskQueue.front();
                    LOG(LEVEL::INFO) << "取出任务";
                    _TaskQueue.pop();
                }
                task();
            }
        }

        bool Equeue(const T& task)
        {
            if (_isrunning)
            {
                MutexGuard mutexguard(_mutex);
                _TaskQueue.push(task);
                if (_ThreadSleepSize > 0)
                    WakeUpOneThread();
                return true;
            }
            return false;
        }

        void Stop()
        {
            LOG(LEVEL::DEBUG) << "线程池停止" << CAGE;
            if (_isrunning)
            {
                MutexGuard mutexguard(_mutex);
                _isrunning = false;
                if (_ThreadSleepSize)
                {
                    WakeUpAllThread();
                }
            }
        }

        void Join()
        {
            LOG(LEVEL::DEBUG) << "线程池回收线程" << CAGE;
            if (!_isrunning)
            {
                for (auto& e : _Thread)
                {
                    e.join();
                }
            }
        }

    private:
        size_t _ThreadSleepSize;
        bool _isrunning;
        std::vector<Thread> _Thread;
        std::queue<T> _TaskQueue;
        size_t _Size;
        Mutex _mutex;
        Cond _cond;
        static ThreadPool<T>* _SingPtr;
        static Mutex _lock;
    };

    template <class T>
    ThreadPool<T>* ThreadPool<T>::_SingPtr = nullptr;

    template <class T>
    Mutex ThreadPool<T>::_lock;
}
#endif

线程池本体

单例模式,双重检查锁。 GetInstance先在外层判断_SingPtr是否为空,空才去抢锁。抢到锁后再判一次,确认真的还没创建,才new出实例并调Start。外层判断省掉了实例创建之后的锁开销,内层判断挡住了首次并发时的重复创建。跟前面讲的懒汉模式,是一个路子。

构造函数只造对象,不启动线程。 ThreadPool的构造函数里,只负责往_Thread里塞Thread对象,每个对象绑一个HanderTask作为任务函数。真正启动线程的活儿,放在Start里,由GetInstance在首次创建后调用。这样设计,是为了让"创建"和"启动"分开,逻辑上更干净。

**HanderTask是工作线程的主循环。**每个线程跑起来之后,就在这个函数里转圈:

  • 加锁,检查队列是否为空。空,就_ThreadSleepSize++,然后_cond.Wait(_mutex)挂起,把锁交出去。
  • 被唤醒后,_ThreadSleepSize--,再看一眼队列和 _isrunning。
  • 如果线程池已经停了,而且队列也空了,break 退出循环,线程结束。
  • 否则,从队头取一个任务,解锁,执行 task()。

Equeue负责投递任务。 加锁,把任务推进队列,如果发现有线程在睡,就 WakeUpOneThread 叫醒一个。一放一取,配合默契。

**Stop和Join负责收尾。**Stop把_isrunning 置为false,然后唤醒所有睡着的线程,让它们检查到停止信号后退出。Join挨个join,把线程资源回收干净。

到这里,线程池的骨架就搭完了。Thread管单个线程的生老病死,ThreadPool管一群线程的调度和任务分发。单例模式保证了全局只有一个线程池,双重检查锁保证了它在多线程环境下也能安全地出生。

3.3 从编译到运行:构建与测试

代码写完了,得让它真正跑起来。一个 Makefile 管编译,一个 Main.cpp 管调用,两下配合,线程池就能转起来了。

3.3.1 Makefile:项目编译规则
bash 复制代码
BIN = proc
SRC = Main.cpp

$(BIN) : $(SRC)
	g++ -o $@ $^ -pthread -std=c++17

.PHONY: clean
clean :
	rm -f $(BIN) ./log/log.txt
3.3.2 Main.cpp:线程池测试入口
cpp 复制代码
#include "ThreadPool.hpp"
using namespace LogModule;
using namespace ThreadPoolModuleBasedOnSingletonPattern;

const size_t NUM = 5;

int main()
{
    ENABLE_FILE_LOG_STRATEGY();

    for (int i = 0; i < NUM; i++)
    {
        sleep(1);
        ThreadPool<task_t>::GetInstance()->Equeue(DownLoadTask);
    }

    ThreadPool<task_t>::GetInstance()->Stop();
    ThreadPool<task_t>::GetInstance()->Join();

    return 0;
}

主程序干的事很直白:把日志策略切到文件模式,然后每隔一秒,往线程池里丢一个下载任务,一共丢5个。丢完之后,调Stop让线程池停下,再调Join把线程回收干净。

几个细节值得留意:

ENABLE_FILE_LOG_STRATEGY()切的是日志输出方式。 这一宏一调,日志就从控制台转到了文件里。不调它,默认走的是控制台策略,日志直接打印到屏幕上。两种方式,各有用处------调试时看控制台,跑久了存文件。

GetInstance()每次都被调用,但线程池只创建一次。 第一次调用时,单例还没出生,GetInstance一路走到new,把线程池造出来,顺手启动所有工作线程。后面几次调用,外层判断一看_SingPtr非空,直接返回,连锁都不用碰。单例的甜头,就体现在这儿。

sleep(1)让任务一个个来,方便观察。 如果一口气把5个任务全丢进去,线程们会瞬间把任务抢光,日志刷得太快,看不出调度过程。加个一秒的间隔,生产一个、消费一个,节奏就清清楚楚了。

**测试时的日志输出,能帮你看清整个调度链路。**HanderTask里埋了不少日志:进入等待、被唤醒、取出任务、退出循环。跑一遍,控制台或日志文件里,线程们的一举一动全都有迹可循。哪个线程先醒、哪个任务先被取走、队列什么时候空、线程什么时候退,全都看得见。


如果这篇文章对你有帮助,别忘了点个赞、点个收藏、点个关注。你的每一次反馈,都是我继续硬核输出的最大动力。我们下篇见。

相关推荐
无忧.芙桃1 小时前
C++语言原理与实践(十二):list类的底层实现
c++·windows·list
看着博客敲代码1 小时前
No space left on device:服务器被 SSH 爆破到日志写满的完整排查与止血
linux·运维·服务器·安全·ssh
明日清晨1 小时前
mobaxterm root身份登录Ubuntu
linux·ubuntu·postgresql
HRTOS1 小时前
HRTOS 官网内容调整记录:首页、系统架构与文档页面优化
单片机·系统架构·51单片机
泡海椒1 小时前
JQuick-Excel 多字段 TRANSFORM:让当前行字段、JContext 与展示列各归其位
开发语言·python·excel
JPower_mr.g1 小时前
SmartCall 音色管理技术解析:基于 SPI 的可扩展音色注册架构
java·开发语言·人工智能·ai·架构·开源
源图客2 小时前
浏览器开发者工具使用
开发语言·php
有只小白叫岳飒2 小时前
c++智能指针 unique_ptr与函数调用
c++
Leo.yuan2 小时前
Kafka+Flink拼凑,还是FineDataLink 5.0一体化?实时数据处理的两种思路
c++·mfc