Linux 线程同步与互斥(五) 日志,线程池

目录

一、线程池原理

二、日志介绍

什么是日志

日志轮转的核心逻辑

日志实现方案

介绍设计模式和策略模式

三、日志代码的实现

大体框架:

获取日志时间

geitimeofday

localtime_r

代码实现

策略模式

[最上层:策略接口 LogStrategy](#最上层:策略接口 LogStrategy)

[下层:具体策略实现 ConsoleStrategy](#下层:具体策略实现 ConsoleStrategy)

​编辑

[下层:具体策略实现 FileLogStrategy](#下层:具体策略实现 FileLogStrategy)

日志类

初始框架

日志生成

[Main.cc 测试](#Main.cc 测试)

四、线程池

[代码实现 :](#代码实现 :)

回调机制

逻辑梳理:

五、完整代码

ThreadPool.hpp

Thread.hpp

Mutex.hpp

Cond.hpp

Logger.hpp

Makefile

Main.cc

六、总结


一、线程池原理

线程池使用了池化技术,以空间换时间。线程池是线程的一种使用模式。线程过多会带来调度开销,影响整体性能。而线程池维护者多个线程,等待着被派发可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价,还能保证内核被充分利用,防止过度调度。

应用场景:

(1)需要大量线程来完成任务,且完成任务的时间较短。例如:web服务器完成网页请求

(2)对性能要求苛刻的应用。例如:要求服务器快速响应用户请求

(3)接收突发性的大量请求,但不至于使服务器因此产生大量线程的应用

但是在写线程池的代码之前,我们需要先写一个日志的代码,为我们后面的线程池做铺垫。

二、日志介绍

什么是日志

计算机中的日志就是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。

日志格式以下几个指标是必须得有的

  • 时间戳
  • 日志等级
  • 日志内容

以下几个指标是可选的

  • 文件名行号
  • 进程,线程相关 id 信息等

它们的作用如下:

日志每天都会更新吗?每天都会产生新的吗?

能查找以前的日志吗?

平均每天产生多少日志?

日志轮转的核心逻辑

我们可以看一下 Linux /var/log/kern.log 路径下的内核日志:

/var/log/kern.log 是 Ubunt 专属的内核日志文件,专门记录内核(Kernel)运行时产生的所有事件,是排查硬件故障、驱动问题、系统崩溃、内核级异常的核心依据。我们执行的命令 sudo cat /var/log/kern.log 就是直接查看该文件的完整内容。

日志实现方案

我们既可以用成熟开源库:spdlog、glog、Boost.Log、Log4cxx 等等,也可以自定义实现日志,我们今天就自己自定义实现一个日志,后面的项目中我们就可以直接使用成熟开源的日志库。

介绍设计模式和策略模式

如果说日志是最终实现的运行记录功能

那设计模式就是整个日志系统的代码设计架构方式

而策略模式是一种具体的设计模式。核心思想就是把不同的 "实现方式" 封装成独立的策略类,运行时动态选择。

我们今天要设计的日志格式如下:

三、日志代码的实现

我们今天的日志代码设计为 .hpp 的格式,并且 .hpp 格式有以下好处:

  1. 日志是工具类、通用功能,整个项目都要用
  2. 头文件(.hpp)可以直接包含,不用链接 .o 文件
  3. 多线程安全、全局唯一,放头文件最方便
  4. C++ 项目惯例:工具类、通用函数、小模块都放 .hpp
  5. 我们可以直接 #include "Log.hpp" 就能用

大体框架:

获取日志时间

geitimeofday

这是 Linux/POSIX 系统下获取高精度系统时间的核心系统调用,获取当前系统时间,精度可达微秒级(1μs = 10⁻⁶秒),远高于 time() 函数的秒级精度。是 Linux 下高并发、高性能场景(日志、性能统计、定时器)的标准时间获取方案。

第一个参数 struct timeval *tv 是输出型参数,用于存储获取到的时间,函数执行后,结构体被填充时间数据。tv_sec 用于生成年月日时分秒。tv_usec 对应日志的微秒后缀,实现 YYYY-MM-DD HH:MM:SS. µµµµµµ 格式。

第二个参数 struct timezone *tz 也是一个输出型参数,用于获取系统时区信息,但现代系统几乎不再使用,标准用法直接传 NULL。

localtime_r

下面我们再来学习一个获取时间的系统调用 localtime_r

我们刚才讲的 gettimeofday 是用来拿原始时间戳,而 localtime_r 是把时间戳转成可读的年月日的,二者是上下游配合关系;而 localtime 是它的线程不安全版本,在多线程里不能使用。

localtime_r 的第一个参数 const time_t *timep 是我们要转换的时间戳(从1970年到现在的秒数),一般来自 time(NULL) 或 gettimeofday 的 tv_sec ,这个参数的类型是一个指针 time_t* ,指向秒数。

第二个参数 struct tm *result 是一个结构体指针,用来存放转换后的「年月日时分秒」的结构体,我们可以定义一个输出缓冲区,把结构体里的结果写到缓冲区中。

我们来看一下这个结构体 tm :

返回值成功返回指向 struct tm 结构体的指针,失败则返回 NULL。

代码实现

我们用来获取时间的函数是 GetCurrentTime(),这段代码的核心目标是获取高精度、线程安全、格式规范的本地时间字符串,为日志系统提供标准时间戳,完整逻辑拆解如下:

  1. 高精度时间获取:通过 gettimeofday 系统调用,获取当前系统的秒级时间戳 tv_sec 和微秒级时间 tv_usec,实现 YYYY-MM-DD HH:MM:SS.µµµµµµ 的微秒级精度,远高于普通秒级时间函数,满足高并发日志的时间区分需求;同时用 (void)n; 显式忽略返回值,消除编译器未使用变量警告。
  2. 线程安全时间转换:使用可重入函数 localtime_r,将 gettimeofday 获取的秒级时间戳,转换为 struct tm 结构体格式的本地日历时间。区别于非线程安全的 localtime,localtime_r 要求用户传入自定义的 struct_time 结构体作为缓冲区,每个线程拥有独立的内存空间,彻底避免多线程下的数据竞争与时间错乱,完美适配多线程日志场景。
  3. 规范格式拼接:定义 char timestr[128] 作为字符串缓冲区,通过 snprintf 安全拼接时间字段:对 struct tm 结构体的偏移字段做修正:tm_year + 1900 还原真实年份、tm_mon + 1 还原真实月份;用 %04d-%02d-%02d %02d:%02d:%02d.%ld 格式化字符串,保证年份 4 位、月 / 日 / 时 / 分 / 秒 2 位补零,微秒部分直接拼接,最终生成符合行业标准的日志时间格式;snprintf 配合缓冲区大小,避免内存越界,保证代码安全性。
  4. 返回结果:将拼接完成的时间字符串作为函数返回值,供日志系统直接调用。

我们在测试代码中调用并打印一下:

运行结果完美验证了时间戳函数的全部设计目标:

  1. 格式完全合规:输出的时间字符串严格遵循 YYYY-MM-DD HH:MM:SS.µµµµµµ 标准格式,如 2026-04-13 21:08:17.924184,年份、月份、日期、时分秒均补零对齐,微秒部分完整保留,满足日志系统的格式要求,便于后续日志分析工具解析。
  2. 时间连续递增:4 次打印的时间戳秒数依次为 17、18、19、20,严格对应 sleep(1) 的 1 秒间隔,验证了时间获取的准确性;微秒数分别为 924184、924313、924419、924525,符合程序运行的实际耗时,证明 gettimeofday 的微秒级精度生效。
  3. 线程安全验证:单线程测试下时间无错乱,结合 localtime_r 的可重入特性,可保证多线程环境下时间戳的正确性,无时间覆盖,满足日志的核心要求。

策略模式

我们要做一个日志系统,但是未来可能:

  1. 既想打印到屏幕显示器(控制台)
  2. 又想写入到文件
  3. 甚至以后改成网络上报

就会出现多种不同的输出方式,策略模式就是为了让我们在换日志输出方式时,不用改其他业务代码。就像耳机接口 → 插耳机是一个策略 → 插音箱是另一个策略 → 而不需要重新改造你的手机。

最上层:策略接口 LogStrategy

我们先设计最上层的策略接口 LogStrategy

  1. 这个抽象策略相当于一个 "统一标准"。

  2. 第一行的 virtual ~LogStrategy() = default 保证子类正确析构。因为后面会有子类继承这个类,如果用基类指针删除子类对象,析构函数必须是 virtual。= default 表示编译器自动生成默认析构。

  3. 第二行的 virtual void SyncLog(const std::string &message) = 0 是纯虚函数,代表 LogStrategy 是抽象类,不能实例化,只能被继承。所有子类(控制台显示器、文件、网络)都必须自己实现 SyncLog。

下层:具体策略实现 ConsoleStrategy

  1. 这是具体的打印策略,我们首先先往显示器上打印。

  2. class ConsoleStrategy : public LogStrategy 表示 ConsoleStrategy 是 LogStrategy 的一个具体实现。也就是说 ConsoleStrategy 是 "控制台显示器输出" 一种具体策略。

  3. void SyncLog(const std::string &message) override,override 表示子类要重写基类的纯虚函数,强制在子类中必须实现 SyncLog。所有使用 LogStrategy 指针的地方,都可以调用 SyncLog 而不关心是哪个子类。

  4. LockGuard lockguard(_mutex),就是我们之前用的 Mutex + LockGuard 多线程安全锁。因为多线程同时写日志 → 会导致输出穿插、乱码。因此这里每个策略对象自己有一个 mutex 来保证线程安全。

  5. std::cerr << message << std::endl 把日志打印到终端。用 cerr 而不是 cout 是因为 cerr 是标准错误输出流,不会在缓冲区进行缓冲,而是直接实时打印。

  6. ~ConsoleStrategy() {} 是子类析构函数。因为基类析构是 virtual,所以子类析构也是安全的。

  7. Mutex _mutex 是每一个策略对象内部自己的锁。保证多个线程同时调用 SyncLog 不会出现日志乱掉。

整体流程与使用方式如下:

步骤 1:定义接口

步骤 2:实现具体策略

步骤 3:外部使用

如果未来我们想换成文件日志,只加一个新类:

然后 main 改成:

从而完美的体现策略模式的优点

下层:具体策略实现 FileLogStrategy

这是策略模式中文件日志的具体实现类,继承自抽象接口 LogStrategy,负责把日志消息持久化写入本地文件,和之前的 ConsoleStrategy(控制台打印)是一对兄弟策略,共享同一个接口规范,可互相替换、动态切换。

定义日志的默认存储路径和默认文件名,用户不传参数时自动使用,避免空路径、空文件名的错误。./log 表示当前目录下的 log 文件夹,log.txt 是默认日志文件名。

继承抽象策略接口 LogStrategy,必须实现父类的纯虚函数 SyncLog,符合策略模式的统一接口规范。让 FileLogStrategy 可以被 LogStrategy* 基类指针指向,实现多态,和 ConsoleStrategy 无缝切换。

构造函数参数:

  1. const std::string &path = defaultpath:默认参数,用户不传路径时用 ./log

  2. const std::string &name = defaultfilename:默认参数,用户不传文件名时用 log.txt

  3. 初始化列表 : _logpath(path), _logfilename(name):直接初始化成员变量,比构造函数内赋值更高效。

  4. LockGuard lockguard(_mutex) 是RAII 风格加锁,保证构造函数的多线程安全(避免多线程同时创建目录时的竞争),和 ConsoleStrategy 的锁逻辑一致。

  5. if (std::filesystem::exists(_logpath)) 检查日志目录是否已经存在:如果存在,直接返回,不做任何操作;如果不存在,才执行创建逻辑,避免重复创建。std::filesystem 是 C++17 引入的文件系统标准库,跨平台、安全,替代了老旧的 mkdir 系统调用。

  6. std::filesystem::create_directories(_logpath) 递归创建日志目录:比如路径是 ./log/2026/04,会自动创建所有不存在的父目录,不会报错。用 try-catch 包裹,捕获目录创建失败的异常,避免程序崩溃。catch (const std::filesystem::filesystem_error &e) 捕获文件系统相关的异常,用 std::cerr 打印错误信息,保证程序不会因为目录创建失败而崩溃。

SyncLog 核心日志写入函数(重写父类接口) :

  1. override 关键字明确表示重写父类 LogStrategy 的 SyncLog 虚函数,编译器会做类型检查,避免写错函数名/参数,保证多态正确性。

  2. LockGuard lockguard(_mutex) 是多线程安全核心:RAII 加锁,保证多个线程同时写日志时,不会出现文件写入乱序、数据损坏。

自动补全路径分隔符:如果用户传入的路径是 ./log(末尾没有 /),会自动加上 /,避免拼接成 ./loglog.txt 这种错误路径,保证路径正确性。

把目录和文件名拼接成完整路径,比如 ./log/log.txt,作为文件写入的目标。

  1. std::ofstream:文件输出流,用于写入文件。

  2. std::ios::app:追加写入模式,核心中的核心。每次打开文件,都会从文件末尾开始写入,不会覆盖原有日志,保证历史日志不丢失,是日志文件的标准打开方式。

检查文件是否成功打开:如果打开失败,打印错误信息,直接返回,避免程序崩溃。

1. out << message << "\n" 把日志消息写入文件,加换行符保证每条日志占一行,可读性拉满。

  1. out.close() :手动关闭文件,释放文件句柄,避免句柄泄漏。

析构函数:因为成员变量(_logpath、_logfilename、_mutex)都是自动管理的,不需要手动释放资源,所以留空即可。父类 LogStrategy 有虚析构函数,所以子类析构函数会被正确调用,保证多态析构安全。

1. _logpath:日志存储目录,成员变量保存,避免每次调用 SyncLog 都传路径。

  1. _logfilename:日志文件名,成员变量保存,和路径对应。

  2. _mutex:互斥锁,保证多线程写文件的安全性,每个策略对象自己持有锁。

我们再回顾一下基类和子类

  1. 父类特征

父类中包含两个纯虚函数,它没有具体的函数实现,只定义接口规范。包含纯虚函数的类是抽象类,不能直接实例化,只能被继承。

  1. 子类责任

所有子类必须重写父类的每一个纯虚函数,提供各自的具体实现。如果子类不重写,它也会变成抽象类,无法实例化。

  1. 多态调用

完成上述设计后,就可以通过父类的指针或引用统一调用不同子类的重写函数。程序运行时会根据实际指向的子类对象,自动调用对应的实现,这就是多态。

日志类

初始框架

Logger 类是整个日志系统的核心入口,负责统一管理日志策略、对外提供日志接口。

_strategy 用 std::unique_ptr 智能指针持有 LogStrategy 基类指针,指向具体的策略对象(ConsoleStrategy/FileLogStrategy)。智能指针自动管理内存,避免手动 new/delete 造成的内存泄漏,是 C++ 现代写法的标准实践。基类指针实现多态:不管指向哪个子类,都能通过 SyncLog 统一调用。

Logger 构造时,默认调用 UseConsoleStrategy(),把初始策略设为控制台打印。

std::make_unique<ConsoleStrategy>() 创建 ConsoleStrategy 对象,智能指针自动接管。赋值给 _strategy 时,旧的策略对象会被自动销毁,内存安全。这样运行时会动态切换日志输出方式,业务代码不用改,只需要调用这两个方法即可。

对外暴露的唯一日志接口,业务代码只需要调用 logger.Debug("日志内容") 即可打印日志。

_strategy->SyncLog(message) 会根据当前指向的策略对象,自动调用对应的实现。

整个项目中,所有代码都用这一个 logger 对象打日志,保证日志策略统一、多线程安全。避免到处创建 Logger 对象,造成策略混乱、资源浪费。项目中任意的 .cpp 文件,只要包含头文件,就能直接用 logger 打日志,无需传递对象、无需重复初始化。整个项目所有文件、所有线程、所有函数,全部能用。

把策略切换方法封装成宏,简化调用。原来的 logger.UseConsoleStrategy() 改成现在的ENABLE_CONSOLE_LOG_STRATEGY(),代码更简洁,可读性更强

FILE:编译器内置宏,自动替换为当前代码的文件名

LINE:编译器内置宏,自动替换为当前代码的行号

调用 logger 时,自动把日志等级、文件名、行号传入,生成标准日志格式

下面我们在测试文件中进行测试 :

我们先来分析一下整个的调用逻辑:

程序启动时全局对象初始化,在 main 函数执行之前,全局对象 logger 就已经完成构造。随后在构造函数中调用 UseConsoleStrategy(),创建 ConsoleStrategy 对象,用 std::unique_ptr<LogStr ategy> 智能指针持有。此时 _strategy 指向 ConsoleStrategy,默认日志输出到控制台。

第一阶段控制台日志输出 ENABLE_CONSOLE_LOG_STRATEGY() 会进行宏展开 → logger.UseConsoleStrategy(),函数内部会用用 std::make_unique 创建新的 ConsoleStrategy 对象,赋值给 _strategy。如果有旧的策略对象被智能指针自动销毁,保证内存安全。此时 _strategy 明确指向 ConsoleStrategy,日志输出目标为控制台。然后进行 5 次重复调用logger.Debug ("console strategy!"),Logger::Debug 方法被调用,形参为日志消息 "console strategy!" 检查 _strategy 非空,执行多态调用 _strategy->SyncLog(message),因为 _strategy 指向 ConsoleStrategy,实际调用 ConsoleStrategy::SyncLog,在 ConsoleStrategy::SyncLog 内部会先LockGuard 加锁,保证多线程安全,std::cerr << message << std::endl 把日志打印到终端,之后锁自动释放,完成一次日志输出。

第二阶段文件日志输出 ENABLE_FILE_LOG_STRATEGY() 进行宏展开 logger.UseFileStrategy() ,创建新的 FileLogStrategy 对象,赋值给 _strategy。原 ConsoleStrategy 对象被智能指针自动销毁。FileLogStrategy 构造函数执行检查 ./log 目录是否存在,不存在则创建。初始化日志路径 ./log/log.txt,准备写入。此时 _strategy 指向 FileLogStrategy,日志输出目标为本地文件。随后进行 5 次重复调用 logger.Debug("file strategy!"),Logger::Debug 方法被调用,入参为 "file strategy!",检查 _strategy 非空,执行多态调用 _strategy->SyncLog(message),因为 _strategy 指向 FileLogStrategy,实际调用 FileLogStrategy::SyncLog,FileLogStrategy::SyncLog 内部会进行 LockGuard 加锁,保证多线程安全,拼接完整日志路径 ./log/log.tx,以 std::ios::app 追加模式打开文件,写入日志消息 file strategy!,加换行符,关闭文件,锁自动释放,完成一次日志写入。

运行结果 :

第一阶段控制台输出 5 行日志完整输出。

第二阶段的文件输出会在当前目录下建立 log 文件夹,cd 到 log 文件夹内部就会有我们新建的文件log.txt。

cat 打印后就会出现打印内容。

日志生成

下面我们就要构建日志字符串生成日志了

我们要设计的日志格式是这样,左半部分其实是固定格式的,右半部分的消息内容是不固定的,所以我们在构建日志字符串时需要分为两部分来完成。

我们要把整个逻辑继续封装成一个类 LogMessage ,这个类是内部类,即在 Logger 类内部,因为它是 Logger 专用的辅助工具,负责帮 Logger 实现流式日志,并且需要访问 Logger 的私有成员 _strategy 才能最终写日志,同时为了封装安全,不让外部随便创建和乱用,所以设计成内部类。

我们先看构造函数,它是日志左半部分的自动生成器 :

  1. _level:日志等级(DEBUG/INFO/WARNING 等)

  2. _curr_time:调用你之前写的GetCurrentTime(),生成微秒级时间戳

  3. _pid:getpid()获取当前进程 ID,多进程日志可区分

  4. filename/line:从LOG(level)宏传入的__FILE/LINE,自动补全代码位置

  5. _logger:引用全局Logger对象,用于后续策略调用

然后用 stringstream 把固定信息拼成标准日志前缀,格式为:

_loginfo 把前缀存入成员变量,等待用户用<<追加自定义内容

这是重载 operator<< 函数,template <typename T>让这个运算符支持任意类型的输入,把用户传入的内容转成字符串,拼接到 _loginfo 末尾,return *this 是链式调用的关键,让 a << b << c 可以连续执行,每次都返回同一个LogMessage对象。

这是析构函数,结合了 RAII 风格 ,当 LOG(level) << ...这个表达式执行完毕后,临时LogMessage 对象会自动销毁,析构函数自动执行,通过 _logger._strategy 基类指针,调用当前生效的策略的SyncLog方法,把完整日志写入目标。

_loginfo 用来存储最终完整的日志字符串(前缀 + 用户内容)

在这个内部类结束后我们继续写 Logger 类的operator()重载:这是临时对象的生成器,核心作用就是当 LOG(level) 宏展开为 logger(level, FILE, LINE) 时,调用这个运算符生成一个临时 LogMessage 对象,完成前缀拼接,返回给用户进行 << 链式调用,传 *this 把全局Logger 对象的引用传入 LogMessage,用于后续析构时的策略调用。

Main.cc 测试

程序启动后,全局 Logger 对象 logger 率先完成构造,默认绑定控制台日志策略并初始化相关资源;随后进入 main 函数,首先执行 ENABLE_FILE_LOG_STRATEGY() 宏,该宏展开后调用logger.UseFileStrategy(),通过智能指针将全局日志策略切换为 FileLogStrategy,FileLogStrategy的构造函数会自动检查并创建 ./log 目录,完成文件日志的初始化准备。接下来连续执行 5 条LOG(LogLevel::XXX) << "hell world" << " 3.14 " << 109 << " hello bit" 语句:每条 LOG 宏会展开为 (logger(level, FILE, LINE)) ,触发 Logger的operator() ,生成一个临时 LogMessage内部类对象;该对象在构造时自动拼接日志的固定前缀,包含微秒级时间戳、对应日志等级字符串、当前进程 ID、Main.cc 文件名与当前代码行号,随后通过重载的模板 operator<<,依次将字符串"hell world"、" 3.14 "、整数109、字符串" hello bit"拼接到日志内容中,每次 << 都返回LogMessage 对象自身以支持链式调用;当整条日志语句执行完毕后,临时 LogMessage 对象生命周期结束触发析构,在析构函数中通过 logger 的引用访问其私有 _strategy 指针,调用FileLogStrategy 的 SyncLog 方法,将完整日志以追加模式写入 ./log/log.txt 文件,5 条不同等级的日志依次完成文件写入。

完成文件日志阶段后,执行 ENABLE_CONSOLE_LOG_STRATEGY() 宏,展开后调用logger.UseConsoleStrategy(),将全局策略切换为 ConsoleStrategy ,原 FileLogStrategy 对象由智能指针自动释放。随后再次连续执行 5 条 LOG 语句,流程与文件阶段完全一致:LOG宏生成临时 LogMessage 对象,拼接相同格式的日志前缀与用户内容,链式<<完成内容拼接,对象析构时调用 ConsoleStrategy 的 SyncLog 方法,通过 std::cerr 将日志实时打印到控制台终端,5 条不同等级的日志依次输出。所有日志操作完成后,main 函数执行 return 0,程序退出,全局 logger 对象自动析构,智能指针安全释放剩余策略对象,完成整个日志系统的全链路调用流程。

打印结果:

程序运行后,系统自动创建 ./log 目录及 log.txt 文件。由于我们执行对程序进行了两次的运行,因此 log.txt 文件中一共打印了 8 次。

四、线程池

  1. 我们要设计的线程池就是提前创建好一批固定数量的线程,让它们一直活着、随时待命,专门用来执行我们提交的各种任务。

  2. 如果没有线程池的话,按照一般的做法就是每次来一个任务就新建一个线程,任务做完就销毁线程,频繁创建销毁线程非常耗资源、速度慢,还容易让系统卡死。

  3. 所以线程池的思路是一开始就创建好多个线程,让它们先等着,不干活也不退出。当有任务要做时,就把任务放进一个公共的 "任务列表" 里。那些闲着的线程会自己去列表里拿任务来执行。

执行完一个,线程不会死,回去继续等下一个任务。所有任务都执行完了后,线程可以继续等着,也可以统一关闭。

  1. 线程池的核心作用就是 : 1. 减少频繁创建销毁线程的开销,2. 控制同时运行的线程数量,避免系统过载,3. 让任务异步、并行执行,提高效率。

代码实现 :

我们直接展示代码,然后逐个板块分析:

上面这幅图就是 **线程池的主体逻辑,**详细的从构造对象 -> 绑定回调 -> 启动线程 -> 触发回调进入业务循环。上面的图都能直观的体现出来。

回调机制

我们再来单独的说一下回调函数这块,因为回调机制应用非常广泛和常见,我们在后面网络的学习中也能遇到 :

回调就是下层代码回头调用上层代码。上层是我们写的线程池 ThreadPool,下层是封装好的线程类 Thread,正常逻辑是上层调用下层;回调就是下层做完事,回头调用上层的函数。

在我们的代码中就是,上层(ThreadPool)构造线程时,把自己的 HandlerTask 打包成 lambda,传给下层 Thread。下层(Thread)把这个函数存起来,不马上调用。而是等到线程真正跑起来,下层 ThreadRoutine 主动调用这个存好的函数,在 self->_cb() 这行代码执行瞬间,就进行了回调,这一调,就回到了上层的 HandlerTask。

使用回调的最大好处是解耦。Thread 类完全不需要知道 ThreadPool 存在,它只知道自己要执行一个 _cb。ThreadPool 类只需要把任务交给 Thread,不需要关心线程底层怎么创建、怎么调度。

下面我们看线程执行 HandlerTask 函数 :

这里解锁可能是通过两种场景进行解锁的,场景 1 是走 break 下班退出,执行break跳出循环时,代码跳出当前作用域,自动解锁。场景 2 就是正常走完流程,往下执行,走到task = ......取完任务,往下走到函数末尾循环结束一轮,RAII 锁超出作用域后自动解锁。但是不管是break退出,还是正常往下走,全都是 RAII 自动解锁,都没有手动解锁。

只要队列里加入任务(size > 0),就 signal 唤醒线程,被唤醒的线程从条件变量中的等待队列里出来,准备继续运行。被唤醒的线程,不会直接执行 wait 后面的代码,它会在内部自动尝试 pthread _mutex_lock(_mutex)重新抢这把锁,此时锁可能还在生产者手里,也可能在别的消费者手里,但是它必须抢到锁后,wait () 会返回,所以重新抢到锁后 wait 返回时,锁已经在手上了,当代码就会走到下一行 _slaver_sleep_count-- 进行减减,所以顺序是:唤醒→ 阻塞在抢锁→ 抢到锁→ wait () 才返回→ 才执行下面的 _slaver_sleep_count-- 。

条件变量的作用:

当任务队列为空时,没有任务可执行的线程会加入条件变量内部维护的等待队列并进入休眠,同时主动释放互斥锁,避免了线程白等浪费 CPU,也让其他线程能够正常访问共享队列;当生产者向队列添加任务后,通过唤醒操作通知等待队列中的线程重新竞争锁,竞争成功的线程会从等待队列移除并继续执行,获取任务进行处理,既保证了共享资源的线程安全,又实现了线程的精准调度与高效利用。

逻辑梳理:

我们把从 main 函数开始,多个线程一起跑的完整流程重新梳理一下:

我们先明确一下角色 :

  1. main 函数:主线程,也是生产者

  2. 线程池里的 N 个线程:消费者,都在跑 HandlerTask

  3. 任务队列:共享资源缓冲区

  4. 条件变量:管理睡觉线程的等待队列

  5. 锁:保护队列不被同时乱改

main 函数完整逻辑 :

  1. main 第一步:创建线程池对象

线程池构造函数跑起来,内部创建 4 个 Thread 对象,每个 Thread 绑定好回调:&ThreadPool::HandlerTask,此时还没有真正的线程,只是对象准备好了

  1. main 第二步:启动线程池

线程池遍历 4 个 Thread,逐个调用 Thread::Start(),每个 Start() 内部调用 pthread_create,系统真正创建 4 个线程,这 4 个线程一出生,就直接进入 HandlerTask

  1. 4 个消费者线程同时开始跑 HandlerTask

它们一进来就:加锁,发现队列为空,进入 while 循环,依次进入条件变量的等待队列睡觉,睡觉前自动释放锁,此时状态:队列空,4 个线程都睡在条件变量的等待队列里,main 线程继续往下跑

  1. main 第三步:扔任务(生产者)

每调用一次 Enqueue:加锁,把任务放进队列,唤醒等待队列里的一个线程,解锁

  1. 多线程同时并发的完整时序(最关键)

我们现在同时串 4 个线程 + main 线程一起跑:

初始状态 : 线程 1、2、3、4 → 都在条件变量等待队列睡觉,任务队列是空的,main 扔第 1 个任务,唤醒线程 1,线程 1 从等待队列移出,去抢锁,线程 1 抢到锁后 wait 返回,线程 1 检查 while:队列不空 → 跳出,线程 1 检查 if:线程池运行 → 不 break,线程 1 取任务,锁释放,线程 1 执行任务,此时线程 1 在干活,线程 2、3、4 还在睡觉

main 扔第 2 个任务,唤醒线程 2,线程 2 抢到锁后,线程 2 取任务 → 解锁 → 执行,此时线程 1、2 都在并行执行任务,3、4 睡觉,main 继续扔任务,直到 10 个任务都进去,每次都会唤醒一个睡觉线程,线程醒来 → 抢锁 → 取任务 → 执行。如果 4 个线程都在忙,没有睡觉的,那唤醒也没用,任务就先在队列里排队。某个线程执行完任务,比如线程 1 执行完了,它会回到 HandlerTask 最外层 while(true) 开头,重新加锁,看队列还有没有任务,有就直接取任务执行,没有就再次进入条件变量等待队列睡觉,直到所有任务执行完,4 个线程执行完最后一个任务后回来加锁,发现队列空,线程池还在运行,全部再次回到条件变量等待队列睡觉,不占 CPU

  1. main 最后:关闭线程池

内部做:_isrunning = false,唤醒所有睡觉线程,所有线程醒来,线程判断:!_isrunning && 队列为空,执行 break,退出循环,线程结束

运行结果 :

本次运行完整验证了线程池从启动、任务调度到销毁的全流程:4个工作线程(New-Thread-1~4)启动后,主线程批量投放加法任务,线程通过条件变量唤醒、互斥锁竞争,依次从任务队列取任务并行执行,实现了线程复用(如New-Thread-1先后处理1+0=1、6+15=21、3+7=10等多轮任务);所有任务执行完毕后,主线程关闭线程池,将运行状态置为false并唤醒所有线程,线程满足退出条件后依次打印quit日志,最终通过pthread_join完成线程资源回收,打印join success日志,整个流程完全符合生产者-消费者模型与多线程并发调度的核心逻辑,验证了线程池的线程安全、高效复用与优雅退出机制。

五、完整代码

ThreadPool.hpp

cpp 复制代码
#pragma once

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

namespace NS_THREAD_POOL_MODULE
{
    using namespace NS_LOG_MODULE;
    using namespace NS_THREAD_MODULE;

    const int defaultnum = 4;

    // 线程池要不要对多个线程进行管理呢??
    // 先描述,在组织!
    template <typename T>
    class ThreadPool
    {
    private:
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T task;
                {
                    // 保护临界区
                    LockGuard lockguard(_mutex);
                    // 检测任务。不休眠:1. 队列不为空 2. 线程池退出 -> 队列为空 && 线程池不退出
                    while (_tasks.empty() && _isrunning)
                    {
                        // 没有任务, 休眠
                        _slaver_sleep_count++;
                        _cond.Wait(_mutex);
                        _slaver_sleep_count--;
                    }
                    // 线程池退出了-> while 就要break -> 不能
                    // 1. 线程池退出 && _tasks empty
                    if (!_isrunning && _tasks.empty())
                    {
                        _mutex.Unlock();
                        break;
                    }
                    // 有任务, 取任务,本质:把任务由公共变成私有
                    // T -> task*
                    task = _tasks.front();
                    _tasks.pop();
                }
                // 处理任务, 约定
                // 处理任务需要再临界区内部处理吗?不需要
                LOG(LogLevel::INFO) << name << "处理任务:";
                task();
                LOG(LogLevel::DEBUG) << task.Result();
            }
            // 线程退出
            LOG(LogLevel::INFO) << name << " quit...";
        }

    public:
        ThreadPool(int slaver_num = defaultnum) : _isrunning(false), _slaver_sleep_count(0), _slaver_num(slaver_num)
        {
            // ThreadPool对象已经存在了
            for (int idx = 0; idx < _slaver_num; idx++)
            {
                _slavers.emplace_back([this]()
                                      { this->HandlerTask(); });
            }
        }
        void Start()
        {
            if (_isrunning)
            {
                LOG(LogLevel::WARNING) << "Thread Pool Is Already Running";
                return;
            }
            _isrunning = true;
            for (auto &slave : _slavers)
            {
                slave.Start();
            }
        }
        void Stop()
        {
            // 1. _isrunning = false
            // 2. 处理完成tasks所有的任务
            // 线程状态: 休眠,正在处理任务 -> 让所有线程全部唤醒
            // HandlerTask自动break
            _mutex.Lock();
            _isrunning = false;
            if (_slaver_sleep_count > 0)
                _cond.Broadcast();
            _mutex.Unlock();
        }
        void Wait()
        {
            for (auto &slave : _slavers)
            {
                slave.Join();
            }
        }
        void Enqueue(T in)
        {
            _mutex.Lock();
            _tasks.push(in);
            if (_slaver_sleep_count > 0)
                _cond.Signal();
            _mutex.Unlock();
        }
        ~ThreadPool()
        {
        }

    private:
        bool _isrunning;
        int _slaver_num;
        std::vector<Thread> _slavers;
        std::queue<T> _tasks; // 任务队列,临界资源
        Mutex _mutex;
        Cond _cond;
        int _slaver_sleep_count;
    };
}

Thread.hpp

cpp 复制代码
#pragma once

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

namespace NS_THREAD_MODULE
{
    using namespace NS_LOG_MODULE;

    static int gnumber = 1;
    using callback_t = std::function<void()>;

    enum class TSTATUS
    {
        THREAD_NEW,
        THREAD_RUNNING,
        THREAD_STOP
    };

    std::string Status2String(TSTATUS s)
    {
        switch (s)
        {
        case TSTATUS::THREAD_NEW:
            return "THREAD_NEW";
        case TSTATUS::THREAD_RUNNING:
            return "THREAD_RUNNING";
        case TSTATUS::THREAD_STOP:
            return "THREAD_STOP";
        default:
            return "UNKNOWN";
        }
    }

    std::string IsJoined(bool joinable)
    {
        return joinable ? "true" : "false";
    }

    class Thread
    {
    private:
        void ToRunning()
        {
            _status = TSTATUS::THREAD_RUNNING;
        }
        void ToStop()
        {
            _status = TSTATUS::THREAD_STOP;
        }
        static void *ThreadRoutine(void *args)
        {
            Thread *self = static_cast<Thread *>(args);
            pthread_setname_np(self->_tid, self->_name.c_str());
            self->_cb();
            self->ToStop();
            return nullptr;
        }

    public:
        Thread(callback_t cb)
            : _tid(-1), _status(TSTATUS::THREAD_NEW), _joinable(true), _cb(cb), _result(nullptr)
        {
            _name = "New-Thread-" + std::to_string(gnumber++);
        }
        bool Start()
        {
            int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
            if (n != 0)
                return false;

            ToRunning();
            return true;
        }
        void Join()
        {
            if (_joinable)
            {
                int n = pthread_join(_tid, &_result);
                if (n != 0)
                {
                    std::cerr << "join error: " << n << std::endl;
                    return;
                }
                (void)_result;
                _status = TSTATUS::THREAD_STOP;
                LOG(LogLevel::DEBUG) << _name << " join success";
            }
            else
            {
                std::cerr << "error, thread join status: " << IsJoined(_joinable) << std::endl;
            }
        }
        // 暂停
        // void Stop() // restart()
        // {
        //     // 让线程暂停
        // }
        void Die()
        {
            if (_status == TSTATUS::THREAD_RUNNING)
            {
                pthread_cancel(_tid);
                _status = TSTATUS::THREAD_STOP;
            }
        }
        void Detach()
        {
            if (_status == TSTATUS::THREAD_RUNNING && _joinable)
            {
                pthread_detach(_tid);
                _joinable = false;
            }
            else
            {
                std::cerr << "detach " << _name << " failed" << std::endl;
            }
        }
        void PrintInfo()
        {
            std::cout << "thread name : " << _name << std::endl;
            std::cout << "thread _tid : " << _tid << std::endl;
            std::cout << "thread _status : " << Status2String(_status) << std::endl;
            std::cout << "thread _joinable : " << IsJoined(_joinable) << std::endl;
        }

        ~Thread()
        {
        }

    private:
        std::string _name;
        pthread_t _tid;
        TSTATUS _status;
        bool _joinable;
        // 线程要有自己的任务处理,即回调函数
        callback_t _cb;

        // 线程退出信息
        void *_result;
    };
}

Mutex.hpp

cpp 复制代码
#pragma once

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


class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    pthread_mutex_t *Ptr()
    {
        return &_lock;
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

class LockGuard // RAII风格代码
{
public:
    LockGuard(Mutex &lock):_lockref(lock)
    {
        _lockref.Lock();
    }
    ~LockGuard()
    {
        _lockref.Unlock();
    }
private:
    Mutex &_lockref;
};

Cond.hpp

cpp 复制代码
#ifndef __COND_HPP
#define __COND_HPP

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

class Cond
{
public:
    Cond()
    {
        pthread_cond_init(&_cond, nullptr);
    }
    void Wait(Mutex &mutex)
    {
        int n = pthread_cond_wait(&_cond, mutex.Ptr());
        (void)n;
    }
    void Signal()
    {
        int n = pthread_cond_signal(&_cond);
        (void)n;
    }
    void Broadcast()
    {
        int n = pthread_cond_broadcast(&_cond);
        (void)n;
    }
    ~Cond()
    {
        pthread_cond_destroy(&_cond);
    }
private:
    pthread_cond_t _cond;
};

#endif

Logger.hpp

cpp 复制代码
#ifndef __LOGGER_HPP
#define __LOGGER_HPP

#include <iostream>
#include <cstdio>
#include <string>
#include <memory>
#include <sstream>
#include <ctime>
#include <sys/time.h>
#include <unistd.h>
#include <filesystem> // C++17
#include <fstream>
#include "Mutex.hpp"

namespace NS_LOG_MODULE
{
    enum class LogLevel
    {
        INFO,
        WARNING,
        ERROR,
        FATAL,
        DEBUG
    };
    std::string LogLevel2Message(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        case LogLevel::DEBUG:
            return "DEBUG";
        default:
            return "UNKNOWN";
        }
    }

    // 1. 时间戳 2. 日期+时间
    std::string GetCurrentTime()
    {
        struct timeval current_time;
        int n = gettimeofday(&current_time, nullptr);
        (void)n;

        // current_time.tv_sec; current_time.tv_usec;
        struct tm struct_time;
        localtime_r(&(current_time.tv_sec), &struct_time); // r: 可重入函数
        char timestr[128];
        snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
                 struct_time.tm_year + 1900,
                 struct_time.tm_mon + 1,
                 struct_time.tm_mday,
                 struct_time.tm_hour,
                 struct_time.tm_min,
                 struct_time.tm_sec,
                 current_time.tv_usec);
        return timestr;
    }

    // 输出角度 -- 刷新策略
    // 1. 显示器打印
    // 2. 文件写入
    // 策略模式,策略接口
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };
    // 控制台日志刷新策略, 日志将来要向显示器打印
    class ConsoleStrategy : public LogStrategy
    {
    public:
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cerr << message << std::endl; // ??
        }
        ~ConsoleStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    const std::string defaultpath = "./log";
    const std::string defaultfilename = "log.txt";


    // 文件策略
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
            : _logpath(path),
              _logfilename(name)
        {
            LockGuard lockguard(_mutex);
            if (std::filesystem::exists(_logpath))
                return;
            try
            {
                std::filesystem::create_directories(_logpath);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
        }

        void SyncLog(const std::string &message) override
        {
            {
                LockGuard lockguard(_mutex);
                if (!_logpath.empty() && _logpath.back() != '/')
                {
                    _logpath += "/";
                }
                std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
                std::ofstream out(targetlog, std::ios::app);     // 追加方式写入
                if (!out.is_open())
                {
                    std::cerr << "open " << targetlog << "failed" << std::endl;
                    return;
                }
                out << message << "\n";
                out.close();
            }
        }

        ~FileLogStrategy()
        {
        }

    private:
        std::string _logpath;
        std::string _logfilename;
        Mutex _mutex;
    };

    // 日志类:
    // 1. 日志的生成
    // 2. 根据不同的策略,进行刷新
    class Logger
    {
        // 日志的生成:
        // 构建日志字符串
    public:
        Logger()
        {
            UseConsoleStrategy();
        }
        void UseConsoleStrategy()
        {
            _strategy = std::make_unique<ConsoleStrategy>();
        }
        void UseFileStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }
        // 内部类, 标识一条完整的日志信息
        //  一条完整的日志信息 = 做半部分固定部分 + 右半部分不固定部分
        //  LogMessage RAII风格的方式,进行刷新
        class LogMessage
        {
        public:
            LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
                : _level(level),
                  _curr_time(GetCurrentTime()),
                  _pid(getpid()),
                  _filename(filename),
                  _line(line),
                  _logger(logger)
            {
                // 先构建出来左半部分
                std::stringstream ss;
                ss << "[" << _curr_time << "] "
                   << "[" << LogLevel2Message(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _filename << "] "
                   << "[" << _line << "] "
                   << " - ";

                _loginfo = ss.str();
            }
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this; // 返回当前LogMessage对象,方便下次继续进行<<
            }

            ~LogMessage()
            {
                if (_logger._strategy)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }

        private:
            LogLevel _level;
            std::string _curr_time;
            pid_t _pid;
            std::string _filename;
            int _line;
            std::string _loginfo; // 一条完整的日志信息

            // 一个引用,引用外部的Logger类对象
            Logger &_logger; // 方便我们后续进行策略式刷新
        };

        // 这里已经不是内部类了
        // 故意采用拷贝LogMessage
        LogMessage operator()(LogLevel level, std::string filename, int line)
        {
            return LogMessage(level, filename, line, *this);
        }

        ~Logger()
        {
        }

    private:
        std::unique_ptr<LogStrategy> _strategy; // 刷新策略
    };

    // 日志对象,全局使用
    Logger logger;

#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();

#define LOG(level) logger(level, __FILE__, __LINE__)

}

#endif

Makefile

cpp 复制代码
threadpool:Main.cc
	g++ -o $@ $^ -std=c++17 -g
.PHONY:clean
clean:
	rm -f threadpool

Main.cc

cpp 复制代码
#include "Logger.hpp"
#include "ThreadPool.hpp"

#include <iostream>
#include <memory>
#include <functional>
#include <ctime>
#include <cstdlib>

using namespace NS_LOG_MODULE;
using namespace NS_THREAD_POOL_MODULE;

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

class Task
{
public:
    Task(){}
    Task(int x, int y):_x(x), _y(y)
    {}
    void operator()()
    {
        _result = _x + _y;
    }
    std::string Result()
    {
        return std::to_string(_x) + "+" + std::to_string(_y) + "=" + std::to_string(_result); 
    }
    ~Task(){}
private:
    int _x;
    int _y;
    int _result;
};


int main()
{
    ENABLE_CONSOLE_LOG_STRATEGY();
    srand((long)time(nullptr) ^ getpid());
    std::unique_ptr<ThreadPool<Task>> tp = std::make_unique<ThreadPool<Task>>();
    
    tp->Start();

    int cnt = 10;
    while (cnt--)
    {
        int x = rand() % 10 + 1;
        usleep(137);
        int y = rand() % 20;
        Task t(x, y);
        tp->Enqueue(t);
        sleep(1);
    }

    tp->Stop();
    tp->Wait();

    return 0;
}

六、总结

本文介绍了线程池和日志系统的设计与实现。线程池采用池化技术复用线程,减少频繁创建销毁的开销,通过任务队列和条件变量实现生产者-消费者模型,支持多线程并发任务处理。日志系统基于策略模式设计,支持控制台和文件两种输出方式,提供线程安全的高精度时间戳记录功能。两者结合实现了高效的任务调度和日志记录,其中线程池通过智能指针管理资源,日志系统采用RAII风格确保资源释放。测试结果表明系统能正确处理并发任务并输出格式规范的日志,验证了设计的正确性和可靠性。

谢谢大家的观看!

相关推荐
埃伊蟹黄面1 小时前
数据链路层
服务器·网络
python_DONG1 小时前
响应面法(Response Surface Methodology, RSM)单目标优化算法
算法·数学建模
6Hzlia1 小时前
【Hot 100 刷题计划】 LeetCode 108. 将有序数组转换为二叉搜索树 | C++ 分治法详解
c++·算法·leetcode
兩尛2 小时前
c++面试常问2
开发语言·c++·面试
华清远见IT开放实验室2 小时前
嵌入式系统化课程 学习内容与服务说明
linux·stm32·学习·嵌入式·全栈·虚拟仿真·测评中心
Rust研习社2 小时前
添加依赖库时的 features 是什么?优雅实现编译期条件编译与模块化开发
开发语言·后端·rust
圆山猫2 小时前
[Linux] Ubuntu 26.04 换阿里云镜像源(最新方法)
linux·ubuntu·阿里云
Tel199253080042 小时前
ENDAT2.2 协议信号转 SSI /BISS-C转换卡 ENDAT2.2 协议信号转DMC多摩川高速协议转换器 互转卡
c语言·开发语言·网络
Dillon Dong2 小时前
【系列主题】从 Docker 构建失败看依赖隔离:多阶段构建的“隐形陷阱”
运维·docker·容器