Linux C/C++ 学习笔记:线程池与日志系统

这部分内容是从“线程同步 → 生产者消费者 → 线程池 → 单例模式 → RAII → 日志系统 → C++ 面向对象设计”逐步串起来的。

阅读这份笔记时不要把它当成知识点清单,而应该把它理解成:为什么需要这些东西,以及它们是怎么一步一步组合起来的。


一、从生产者消费者模型理解线程池

前面学习线程同步时,我们已经知道:

多个线程同时访问共享资源,如果不进行同步,就可能出现数据竞争。

线程池其实就是把这个问题进一步工程化。

假设程序不断产生任务:

任务1
任务2
任务3
任务4
...

最简单的想法是:

来一个任务
    ↓
创建一个线程
    ↓
执行任务
    ↓
销毁线程

但是如果任务非常多,就会频繁创建和销毁线程,成本比较高。

于是我们可以反过来:

程序启动
   ↓
提前创建多个线程
   ↓
线程一直存在
   ↓
等待任务
   ↓
拿到任务就执行
   ↓
执行完继续等待

这就是线程池

而“任务从哪里来”呢?

我们准备一个共享任务队列:

                    生产者
                      │
                      │ Enqueue(task)
                      ↓
                ┌─────────────┐
                │   任务队列   │
                │ std::queue   │
                └──────┬──────┘
                       │
              ┌────────┼────────┐
              ↓        ↓        ↓
            线程1     线程2     线程3
              │        │        │
              ↓        ↓        ↓
             执行任务  执行任务  执行任务

所以:

线程池的本质,就是一个多线程的生产者消费者模型。

生产者负责把任务放进任务队列,消费者(工作线程)负责从任务队列取任务并执行。


二、为什么线程池需要互斥锁?

任务队列是所有线程共享的:

std::queue<T> _taskq;

生产者可能同时:

_taskq.push(task);

消费者可能同时:

_taskq.front();
_taskq.pop();

如果多个线程同时修改 std::queue,就可能发生数据竞争。

因此:

任务队列
   │
   └── 共享资源
          ↓
       互斥锁保护

也就是说,访问任务队列的临界区必须加锁。

但问题又来了:

如果没有任务,消费者难道要一直拿着锁检查队列吗?

当然不能。

于是就引出了条件变量


三、条件变量:让没有任务的线程睡眠

假设线程池暂时没有任务。

如果线程这样写:

while (_taskq.empty())
{
    // 不断检查
}

这叫忙等(busy waiting)

线程虽然没有真正执行任务,但是一直占用 CPU。

更合理的方式是:

任务队列为空
    ↓
线程进入等待
    ↓
释放互斥锁
    ↓
线程睡眠
    ↓
生产者加入任务
    ↓
唤醒一个消费者
    ↓
消费者重新获得锁
    ↓
检查任务队列

这就是条件变量的作用。

线程池中的核心代码:

while (_taskq.empty() && _isrunning)
{
    _sleepernum++;
    _cond.Wait(_mutex);
    _sleepernum--;
}

这里有两个条件:

_taskq.empty()

表示:

当前没有任务。

以及:

_isrunning

表示:

线程池还没有要求退出。

只有:

没有任务
+
线程池还在运行

两个条件同时成立,线程才应该安心睡眠。


四、为什么 wait() 会和互斥锁一起使用?

这是条件变量最容易被问到的地方。

假设线程已经拿到了 _mutex

LockGuard lockguard(_mutex);

然后发现:

_taskq.empty()

如果线程直接睡眠,却没有释放 _mutex,生产者就无法获得这把锁,也就无法向任务队列中加入任务。

因此 wait() 必须完成一个很关键的操作:

线程持有 mutex
       ↓
调用 wait()
       ↓
释放 mutex
       ↓
进入睡眠
       ↓
被唤醒
       ↓
重新竞争 mutex
       ↓
获得 mutex
       ↓
wait() 返回

所以可以记住:

条件变量的 wait 会在等待期间释放互斥锁,被唤醒后重新竞争这把锁。

这也是为什么条件变量通常和互斥锁成对出现。


五、为什么这里必须使用 while,不能简单使用 if

线程池里经常看到:

while (_taskq.empty() && _isrunning)
{
    _cond.Wait(_mutex);
}

而不是:

if (_taskq.empty() && _isrunning)
{
    _cond.Wait(_mutex);
}

这是非常典型的面试题。

原因主要有两个。

1. 虚假唤醒

条件变量允许线程在没有真正获得目标条件的情况下被唤醒。

所以:

线程被唤醒 ≠ 条件一定满足。

醒来以后必须重新检查。

2. 多个线程竞争同一个任务

例如任务队列只有一个任务:

任务队列:[Task1]

线程A:等待
线程B:等待
线程C:等待

如果某次唤醒导致 A、B 都重新竞争:

A 获得锁
 ↓
取走 Task1
 ↓
队列为空
 ↓
释放锁

B 获得锁
 ↓
发现队列已经空了

因此 B 不能认为:

“我刚才被唤醒,所以肯定有任务。”

必须重新检查:

while (_taskq.empty())
{
    _cond.Wait(_mutex);
}

所以面试可以直接回答:

条件变量被唤醒后,条件不一定满足,因此需要使用 while 循环重新检查条件。


六、生产者和消费者到底分别做什么?

生产者:Enqueue

生产者把任务放进队列:

Enqueue(task);

逻辑是:

判断线程池是否运行
      ↓
加锁
      ↓
任务进入队列
      ↓
如果存在休眠线程
      ↓
唤醒一个消费者
      ↓
解锁

消费者:HandlerTask

工作线程不断:

加锁
 ↓
检查任务队列
 ↓
没有任务 → wait
 ↓
有任务 → 取出任务
 ↓
解锁
 ↓
执行任务
 ↓
再次循环

于是整个线程池就形成了完整的生产者消费者模型。


七、为什么取出任务以后必须尽快释放锁?

这是线程池设计中非常重要的一点。

假设:

LockGuard lockguard(_mutex);

t = _taskq.front();
_taskq.pop();

t();

看起来没有问题,但实际上有一个很严重的问题:

执行任务的时候还持有互斥锁。

如果 t() 执行 10 秒,那么:

线程1:
拿锁
 ↓
取任务
 ↓
执行10秒
 ↓
释放锁

这10秒期间,其他线程无法访问任务队列。

线程池虽然有多个线程,但实际上又被这把锁限制住了。

正确思路是:

{
    LockGuard lockguard(_mutex);

    t = _taskq.front();
    _taskq.pop();
}

t();

也就是:

共享资源操作
    ↓
加锁
    ↓
取出任务
    ↓
立刻解锁
    ↓
执行任务

为什么这样可以?

因为:

_taskq

是共享资源,而:

t

在任务被取出来以后,就属于当前工作线程自己的局部变量。

所以:

只需要保护任务队列,不需要在执行任务的整个过程中持有队列锁。

这就是所谓:

缩小临界区 / 缩小锁的粒度。


八、signalbroadcast

任务来了,通常不需要把所有线程都叫醒。

例如:

线程1:睡眠
线程2:睡眠
线程3:睡眠
线程4:睡眠
线程5:睡眠

来了1个任务

只需要:

_cond.Signal();

唤醒一个消费者即可。

如果来了很多任务,可以不断唤醒线程。

Broadcast() 是:

把所有正在等待的线程都唤醒。

它在线程池停止的时候尤其重要。


九、为什么 Stop 的时候必须唤醒所有线程?

这是线程池最经典的面试题之一。

假设:

线程1:Wait()
线程2:Wait()
线程3:Wait()
线程4:Wait()
线程5:Wait()

此时任务队列为空。

主线程执行:

_isrunning = false;

如果什么都不做,那么这些线程仍然睡在:

_cond.Wait(_mutex);

它们根本没有机会重新检查:

_isrunning

于是后面:

thread.Join();

就可能一直等。

所以停止线程池必须:

_isrunning = false;
WakeUpAllThread();

完整过程:

Stop()
  ↓
_isrunning = false
  ↓
Broadcast()
  ↓
所有休眠线程被唤醒
  ↓
重新获得 mutex
  ↓
检查:
!_isrunning && _taskq.empty()
  ↓
退出线程
  ↓
Join()

因此:

Stop 负责发出退出通知并唤醒阻塞线程,Join 负责等待线程真正退出。


十、线程池的退出为什么还要检查任务队列?

线程池停止时不能简单:

if (!_isrunning)
    break;

因为可能出现:

_isrunning = false
_taskq 里面还有任务

如果此时直接退出:

队列里的任务就丢失了。

所以代码通常使用:

if (!_isrunning && _taskq.empty())
{
    break;
}

这意味着:

线程池已经停止运行,并且任务队列已经处理完了,线程才真正退出。

这是一种比较合理的优雅退出思路。


十一、线程池中的 _sleepernum

线程池会记录:

int _sleepernum;

表示:

当前正在等待条件变量的线程数量。

例如:

5个工作线程

线程1:工作
线程2:sleep
线程3:sleep
线程4:sleep
线程5:sleep

_sleepernum = 4

任务到来时:

if (_sleepernum > 0)
{
    _cond.Signal();
}

就可以只唤醒一个休眠线程。

这里体现的是一种优化:

有任务就尽量唤醒一个能够处理任务的线程,而不是每次都广播所有线程。


十二、线程池生命周期

线程池完整生命周期可以理解成:

构造 ThreadPool
      ↓
创建 Thread 对象
      ↓
Start()
      ↓
工作线程开始执行 HandlerTask()
      ↓
等待任务
      ↓
获取任务并执行
      ↓
...
      ↓
Stop()
      ↓
Broadcast()
      ↓
休眠线程被唤醒
      ↓
线程退出
      ↓
Join()
      ↓
线程池结束

这里一定要区分:

Start

让线程真正开始运行。

Stop

告诉线程池:“不要再继续运行了”。

Join

主线程等待工作线程真正结束。


十三、为什么线程池经常使用单例?

如果程序到处都创建线程池:

ThreadPool A → 5个线程
ThreadPool B → 5个线程
ThreadPool C → 5个线程

线程数量会不断增加。

通常一个程序只需要一个统一的任务调度中心,所以可以设计为单例:

ThreadPool<T>::GetInstance();

这样:

整个程序
   ↓
唯一线程池
   ↓
统一任务队列
   ↓
统一工作线程

十四、懒汉单例和双重检查

线程池采用的是懒汉思想:

第一次使用的时候才创建对象。

例如:

static ThreadPool<T>* GetInstance()
{
    if (inc == nullptr)
    {
        LockGuard lockguard(_lock);

        if (inc == nullptr)
        {
            inc = new ThreadPool<T>();
            inc->Start();
        }
    }

    return inc;
}

这里有两次:

if (inc == nullptr)

这叫双重检查

为什么?

假设两个线程同时第一次调用:

线程A ─┐
       ├── 都发现 inc == nullptr
线程B ─┘

如果没有锁:

A:创建对象
B:也创建对象

单例就被破坏了。

加入锁:

A → 加锁 → 创建 → 解锁
B → 等待 → 获得锁
             ↓
          再检查一次
             ↓
          已经存在
             ↓
          不再创建

第一次检查的意义:

对象已经存在时,不需要每次都加锁。

第二次检查的意义:

防止多个线程第一次同时发现对象不存在而重复创建。

所以:

双重检查兼顾了线程安全和运行效率。


十五、为什么单例要禁止拷贝?

如果允许:

ThreadPool a;
ThreadPool b = a;

那么就产生了第二个线程池。

这与“单例”矛盾。

所以:

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

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

禁止:

  • 拷贝构造
  • 赋值

这是单例模式常见写法。


十六、完整线程池代码

#pragma once

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

#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"

namespace ThreadPoolModule
{
    using namespace ThreadModule;
    using namespace LogModule;
    using namespace CondModule;
    using namespace MutexModule;

    static const int gnum = 5;

    template <typename T>
    class ThreadPool
    {
    private:
        ThreadPool(int num = gnum)
            : _num(num)
            , _isrunning(false)
            , _sleepernum(0)
        {
            for (int i = 0; i < num; ++i)
            {
                _threads.emplace_back(
                    [this]()
                    {
                        HandlerTask();
                    }
                );

                LOG(LogLevel::INFO)
                    << "create new thread success";
            }
        }

        // 禁止拷贝
        ThreadPool(const ThreadPool<T>&) = delete;

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

        // 唤醒所有休眠线程
        void WakeUpAllThread()
        {
            LockGuard lockguard(_mutex);

            if (_sleepernum > 0)
            {
                _cond.Broadcast();
            }

            LOG(LogLevel::INFO)
                << "唤醒所有休眠线程";
        }

        // 唤醒一个休眠线程
        void WakeUpOne()
        {
            if (_sleepernum > 0)
            {
                _cond.Signal();

                LOG(LogLevel::INFO)
                    << "唤醒一个休眠线程";
            }
        }

        // 工作线程真正执行的函数
        void HandlerTask()
        {
            char name[1024] = {0};

            pthread_getname_np(
                pthread_self(),
                name,
                sizeof(name)
            );

            while (true)
            {
                T task;

                {
                    LockGuard lockguard(_mutex);

                    // 没有任务,并且线程池还在运行
                    // 工作线程进入等待状态
                    while (_taskq.empty() && _isrunning)
                    {
                        ++_sleepernum;

                        _cond.Wait(_mutex);

                        --_sleepernum;
                    }

                    // 线程池已经停止,并且任务队列为空
                    // 当前线程可以退出
                    if (!_isrunning && _taskq.empty())
                    {
                        LOG(LogLevel::INFO)
                            << name
                            << "退出:线程池停止且任务队列为空";

                        break;
                    }

                    // 从共享任务队列取出任务
                    task = _taskq.front();
                    _taskq.pop();
                }

                // 锁已经释放
                // task 已经属于当前线程,不需要继续持锁
                task();
            }
        }

    public:
        static ThreadPool<T>* GetInstance()
        {
            // 第一次检查
            if (_instance == nullptr)
            {
                LockGuard lockguard(_lock);

                LOG(LogLevel::DEBUG)
                    << "获取线程池单例";

                // 第二次检查
                if (_instance == nullptr)
                {
                    LOG(LogLevel::DEBUG)
                        << "首次使用线程池,创建单例";

                    _instance = new ThreadPool<T>();

                    _instance->Start();
                }
            }

            return _instance;
        }

        // 启动线程池
        void Start()
        {
            if (_isrunning)
            {
                return;
            }

            _isrunning = true;

            for (auto& thread : _threads)
            {
                thread.Start();

                LOG(LogLevel::INFO)
                    << "start new thread success: "
                    << thread.Name();
            }
        }

        // 停止线程池
        void Stop()
        {
            if (!_isrunning)
            {
                return;
            }

            // 通知线程池停止
            _isrunning = false;

            // 唤醒可能正在 Wait 的工作线程
            WakeUpAllThread();
        }

        // 等待所有工作线程退出
        void Join()
        {
            for (auto& thread : _threads)
            {
                thread.Join();
            }
        }

        // 添加任务
        bool Enqueue(const T& task)
        {
            if (!_isrunning)
            {
                return false;
            }

            {
                LockGuard lockguard(_mutex);

                _taskq.push(task);
            }

            // 有任务就唤醒一个休眠线程
            WakeUpOne();

            return true;
        }

        ~ThreadPool() = default;

    private:
        std::vector<Thread> _threads;

        int _num;

        // 共享任务队列
        std::queue<T> _taskq;

        // 条件变量
        Cond _cond;

        // 保护任务队列
        Mutex _mutex;

        // 线程池是否运行
        bool _isrunning;

        // 当前休眠线程数量
        int _sleepernum;

        // 单例对象
        static ThreadPool<T>* _instance;

        // 保护单例创建过程
        static Mutex _lock;
    };

    template <typename T>
    ThreadPool<T>* ThreadPool<T>::_instance = nullptr;

    template <typename T>
    Mutex ThreadPool<T>::_lock;
}

十七、从线程池自然过渡到日志系统

线程池解决的是:

多个线程如何高效地执行任务。

但是线程一多,就会产生另一个问题:

线程1 → LOG()
线程2 → LOG()
线程3 → LOG()
线程4 → LOG()

如果多个线程同时输出日志:

线程1:任务开始
线程2:任务开始
线程1:任务结束
线程3:任务开始

多个线程可能同时访问同一个输出资源。

所以日志系统也必须考虑:

线程安全。

这就是为什么在日志代码里又看到了:

Mutex
LockGuard

你会发现:

前面学的线程同步知识,在日志系统里又真正用起来了。


十八、日志系统到底想解决什么问题?

如果每次打印日志都这样:

std::cout << ...

那么调用者需要自己负责:

  • 时间
  • 日志等级
  • 文件名
  • 行号
  • 进程 ID
  • 输出位置
  • 加锁
  • 文件打开
  • 文件关闭

调用起来非常麻烦。

所以日志系统希望做到:

LOG(LogLevel::DEBUG)
    << "server start, port="
    << 8080;

调用者只关心:

我要记录什么。

至于:

记录到哪里、怎么格式化、怎么加锁

全部交给日志系统。


十九、Logger、LogMessage、LogStrategy 为什么要分开?

日志系统可以拆成三个角色:

                 Logger
                   │
                   │ 管理日志流程
                   ↓
              LogMessage
                   │
                   │ 拼接日志
                   ↓
              LogStrategy
               /       \
              /         \
         Console         File

三个类职责不同。

Logger

负责:

管理当前使用哪一种日志策略。

LogMessage

负责:

形成一条完整的日志消息。

LogStrategy

负责:

具体把日志输出到哪里。

这其实体现了面向对象设计里的一个重要原则:

一个类尽量只负责一类事情。


二十、策略模式

为什么不直接让 Logger 负责:

if (console)
{
    cout...
}
else if (file)
{
    ofstream...
}

因为以后可能还需要:

控制台
文件
网络
数据库
远程日志服务器

Logger 会越来越臃肿。

所以定义抽象接口:

class LogStrategy
{
public:
    virtual ~LogStrategy() = default;

    virtual void SyncLog(
        const std::string& message
    ) = 0;
};

然后:

LogStrategy
   │
   ├── ConsoleLogStrategy
   │
   └── FileLogStrategy

Logger 只保存:

std::unique_ptr<LogStrategy>

这样就可以在运行过程中切换:

EnableConsoleLogStrategy();

EnableFileLogStrategy();

这就是:

策略模式:把可以变化的行为封装成独立策略。


二十一、多态在日志系统里是怎么发生的?

例如:

std::unique_ptr<LogStrategy> _fflush_strategy;

实际可能指向:

ConsoleLogStrategy

也可能指向:

FileLogStrategy

当执行:

_fflush_strategy->SyncLog(message);

到底调用哪个 SyncLog()

由实际对象类型决定。

这就是:

运行时多态。

所以这里其实把你之前学的 C++ 面向对象知识真正用起来了:

抽象基类
   ↓
纯虚函数
   ↓
派生类
   ↓
virtual
   ↓
基类指针/智能指针
   ↓
运行时多态

二十二、为什么基类析构函数要是虚函数?

日志策略是多态基类:

class LogStrategy
{
public:
    virtual ~LogStrategy() = default;
};

为什么?

因为可能存在:

LogStrategy* p = new FileLogStrategy;
delete p;

如果基类析构函数不是虚函数,就可能无法正确调用派生类析构函数。

因此:

作为多态基类时,析构函数通常应该声明为 virtual。

这是 C++ 面试高频题。


二十三、unique_ptr 在日志系统中的作用

Logger 中:

std::unique_ptr<LogStrategy> _fflush_strategy;

它表达了一个非常明确的所有权关系:

Logger
  │
  └── 独占拥有
          ↓
     LogStrategy

切换策略时:

_fflush_strategy =
    std::make_unique<FileLogStrategy>();

旧策略对象会自动释放。

因此不需要:

delete

这就是智能指针和 RAII 的结合。


二十四、RAII:为什么 C++ 特别喜欢它?

RAII 可以简单理解成:

让对象的生命周期管理资源。

例如锁:

{
    LockGuard guard(_mutex);

    // 临界区
}

构造:

LockGuard
   ↓
加锁

析构:

LockGuard
   ↓
解锁

智能指针也是一样:

std::unique_ptr<LogStrategy>

对象离开作用域:

unique_ptr 析构
   ↓
自动释放对象

所以 RAII 不只是一个“锁的技巧”,而是 C++ 非常重要的资源管理思想。


二十五、日志等级为什么使用 enum class

日志等级:

enum class LogLevel
{
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

它表示:

DEBUG
INFO
WARNING
ERROR
FATAL

使用时:

LogLevel::DEBUG

相比普通 enum:

enum LogLevel
{
    DEBUG,
    INFO
};

enum class 是强类型枚举,不容易和其他枚举值发生名字冲突。

所以现代 C++ 中通常更推荐:

enum class

二十六、日志的一条消息是怎么形成的?

调用:

LOG(LogLevel::DEBUG)
    << "value="
    << 100
    << ", name="
    << "Julian";

实际上不是直接输出。

而是:

LOG()
  ↓
创建 LogMessage 临时对象
  ↓
构造日志前缀
  ↓
operator<<("value=")
  ↓
operator<<(100)
  ↓
operator<<(", name=")
  ↓
operator<<("Julian")
  ↓
完整日志形成
  ↓
临时对象生命周期结束
  ↓
LogMessage 析构
  ↓
调用 Logger 的日志策略
  ↓
真正输出

这个设计非常值得理解。


二十七、为什么 operator<< 返回引用?

例如:

LogMessage& operator<<(const T& info)
{
    std::stringstream ss;

    ss << info;

    _loginfo += ss.str();

    return *this;
}

关键:

return *this;

返回当前对象的引用。

所以:

log << "hello" << 123 << 3.14;

可以不断作用在同一个对象上。

这就是链式调用。


二十八、为什么 LogMessage 要保存 Logger 的引用?

LogMessage 的职责:

拼接日志。

Logger 的职责:

管理输出策略。

因此 LogMessage 最后需要:

_logger._fflush_strategy->SyncLog(_loginfo);

所以 LogMessage 保存:

Logger& _logger;

这里使用引用意味着:

LogMessage 不拥有 Logger,只是使用 Logger。

它们的关系可以理解成:

Logger
  ↑
  │ 引用
  │
LogMessage

而不是:

LogMessage
  └── new Logger

否则就会产生错误的对象所有权关系。


二十九、为什么日志刷新放到析构函数?

这是整套日志系统里非常有 C++ 味道的一部分。

例如:

LOG(LogLevel::DEBUG)
    << "hello"
    << 123
    << 3.14;

这里创建的是一个临时 LogMessage

表达式执行过程中:

hello
 ↓
123
 ↓
3.14

不断拼接。

当整个表达式结束:

临时对象生命周期结束
       ↓
调用析构函数
       ↓
SyncLog()

所以析构时日志已经完整。

这相当于利用:

C++ 临时对象生命周期 + RAII

实现了“自动刷新”。


三十、日志为什么需要加锁?

多线程环境:

线程1 ──┐
线程2 ──┤
线程3 ──┤ → 同一个日志文件
线程4 ──┘

如果不加锁,可能导致日志内容互相交错。

所以文件输出策略中:

LockGuard lockguard(_mutex);

保护:

ofstream

相关的写入过程。

控制台输出同样需要考虑多个线程同时写的问题。

因此:

线程池让并发出现了,日志系统就必须考虑并发日志输出。

这就是两个模块之间的实际联系。


三十一、文件日志为什么使用 ios::app

std::ofstream out(
    filename,
    std::ios::app
);

app

append,追加。

意思是:

已有日志:
A
B
C

新的日志:
D

最终:
A
B
C
D

不会因为程序重新启动而直接覆盖之前的日志。


三十二、filesystem 的作用

日志文件需要先确认目录是否存在:

std::filesystem::exists(_path)

如果不存在:

std::filesystem::create_directories(_path)

创建目录。

这是 C++17 filesystem 提供的文件系统操作能力。


三十三、__FILE____LINE__

日志通常需要记录:

[时间]
[等级]
[进程ID]
[源文件]
[行号]
[日志内容]

因此:

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

其中:

__FILE__

表示当前源文件。

__LINE__

表示当前代码所在行号。

例如:

LOG(LogLevel::DEBUG) << "hello";

最终可以得到类似:

[2026-08-25 19:30:00]
[DEBUG]
[12345]
[main.cpp]
[20]
-hello

这对排查问题非常有帮助。


三十四、完整日志系统代码

改代码依赖由C++封装的 Mutex.hppLockGuard

#pragma once

#include <iostream>
#include <string>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <memory>
#include <ctime>
#include <cstdio>
#include <unistd.h>

#include "Mutex.hpp"

namespace LogModule
{
    // ============================================================
    // 日志策略基类
    // ============================================================

    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;

        // 纯虚函数:由具体日志策略实现
        virtual void SyncLog(
            const std::string& message
        ) = 0;
    };


    // ============================================================
    // 控制台日志策略
    // ============================================================

    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy() = default;

        void SyncLog(
            const std::string& message
        ) override
        {
            LockGuard lockguard(_mutex);

            std::cout << message << "\r\n";
        }

        ~ConsoleLogStrategy() override = default;

    private:
        Mutex _mutex;
    };


    // ============================================================
    // 文件日志策略
    // ============================================================

    const std::string defaultpath = "./log";
    const std::string defaultfile = "my.log";
    const std::string gsep = "\r\n";

    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(
            const std::string& path = defaultpath,
            const std::string& file = defaultfile
        )
            : _path(path)
            , _file(file)
        {
            if (std::filesystem::exists(_path))
            {
                return;
            }

            try
            {
                std::filesystem::create_directories(_path);
            }
            catch (const std::filesystem::filesystem_error& e)
            {
                std::cerr << e.what() << '\n';
            }
        }

        void SyncLog(
            const std::string& message
        ) override
        {
            LockGuard lockguard(_mutex);

            std::string filename =
                _path +
                (_path.back() == '/' ? "" : "/") +
                _file;

            // 追加方式打开文件
            std::ofstream out(
                filename,
                std::ios::app
            );

            if (!out.is_open())
            {
                return;
            }

            out << message << gsep;
        }

        ~FileLogStrategy() override = default;

    private:
        std::string _path;
        std::string _file;

        Mutex _mutex;
    };


    // ============================================================
    // 日志等级
    // ============================================================

    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };


    std::string Level2Str(LogLevel level)
    {
        switch (level)
        {
            case LogLevel::DEBUG:
                return "DEBUG";

            case LogLevel::INFO:
                return "INFO";

            case LogLevel::WARNING:
                return "WARNING";

            case LogLevel::ERROR:
                return "ERROR";

            case LogLevel::FATAL:
                return "FATAL";

            default:
                return "UNKNOWN";
        }
    }


    // ============================================================
    // 获取当前时间
    // ============================================================

    std::string GetTimeStamp()
    {
        time_t curr = time(nullptr);

        struct tm curr_tm;

        localtime_r(
            &curr,
            &curr_tm
        );

        char timebuffer[128];

        snprintf(
            timebuffer,
            sizeof(timebuffer),
            "%d-%d-%d %d:%d:%d",
            curr_tm.tm_year + 1900,
            curr_tm.tm_mon + 1,
            curr_tm.tm_mday,
            curr_tm.tm_hour,
            curr_tm.tm_min,
            curr_tm.tm_sec
        );

        return timebuffer;
    }


    // ============================================================
    // Logger
    // ============================================================

    class Logger
    {
    public:

        Logger()
        {
            EnableConsoleLogStrategy();
        }


        // 切换为文件日志策略
        void EnableFileLogStrategy()
        {
            _fflush_strategy =
                std::make_unique<FileLogStrategy>();
        }


        // 切换为控制台日志策略
        void EnableConsoleLogStrategy()
        {
            _fflush_strategy =
                std::make_unique<ConsoleLogStrategy>();
        }


        // ========================================================
        // LogMessage
        //
        // 负责形成一条完整日志
        // ========================================================

        class LogMessage
        {
        public:

            LogMessage(
                LogLevel level,
                const std::string& src_name,
                int line_number,
                Logger& logger
            )
                : _curr_time(GetTimeStamp())
                , _level(level)
                , _pid(getpid())
                , _src_name(src_name)
                , _line_number(line_number)
                , _logger(logger)
            {
                std::stringstream ss;

                ss << "["
                   << _curr_time
                   << "]"

                   << "["
                   << Level2Str(_level)
                   << "]"

                   << "["
                   << _pid
                   << "]"

                   << "["
                   << _src_name
                   << "]"

                   << "["
                   << _line_number
                   << "]"

                   << "-";

                _loginfo = ss.str();
            }


            // ====================================================
            // operator<<
            //
            // 支持:
            //
            // LOG(...) << "hello" << 123 << 3.14;
            // ====================================================

            template <typename T>
            LogMessage& operator<<(
                const T& info
            )
            {
                std::stringstream ss;

                ss << info;

                _loginfo += ss.str();

                // 返回当前对象引用
                // 支持链式调用
                return *this;
            }


            // ====================================================
            // 析构时刷新日志
            // ====================================================

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

        private:

            std::string _curr_time;

            LogLevel _level;

            pid_t _pid;

            std::string _src_name;

            int _line_number;

            // 一条完整日志
            std::string _loginfo;

            // 引用 Logger
            Logger& _logger;
        };


        // ========================================================
        // operator()
        //
        // 允许:
        //
        // logger(level, file, line)
        // ========================================================

        LogMessage operator()(
            LogLevel level,
            const std::string& name,
            int line
        )
        {
            return LogMessage(
                level,
                name,
                line,
                *this
            );
        }


        ~Logger() = default;

    private:

        // Logger 独占当前日志策略
        std::unique_ptr<LogStrategy>
            _fflush_strategy;
    };


    // ============================================================
    // 全局 Logger
    // ============================================================

    Logger logger;


    // ============================================================
    // 日志宏
    // ============================================================

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


    #define Enable_Console_Log_Strategy() \
        logger.EnableConsoleLogStrategy()


    #define Enable_File_Log_Strategy() \
        logger.EnableFileLogStrategy()
}

三十五、日志系统一次调用到底发生了什么?

看到:

LOG(LogLevel::DEBUG)
    << "Hello, log"
    << 3.14;

不要只记“这是日志”。

应该能在脑子里走完整流程:

LOG(LogLevel::DEBUG)
        ↓
logger(
    LogLevel::DEBUG,
    __FILE__,
    __LINE__
)
        ↓
Logger::operator()
        ↓
创建临时 LogMessage
        ↓
LogMessage 构造函数
        ↓
生成:
[时间][DEBUG][PID][文件][行号]-
        ↓
operator<<("Hello, log")
        ↓
_loginfo += "Hello, log"
        ↓
operator<<(3.14)
        ↓
_loginfo += "3.14"
        ↓
整条表达式结束
        ↓
临时 LogMessage 析构
        ↓
_logger._fflush_strategy
        ↓
SyncLog(_loginfo)
        ↓
Console / File

这才是这套日志代码真正值得学习的地方。


三十六、线程安全和可重入到底是什么关系?

这是另一个容易混淆的知识点。

线程安全

关注的是:

多个线程同时执行。

例如:

线程A → 函数
线程B → 函数
线程C → 函数

如果仍然能保证数据正确,就是线程安全。

可重入

关注的是:

同一个执行上下文中,函数能否被再次进入。

例如函数执行过程中再次调用自己,或者被其他执行路径重新进入。

两者不是完全等价。

常见结论:

可重入函数一定是线程安全的,但线程安全函数不一定是可重入的。

因为线程安全可以通过锁实现:

mutex.lock();

...

mutex.unlock();

但如果同一个线程在持有锁的时候再次进入这个函数,又尝试获取同一把非递归锁,就可能出现问题。


三十七、死锁

线程池和日志系统都涉及锁,所以必须理解死锁。

最典型的例子:

线程A:
拿到锁1
等待锁2

线程B:
拿到锁2
等待锁1

于是:

A → 等 B
B → 等 A

谁也无法继续。

经典死锁条件包括:

  1. 互斥条件
  2. 请求并保持
  3. 不可剥夺
  4. 循环等待

四个条件同时满足,就可能发生死锁。


三十八、怎么避免死锁?

方法一:固定加锁顺序

所有线程都:

锁1 → 锁2

不要出现:

线程A:锁1 → 锁2
线程B:锁2 → 锁1

方法二:缩小临界区

只锁真正需要保护的共享资源。

例如线程池:

取任务
 ↓
解锁
 ↓
执行任务

方法三:使用 RAII

LockGuard guard(mutex);

让对象生命周期自动管理锁。

方法四:不要在持锁状态下做耗时操作

例如不要:

加锁
 ↓
网络请求
 ↓
磁盘 IO
 ↓
耗时计算
 ↓
解锁

三十九、把这两个模块真正串起来

现在重新看线程池和日志系统,就会发现它们并不是两个孤立的知识点。

线程池:

多个线程
   ↓
共享任务队列
   ↓
Mutex
   ↓
Cond
   ↓
生产者消费者

日志:

多个线程
   ↓
共享输出资源
   ↓
Mutex
   ↓
Logger
   ↓
Strategy

它们共同依赖:

线程
 ↓
共享资源
 ↓
同步
 ↓
锁
 ↓
RAII

同时,日志系统又把前面学过的 C++ 面向对象知识串了起来:

抽象类
 ↓
纯虚函数
 ↓
继承
 ↓
多态
 ↓
策略模式
 ↓
unique_ptr
 ↓
RAII

线程池则把 Linux 线程知识串起来:

线程
 ↓
互斥量
 ↓
条件变量
 ↓
生产者消费者
 ↓
线程池
 ↓
单例

所以这部分真正的学习目标不是:

“记住 ThreadPool 有哪些成员变量。”

而是:

理解一个多线程程序是如何把线程同步、任务调度、资源管理和面向对象设计组合起来的。


四十、面试时最值得重点掌握的内容

如果时间有限,优先掌握下面这些。

第一优先级:线程池核心原理

一定要能回答:

1. 什么是线程池?

预先创建一组线程,通过任务队列复用线程执行任务,减少线程频繁创建和销毁的开销。

2. 为什么线程池是生产者消费者模型?

生产者向任务队列添加任务,消费者线程从任务队列取任务执行,任务队列是二者之间的共享缓冲区。

3. 为什么使用条件变量?

没有任务时让消费者阻塞睡眠,避免忙等浪费 CPU;有任务时唤醒消费者。

4. 为什么 wait() 要配合 while

防止虚假唤醒,并处理多个线程竞争同一个任务的情况。唤醒后必须重新检查条件。

5. 为什么 Stop 要 Broadcast?

因为工作线程可能阻塞在 wait 上,如果不唤醒,它们无法检查退出条件,可能导致 Join 一直等待。

6. 为什么取任务以后立即释放锁?

因为任务本身已经属于当前线程,执行任务不需要访问共享任务队列。尽快释放锁可以缩小临界区,提高并发效率。


四十一、第二优先级:C++设计思想

重点掌握:

RAII
unique_ptr
虚函数
纯虚函数
多态
策略模式
单例模式

尤其是:

为什么这么设计?

而不是只会说定义。

例如面试官问:

为什么日志系统要设计 LogStrategy?

不要只回答:

“因为这是策略模式。”

更好的回答是:

“日志输出方式是容易变化的部分,可能输出到控制台,也可能输出到文件。把这部分抽象成 LogStrategy,可以让 Logger 依赖抽象接口,通过多态替换具体输出策略,从而降低模块之间的耦合。”

这才是真正理解了。


四十二、最终知识地图

                    Linux / C++
                         │
             ┌───────────┴───────────┐
             │                       │
           线程                    C++面向对象
             │                       │
          互斥量                    继承
             │                       │
         条件变量                   多态
             │                       │
       生产者消费者               纯虚函数
             │                       │
           线程池                 抽象基类
             │                       │
          单例模式                策略模式
             │                       │
             └───────────┬───────────┘
                         │
                        RAII
                         │
                 智能指针 / LockGuard
                         │
                         ↓
                    日志系统
                         │
              ┌──────────┴──────────┐
              ↓                     ↓
          Console                 File
              │                     │
              └──────────┬──────────┘
                         ↓
                    多线程安全

四十三、这一阶段应该达到什么程度?

不要求你现在把整个线程池代码默写出来。

真正应该达到的是:

看到代码,能够解释每一块为什么存在。

例如看到:

std::queue<T> _taskq;

你应该知道:

这是生产者消费者之间共享的任务缓冲区。

看到:

Mutex _mutex;

应该想到:

任务队列是共享资源,需要互斥保护。

看到:

Cond _cond;

应该想到:

没有任务时消费者不能忙等,需要睡眠等待;任务到来后由生产者唤醒。

看到:

while (_taskq.empty() && _isrunning)

应该想到:

“没有任务”和“线程池仍然运行”共同决定线程是否应该等待。

看到:

if (!_isrunning && _taskq.empty())

应该想到:

线程池停止后仍然要处理完队列中的任务,然后退出。

看到:

std::unique_ptr<LogStrategy>

应该想到:

Logger 依赖抽象策略,通过智能指针管理具体日志策略,实现运行时多态。

看到:

return *this;

应该想到:

为了让 operator<< 支持链式调用。

看到:

~LogMessage()

应该想到:

利用临时对象生命周期,在完整日志拼接完成后自动刷新。


四十四、一句话总结

线程池解决的是“如何复用和调度线程”,生产者消费者模型解决的是“如何在线程之间安全传递任务”,互斥锁和条件变量负责同步,Stop/Join 负责线程生命周期;单例保证线程池实例唯一;日志系统则进一步利用 RAII、智能指针、继承、多态和策略模式,把“日志生成”和“日志输出”解耦。

这部分真正需要掌握的不是几十个 API,而是下面这条逻辑:

共享资源
   ↓
需要同步
   ↓
Mutex
   ↓
没有任务时不能忙等
   ↓
Cond
   ↓
生产者消费者
   ↓
把消费者提前创建并复用
   ↓
线程池
   ↓
线程池需要统一管理
   ↓
单例
   ↓
多线程程序需要记录运行状态
   ↓
日志系统
   ↓
日志输出方式可能变化
   ↓
策略模式 + 多态
   ↓
资源自动管理
   ↓
RAII + unique_ptr

这才是这一部分知识真正的主线。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐