日志和线程池

基础知识:

虚函数和纯虚函数

纯虚函数:用来定义【接口规范】 基类只规定 "要有这个行为",不知道具体怎么做。

普通虚函数:提供【默认行为,允许子类个性化覆盖】:基类有一套通用逻辑;大部分子类直接复用,少数子类按需改写。

普通类

可以直接创建对象的类 ,里面没有纯虚函数。 你可以随便 类名 对象; 实例化。

抽象类

包含至少一个纯虚函数 virtual func() = 0; 的类规则:抽象类不能直接创建对象! 只能用指针 / 引用指向它的子类。(eg:LogStrategy obj; 编译报错!抽象类不能实例化)

对比项 普通虚函数 virtual func(){} 纯虚函数 virtual func() = 0;
基类实现 允许写实现 一般不写实现
所在类 普通类,可以实例化 抽象类,不能实例化
子类重写 都可以,不写就继承父类的 基类没有,强制必须重写
设计定位 扩展:有默认逻辑,支持覆盖 接口:定义规范,无默认逻辑

enum class LogLevel

两大关键特性:

作用域隔离 不能直接写 DEBUG,必须带上名字

复制代码
LogLevel::DEBUG;    //  正确
DEBUG;              //  编译报错

禁止隐式向整型转换

复制代码
int x = LogLevel::INFO; //  编译报错,不能自动转int
int x = static_cast<int>(LogLevel::INFO); //  必须显式强制转换

可以手动指定枚举底层数值

默认规则:

  • DEBUG0 INFO1 ``WARNING2 ``ERROR3 ``FATAL4

    enum class LogLevel
    {
    DEBUG = 10,
    INFO, // 自动=11
    };

对比一下老式写法:

复制代码
enum LogLevel {
    DEBUG, INFO, WARNING
};

缺陷:

  1. 枚举名字泄露到外层作用域 ,直接写 DEBUG 就能访问,容易发生名字冲突;
  2. 隐式转换成整数,可以随便和 int 比较、赋值,类型不安全。

时间相关接口:

time(nullptr)

css 复制代码
time_t time(time_t *timer);

作用 :获取当前系统 UTC 时间戳

  • 参数传nullptr:直接返回时间戳;
  • 如果传入&curr,会同时把值写入变量,两种用法等价
css 复制代码
curr = time(nullptr);
time(&curr);

struct tm

时间分解结构体,把秒数戳拆成年、月、日、时、分、秒:

css 复制代码
struct tm {
    int tm_sec;   // 秒 [0,60](60为闰秒)
    int tm_min;   // 分 [0,59]
    int tm_hour;  // 时 [0,23]
    int tm_mday;  // 日期 [1,31]
    int tm_mon;   // 月份 **[0,11]** → 所以输出要 +1
    int tm_year;  // 年份 = 实际年份 - 1900 → 输出要 +1900
    int tm_wday;  // 星期 [0,6] 0=周日
    int tm_yday;  // 年内第几天
    int tm_isdst; // 夏令时标记
};

localtime_r(&curr, &curr_tm)

css 复制代码
struct tm *localtime_r(const time_t *timer, struct tm *result);

功能 :把time_t时间戳 → 转为本地时区struct tm分解时间

_r代表可重入 / 线程安全版本 ,对比:localtime() 不安全

线程安全的单例模式

饿汉实现方式和懒汉实现方式

洗碗的例子

吃完饭, 立刻洗碗, 这种就是饿汉方式,因为下⼀顿吃的时候可以立刻拿着碗就能吃饭.

吃完饭, 先把碗放下, 然后下⼀顿饭用到这个碗了再洗碗, 就是懒汉方式式.

懒汉方式最核心的思想是"延时加载".从而能够优化服务器的启动速度.

饿汉方式实现单例模式

思想:程序启动时,直接创建实例;提前加载,不是用到的时候才创建。如果data很大可能会影响启动速度

css 复制代码
template <typename T>
class Singleton {
    static T data; // 静态成员,全局初始化阶段就创建
public:
    static T* GetInstance() {
        return &data;
    }
};
template<typename T>
T Singleton<T>::data; // 类外定义!

懒汉方式实现单例模式

思想:延时加载,第一次调用GetInstance()才 new 创建对象。延时加载,不用就不创建,优化程序启动速度

css 复制代码
template <typename T>
class Singleton {
    static T* inst;
public:
    static T* GetInstance() {
        if (inst == nullptr) {
            inst = new T();
        }
        return inst;
    }
};
template<typename T>
T* Singleton<T>::inst = nullptr; // 类外初始化

为什么要加static?

1.static T*GetInstance(){}

不加 static:必须先创建 Singleton 对象,才能调用 GetInstance(); 加 static:不用创建任何 Singleton 对象,直接 Singleton<T>::GetInstance() 获取单例(没有this指针了)。

普通成员函数:

css 复制代码
class Singleton{
public:
    T* GetInstance() { ... }
};

调用方式必须:

css 复制代码
Singleton s;    // 先创建类对象
s.GetInstance(); // 通过对象调用

我们设计单例的目的就是:全局只有唯一一份 T 实例 。 如果还要先创建 Singleton 对象才能调用,等于凭空多出一个对象,违背单例思想。

2.static T data; / static T* inst;

不属于任何一个类对象,所有 Singleton<T> 对象共享同一份 data,如果不加 static: T data 是普通成员,u每创建一个 Singleton 对象,就多出一份 data,不可能实现 "全局唯一实例"

为什么线程池要用这种方式?

线程池是系统全局资源 ,整个程序只需要唯一一组工作线程 ,不能到处新建多个线程池; 单例就是用来保证:全局只能有一个线程池实例

线程安全(对线程)和重入问题(对函数)

重入其实可以分为两种情况

• 多线程重入函数

• 信号导致⼀个执行流重复进入函数

eg:

css 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void test()
{
    std::cout << "进入test\n";
    sleep(5);
    std::cout << "离开test\n";
}

void sig_cb(int sig)
{
    std::cout << "收到信号!\n";
    test(); // 信号处理函数再次调用test
}

int main()
{
    signal(SIGINT, sig_cb);
    test();
    return 0;
}
  • 程序运行,进入test()休眠
  • 按下 Ctrl + C 触发信号
  • 主线程被强行打断,进入信号回调,再次调用 test () 同一个线程,test还没走完,又一次进入,就是信号引发函数重入

重要结论:

函数是可重入的,那就是线程安全的

线程安全不⼀定是可重入的,而可重入函数则⼀定是线程安全的。

怎么理解线程安全不⼀定是可重入的?

css 复制代码
Mutex m;
int g_val = 0;
void func()
{
    LockGuard guard(m);
    g_val++;
}

线程安全:多线程调用有锁保护,不会乱。

不可重入 场景:主线程正在func内持有锁,此时收到信号,信号处理函数又调用func同一个线程再次尝试获取锁,死锁

死锁:

4大必要条件((四个条件必须同时全部满足,才会产生死锁;破坏任意一条,就能避免死锁)):

  • 互斥:一把锁一次只能一个人拿,不能共用。
  • 请求与保持:我手上已经拿着一把锁不松手,同时还要去抢另一把锁。
  • 不可剥夺:别人不能强行把我手里的锁抢走,只能我自己主动放手。
  • 循环等待 A 拿着锁 1,想要 B 手里的锁 2; B 拿着锁 2,想要 A 手里的锁 1; 两个人互相等对方放手,形成死循环。

避免死锁:

死锁检测算法(了解)

银行家算法(了解)

STL 中的容器是否是线程安全的?

不是. 原因是,STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响. 而且对于不同的容器,加锁方式的不同,性能可能也不同 (例如 hash 表的锁表和锁桶). 因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全.

智能指针是否是线程安全的?

对于 unique_ptr, 由于只是在当前代码块范围内生效,因此不涉及线程安全问题. 对于 shared_ptr, 多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作 (CAS) 的方式保证 shared_ptr 能够高效,原子的操作引用计数.

其他常见的各种锁

  • 悲观锁
  • 乐观锁
  • CAS 操作

日志代码实现:

Log.hpp:

css 复制代码
#pragma once
#include <iostream>
#include "Mutex.hpp"
#include <string>
#include <fstream>
#include <filesystem>
#include <pthread.h>
#include <thread>
#include <unistd.h>

// LogStrategy  ConsoleLogStrategy FileLogStrategy
using namespace std;
namespace LogMoudle
{
    const string gsap = "\r\n";//换行转义是 \r\n,
    // / 只是普通斜杠字符,不是转义符,控制台原样输出文本,不会触发换行。
    using namespace MutexModule;
    class LogStrategy
    {
    public:
        virtual ~LogStrategy(){};// 析构函数也要写vitural
        virtual void SyncLog(const string &message) = 0; // 纯虚函数
    };
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
       
        // 构造函数和析构函数都可以不写,系统自己生成
        virtual void SyncLog(const string &message)
        {
            //可能多个文件同时向显示器打印,也要锁
            LockGuard guard(_mutex);
            cout << message << gsap;
        }

    private:
        Mutex _mutex;
    };
    const string defaultpath = "./log";
    const string defaultfile = "my.log";
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const string &path = defaultpath, const string &file = defaultfile)
            : _path(path), _file(file)
        {
            // 下面的任务就是创建目录
           //LockGuard guard(_mutex);
           
            if (filesystem::exists(_path))
            {
                return; // 目录已经存在为真直接return
            }
            try
            {
                filesystem::create_directories(_path);
            }
            catch (filesystem::filesystem_error &e)
            {
                cerr << e.what() << "\n";
            }
        }
        virtual void SyncLog(const string &message)
        {
            //可能多个线程同时调用 SyncLog 写同一个文件,要加锁。
            LockGuard guard(_mutex);
            string tmp = _path + (_path.back() == '/' ? "" : "/") + _file;
            ofstream out(tmp, ios::app);
            if (!out.is_open()) // 打开失败直接返回
            {
                return;
            }
            out << message << gsap;
            out.close();
        }
        ~FileLogStrategy() {}

    private:
        string _path;
        string _file;
        Mutex _mutex;
    };
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };
    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 "UNKNOW";
        }
    }
    string GetTimeStamp()
    {
        time_t curr = time(nullptr);
        struct tm curr_tm;
        localtime_r(&curr, &curr_tm);
        char timebuffer[128];
        snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
                 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  EnableConsoleLogStrategy
    class Logger
    {
    public:
        void EnableConsoleLogStrategy()
        {
            _fflush_strategy = make_unique<ConsoleLogStrategy>();
        }
        void EnableFileLogStrategy()
        {
            _fflush_strategy = std::make_unique<FileLogStrategy>();
        }

        

        class LogMessage
        {
        public:
            LogMessage(LogLevel level, 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) // 后续直接传this指针
            {
                stringstream ss;
                ss << "[" << _curr_time << "] "
                   << "[" << Level2Str(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _src_name << "] "
                   << "[" << _line_number << "] "
                   << "- ";
                _loginfo = ss.str();
            }
            template <typename T>//函数模板
            LogMessage &operator<<(const T &in)
            {
                //一行日志语句(LOG(INFO) << "test1" << 123 << "test2";),只会产生唯一一个临时 LogMessage
                //不存在多个线程同时操作同一个 LogMessage 对象!
                stringstream ss;
                ss << in;
                _loginfo += ss.str();
                return *this;
            }
            ~ LogMessage()//临时对象析构自动调用,做到刷新
            {
                if(_logger._fflush_strategy)//指针不为空
                {
                    _logger._fflush_strategy->SyncLog(_loginfo);
                }
            }


        private:
            string _curr_time;
            LogLevel _level;
            pid_t _pid;
            string _src_name;
            int _line_number;
            string _loginfo;
            Logger &_logger;
        };
        // 这里故意写成返回临时对象
        LogMessage operator()(LogLevel level, std::string name, int line)
        {
            return LogMessage(level, name, line, *this);
        }

    private:
        unique_ptr<LogStrategy> _fflush_strategy;
    };
    Logger logger;//为什么在这里写这个,这样在其它文件就不需要再写 Logger logger这里已经定义好了
    //使用宏简化用户使用操作
    #define LOG(level) logger(level, __FILE__, __LINE__)//自动获取文件名和行数
    #define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
    #define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()

}

Main.cc:

css 复制代码
#include "Log.hpp"
#include <memory>
using namespace LogMoudle;
int main()
{
    Enable_Console_Log_Strategy();
    LOG(LogLevel::DEBUG) << "hello world" << 3.141;
    LOG(LogLevel::DEBUG) << "hello world" << 3.142;

    Enable_File_Log_Strategy();
    LOG(LogLevel::DEBUG) << "hello world" << 3.143;
    LOG(LogLevel::DEBUG) << "hello world" << 3.144;

    return 0;
}

运行结果:

my.log:

css 复制代码
[2026-07-24 11:28:54] [DEBUG] [3974252] [Main.cc] [11] - hello world3.143
[2026-07-24 11:28:54] [DEBUG] [3974252] [Main.cc] [12] - hello world3.144

显示器:

线程池代码实现:

其它板块Task.hpp,Cond.hpp,Mutex.hpp,pthread.hpp前面已经写过了这里就不再重复写了

ThreadPool.hpp:

css 复制代码
#include <iostream>
#include "pthread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
#include <vector>
#include "Log.hpp"
#include <queue>
#include "Task.hpp"
using namespace std;
using namespace CondModule;
using namespace MutexModule;
using namespace ThreadModlue;
using namespace LogMoudle;

const int defaultnum = 5;

namespace ThreadPoolModule
{
    template <typename T>
    class ThreadPool
    {
    private:
        void WakeUpAllThread()
        {
            LockGuard guard(_mutex);
            if (_sleepnum > 0)
            {
                _cond.Broadcast();
            }
            LOG(LogLevel::INFO) << "唤醒所有线程";
        }
        void WakeUpOne()
        {
            if (_sleepnum > 0)
            {
                _cond.Signal();
            }
            LOG(LogLevel::INFO) << "唤醒一个线程";
        }
        void Start()
        {
            if (_isrunning)
                return;
            _isrunning = true;
            for (auto &e : _threads)
            {
                e.Start();
                LOG(LogLevel::INFO) << "start new thread success: " << e.Name();
            }
            // _isrunning = true;
        }
        ThreadPool(int num = defaultnum)
            : _num(num), _sleepnum(0), _isrunning(false)
        {
            for (int i = 0; i < num; i++)
            {
                _threads.emplace_back(
                    [this]()
                    { HandlerTask(); });
                // 这样写不行_threads.emplace_back(HandlerTask);//HandlerTask 是非静态成员函数,类型是 void (ThreadPool::*)()
                // 而 Thread 构造参数是 std::function<void()>类型是void(*)()没有this指针
            }
        }

        // 禁用掉拷贝构造和赋值重载,保证只能创建一个实例
        ThreadPool(const ThreadPool<T> &) = delete;
        ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;

    public:
        static ThreadPool<T> *GetInstance()
        {
            // 两个if判断+锁,解决如果线程池本身,会被多线程获取的问题

            // 外层第一次检查.无锁快速判断:如果实例已经创建,直接 return,不用争抢锁。
            // 作用:避免每次调用 GetInstance 都频繁加锁,提升性能
            if (_inst == nullptr)
            {
                LockGuard guard(_lock);
                // 内层第二次检查.解决多线程并发冲突:
                // 假设线程 A、B 同时通过外层判断,都进入等待锁;
                // A 拿到锁 new 出实例释放锁;
                // B 拿到锁后,内层再次判断,发现实例已经存在,跳过创建。
                // 防止多个线程重复 new,产生多个对象。
              
                if (_inst == nullptr)
                {
                    LOG(LogLevel::DEBUG) << "首次创建单例线程池";
                    _inst = new ThreadPool<T>();
                    _inst->Start();
                }
            }
            return _inst;
        }
        void Stop()
        {
            if (_isrunning == false)
                return;
            _isrunning = false;
            WakeUpAllThread();
        }
        void Join()
        {
            for (auto &e : _threads)
            {
                e.Join();
            }
        }

        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T t; // 写到锁外面
                {
                    LockGuard guard(_mutex);
                    // 1还是还处于运行状态2已经没有任务了
                    while (_isrunning == true && _taskq.empty())
                    {
                        _sleepnum++;
                        //LOG(LogLevel::DEBUG)<<_sleepnum;
                        _cond.Wait(_mutex);
                        _sleepnum--;
                    }
                    // 如果isrunning==false并且任务队列为空了就应该退出了
                    if (!_isrunning && _taskq.empty())
                    {
                        LOG(LogLevel::INFO) << name << " 退出了, 线程池退出&&任务队列为空";
                        break;
                    }
                    // 一定有任务
                    t = _taskq.front(); // 从q中获取任务,任务已经是线程私有的了!!!
                    _taskq.pop();
                }
                t(); // 同样的处理任务也不用加锁,放到外面
            }
        }

        bool Enqueue(const T &in)
        {
            LockGuard guard(_mutex); // 上课的方式写错了,锁要先加
            if (!_isrunning)
                return false; // 线程池关闭,入队失败
            _taskq.push(in);
            if (_sleepnum == _threads.size())
                WakeUpOne();
            return true;
        }
       
       

    private:
        vector<Thread> _threads;
        queue<T> _taskq;
        Cond _cond;
        Mutex _mutex;

        bool _isrunning; // 你这个是线程池有没有启动,和pthread.hpp的_isrunning区别开(单独的一个个线程有没有启动)
        int _sleepnum;
        int _num;

        // 声明:
        static ThreadPool<T> *_inst; // 单例指针
        static Mutex _lock;
    };
    // static不属于类成员不会走初始化列表初始化,所以在类外初始化
    // 定义:
    template <typename T>
    ThreadPool<T> *ThreadPool<T>::_inst = nullptr;

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

}

Main.cc:

css 复制代码
#include "ThreadPool.hpp"
using namespace ThreadPoolModule;
int main()
{

    Enable_Console_Log_Strategy();

    int count = 3;
    while (count)
    {
        sleep(1);
        ThreadPool<task_t>::GetInstance()->Enqueue(Download);
        count--;
    }

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

运行结果:

相关推荐
码云数智-园园1 小时前
建站平台有哪些?建站工具怎么选
开发语言
nVisual1 小时前
01-环境监控集成方案
运维·服务器·开发语言·网络·数据库·数据中心布线·综合布线管理软件
此生决int1 小时前
深入理解C++系列(04)——类和对象(下)
开发语言·c++
Yeauty1 小时前
你那条 ffmpeg 命令,一键翻成 Rust builder 代码
开发语言·rust·ffmpeg
鱼香鱼香rose1 小时前
java-2
java·开发语言·python
是小蟹呀^9 小时前
Spring Security + JWT 面试题整理
java·jwt·springsecurity
星栈独行11 小时前
翻完 Pi 源码:它和 Codex、Claude Code 有何不同
开发语言·javascript·人工智能·程序人生
spencer_tseng11 小时前
Redis + Nacos.bat
java·windows·dos