【Linux】线程池(一)C++ 手写线程池:基于策略模式实现高性能日志模块

文章目录


池化技术

池化技术可以减少很多的底层重复工作,例如创建进程、线程、申请内存空间时的系统调用和初始化工作,例如线程池,先预先创建好一些线程,当任务到来时直接将预先创建好的线程唤醒去处理任务,效率会远远高于任务到来时临时创建线程。例如内存池,但我们要用1mb空间时内存池会一次性申请20mb空间,效率会远远高于用多少空间申请多少空间(申请空间会调用系统调用)。

线程池是执行流级别的池化技术,STL中的空间配置器和内存池是内存块管理级别的池化技术。

线程池的日志模块

下⾯开始,我们结合我们之前所做的所有封装,进⾏⼀个线程池的设计。在写之前,我们要做如下准备。

  • 准备线程的封装
  • 准备锁和条件变量的封装
  • 引⼊日志,对线程进⾏封装

日志与策略模式

什么是设计模式

IT⾏业这么⽕, 涌⼊的⼈很多. 俗话说林⼦⼤了啥⻦都有. ⼤佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖⼤佬的后腿, 于是⼤佬们针对⼀些经典的常⻅的场景, 给定了⼀些对应的解决⽅案, 这个就是设计模式。

什么是日志

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

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

时间戳

日志等级(严重程度)

日志内容

以下⼏个指标是可选的

⽂件名⾏号

进程,线程相关id信息等

⽇志有现成的解决⽅案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采⽤⾃定义日志的⽅式。

这⾥我们采⽤设计模式中的策略模式来进⾏⽇志的设计,具体策略模式介绍,详情看下文。

我们想要的日志格式如下:

日志模块

两个核心问题

1、日志内容的刷新策略,刷新到显示器或文件或网络。

2、构建一条完整的日志。

设计文件等级

我们知道枚举类型中的枚举值(编译期常量)的底层存储是是整数,所以我们需要把它们转换成字符串输出。(补充:枚举值的类型就是枚举类本身)

cpp 复制代码
//Logger.hpp

// C++11支持的强枚举,访问成员需指定作用域,不易出现命名冲突
enum class LogLevel
{
    DEBUG,
    INFO,   // 正常消息
    WARING, // 出现错误,但不影响程序运行
    ERROR,  // 导致程序退出的错误
    FATAL,  // 重大错误
};

std::string Level_to_string(LogLevel level)
{
    switch (level)
    {
    case LogLevel::DEBUG:
        return "DEBUG";
    case LogLevel::INFO:
        return "INFO";
    case LogLevel::WARING:
        return "WARING";
    case LogLevel::ERROR:
        return "ERROR";
    case LogLevel::FATAL:
        return "FATAL";
    default:
        return "Unknown";
    }
}

刷新策略

日志的刷新策略我们打算用策略模式来设计,策略模式其实就是利用C++的多态特性,先创建一个日志刷新基类,然后根据日志具体往哪刷新写具体的派生类函数,例如往显示器中写、往文件中写、往网络中写等等。

小编重点讲一下往文件中写,具体实现注意事项及步骤如下:

1、当我们要把日志内容刷新到文件中时需要在当前工作目录下新建一个文件夹,因为日志需要根据不同的日志等级把日志内容写到不同的文件里。

2、我们要新建文件夹首先可以用mkdir系统调用:

但是小编更推荐大家使用C++17提供的文件操作接口,需要包< filesystem >头文件,在派生类FileLogStrategy的构造函数里需要先将指定目录文件创建好,首先要判断在当前工作目录下指定目录文件存不存在,若存在直接返回,若存在则新建该指定目录文件,但是创建目录可能会遇到各种问题例如父目录不存在等等,所以我们创建目录时需要try-catch捕异常。

3、然后实现SyncLog往日志文件中刷新日志内容。首先我们要先拼接目标文件路径,然后使用C++风格的文件操作对指定文件写入日志内容。

下面是源码及测试代码:

cpp 复制代码
//logger.hpp
// 策略模式,策略接⼝
class LogStrategy
{
public:
    virtual ~LogStrategy() = default;
    // 纯虚函数,强制派生类重写该函数
    virtual void SyncLog(const std::string &logmessage) = 0;
};

// 控制台日志策略,就是日志只向显示器打印,方便我们debug
class ConsoleLogStrategy : public LogStrategy
{
public:
    ~ConsoleLogStrategy()
    {
    }

    void SyncLog(const std::string &logmessage) override
    {
        //显示器是共享资源,需要加锁保护
        {
            LockGuard lockguard(&_lock);
            std::cout << logmessage << std::endl;
        }
    }

private:
    Mutex _lock;
};


// 日志向文件打印
const std::string logdefaultpath = "log";
const static std::string logdefaultfilename = "test.log";

class FileLogStrategy : public LogStrategy
{
public:
    FileLogStrategy(const std::string &dir = logdefaultpath, 
        const std::string &filename = logdefaultfilename)
    :_dir_path_name(dir)
    ,_filename(filename)
    {
        //有可能多个线程都在新建目录,所以最好对新建目录也进行加锁
        {
            LockGuard lockguard(&_lock);
            if(std::filesystem::exists(_dir_path_name))
            {
                //当前工作路径下目录存在,不用新建目录,直接返回
                return;
            }
            try
            {
                std::filesystem::create_directories(_dir_path_name);
            }
            catch(const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
            
        }
    }

    void SyncLog(const std::string &logmessage) override
    {
        //显示器是共享资源,需要加锁保护
        {
            LockGuard lockguard(&_lock);
            //拼接目标文件路径
            std::string target = _dir_path_name;
            target += "/";
            target += _filename;

            std::ofstream out(target.c_str(), std::ios::app); //append
            if(!out.is_open())
            {
                //打开文件失败
                return;
            }
            out << logmessage << "\n";  //等价于out.write()
            out.close();
        }
    }

    ~FileLogStrategy()
    { 
    }

private:
    std::string _dir_path_name; //要写入的目录路径名    // log
    std::string _filename;      //形成日志文件的文件名  // hello.log 
    //例子:先创建一个log目录,再在log路径下形成hello.log文件,并把日志内容写到该文件中
    Mutex _lock;
};
cpp 复制代码
//main.cc
#include "Logger.hpp"

int main()
{
    std::string test = "hello logger";
    // //测试策略1,显示器写入
    // //智能指针,本质就是对 LogStrategy * 原生指针做了封装
    // std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<ConsoleLogStrategy>();
    // logger_ptr->SyncLog(test);
    // logger_ptr->SyncLog(test);
    // logger_ptr->SyncLog(test);
    // logger_ptr->SyncLog(test);
    // logger_ptr->SyncLog(test);

    //测试策略2,文件写入
    //智能指针,本质就是对 LogStrategy * 原生指针做了封装
    std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<FileLogStrategy>();
    logger_ptr->SyncLog(test);
    logger_ptr->SyncLog(test);
    logger_ptr->SyncLog(test);
    logger_ptr->SyncLog(test);
    logger_ptr->SyncLog(test);
    return 0;
}

获取日志时间

1、首先需要获取时间戳,利用time系统调用:

参数传递nullptr,返回值类型本质是对long int类型做的封装,返回值具体是1970-01-01午夜到现在的秒数。

2、然后根据时间戳,转化成可读性较强的时间信息,例如1970-01-01 00:00:00。实现该功能需要需要利用localtime函数,localtime函数有两种类型,后缀加_r和不加_r,加_r表示该函数可被重入,小编推荐使用加_r的。

它会将时间戳转化为struct tm结构体,第一个是输入型参数,传递时间戳,第一个是输出型参数,将结果通过指针带出,成功返回结构体指针,失败返回nullptr。

3、把年月日时分秒转化为字符串,我们用以前介绍过的snprintf:

但是一点需要注意,struct tm中的年剪掉了1900,月是0-11,需要我们手动把年加1900,把月加1。

cpp 复制代码
// 根据时间戳,获取可读性较强的时间信息
std::string GetCurrentTime()
{
    // 1、获取时间戳
    time_t curtime = time(nullptr);
    // 2、把时间戳转化为年月日时分秒
    struct tm currtm;
    localtime_r(&curtime, &currtm);
    // 3、把年月日时分秒转化为字符串
    char timebuffer[64];
    snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
        currtm.tm_year + 1900,
        currtm.tm_mon + 1,
        currtm.tm_mday,
        currtm.tm_hour,
        currtm.tm_min,
        currtm.tm_sec
    );

    return timebuffer;
}

logger类实现

现在所有零件都已加工完成,接下来需要把它们拼接成一个完整的日志类,供我们使用。

首先需要激活策略,要开启什么策略,就把策略对应的派生类对象创建出来,并把派生类成员指针变量指向该对象。

cpp 复制代码
class Logger
{
public:
    Logger()
    {
    }

    //激活策略
    void EnableConsoleLogStrategy()
    {
        _strategy = std::make_unique<ConsoleLogStrategy>();
    }

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

    ~Logger()
    {
    }

private:
    std::unique_ptr<LogStrategy> _strategy;
};

现在我们还缺形成一条完整日志的方式,这里小编利用内部类来实现,在logger内部定义一个LogMessage的内部类,利用LogMessage将各种日志信息拼接成一个字符串,在LogMessage的析构函数中调用logger成员变量_strategy的刷新函数将字符串刷新出去,这也是RAII风格的设计思路,所以内部类还需要定义一个对外部类引用的成员变量。

形成一条完整日志后就需要将日志信息信息刷新出去,这里我们通过重载括号运算符实现,比定义一个具体writelog函数更优雅,具体分析见代码注释。

内部类LogMessage实现

LogMessage的构造函数:

首先将成员变量利用初始化列表初始化,然后用stringstream类对象ss将各种类型的成员变量格式化为字符串,接着调用对象ss的str接口,将其赋给_logiofo,构成日志信息的左半部分。

stringstream需要包<sstream>头文件,它是C++提供的格式化方案,前面我们用的snprintf是C语言提供的方案。

LogMessage的operator<<:

然后拼接_loginfo右半部分,首先我们先大致看一下未来的日志输出方式:

cpp 复制代码
LOG(LogLevel::FATAL) << "hello world" << 1234 << ", 3.14" << 'c';

我们可以看到右半部分是参数是可变的,并且参数类型也不确定,所以处理思路是在内部类中对输出运算符做重载,并且参数类型设置成模板,这样任意类型参数都可以输出。

LogMessage的析构函数:

当LogMessage调用operator<<完毕后,_loginfo也就拼接完毕了,此时LogMessage要析构了,我们就可以在LogMessage的析构函数中将_loginfo的日志信息刷新出去。

日志刷新流程图及源码

cpp 复制代码
//logger.hpp
#pragma once

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

// C++11支持的强枚举,访问成员需指定作用域,不易出现命名冲突
enum class LogLevel
{
    DEBUG,
    INFO,   // 正常消息
    WARING, // 出现错误,但不影响程序运行
    ERROR,  // 导致程序退出的错误
    FATAL,  // 重大错误
};

std::string Level_to_string(LogLevel level)
{
    switch (level)
    {
    case LogLevel::DEBUG:
        return "DEBUG";
    case LogLevel::INFO:
        return "INFO";
    case LogLevel::WARING:
        return "WARNING";
    case LogLevel::ERROR:
        return "ERROR";
    case LogLevel::FATAL:
        return "FATAL";
    default:
        return "Unknown";
    }
}

// 根据时间戳,获取可读性较强的时间信息
std::string GetCurrentTime()
{
    // 1、获取时间戳
    time_t curtime = time(nullptr);
    // 2、把时间戳转化为年月日时分秒
    struct tm currtm;
    localtime_r(&curtime, &currtm);
    // 3、把年月日时分秒转化为字符串
    char timebuffer[64];
    snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
             currtm.tm_year + 1900,
             currtm.tm_mon + 1,
             currtm.tm_mday,
             currtm.tm_hour,
             currtm.tm_min,
             currtm.tm_sec);

    return timebuffer;
}

/////////////////////////////////////////////////////////////

// 策略模式,策略接⼝
class LogStrategy
{
public:
    virtual ~LogStrategy() = default;
    // 纯虚函数,强制派生类重写该函数
    virtual void SyncLog(const std::string &logmessage) = 0;
};

// 控制台日志策略,就是日志只向显示器打印,方便我们debug
class ConsoleLogStrategy : public LogStrategy
{
public:
    ~ConsoleLogStrategy()
    {
    }

    void SyncLog(const std::string &logmessage) override
    {
        // 显示器是共享资源,需要加锁保护
        {
            LockGuard lockguard(&_lock);
            std::cout << logmessage << std::endl;
        }
    }

private:
    Mutex _lock;
};

// 日志向文件打印
const std::string logdefaultpath = "log";
const static std::string logdefaultfilename = "test.log";

class FileLogStrategy : public LogStrategy
{
public:
    FileLogStrategy(const std::string &dir = logdefaultpath,
                    const std::string &filename = logdefaultfilename)
        : _dir_path_name(dir), _filename(filename)
    {
        // 有可能多个线程都在新建目录,所以最好对新建目录也进行加锁
        {
            LockGuard lockguard(&_lock);
            if (std::filesystem::exists(_dir_path_name))
            {
                // 当前工作路径下目录存在,不用新建目录,直接返回
                return;
            }
            try
            {
                std::filesystem::create_directories(_dir_path_name);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
        }
    }

    void SyncLog(const std::string &logmessage) override
    {
        // 显示器是共享资源,需要加锁保护
        {
            LockGuard lockguard(&_lock);
            // 拼接目标文件路径
            std::string target = _dir_path_name;
            target += "/";
            target += _filename;

            std::ofstream out(target.c_str(), std::ios::app); // append
            if (!out.is_open())
            {
                // 打开文件失败
                return;
            }
            out << logmessage << "\n"; // 等价于out.write()
            out.close();
        }
    }

    ~FileLogStrategy()
    {
    }

private:
    std::string _dir_path_name; // 要写入的目录路径名    // log
    std::string _filename;      // 形成日志文件的文件名  // hello.log
    // 例子:先创建一个log目录,再在log路径下形成hello.log文件,并把日志内容写到该文件中
    Mutex _lock;
};

////////////////////////////////////////////////////////////

class Logger
{
public:
    Logger()
    {
    }

    // 激活策略
    void EnableConsoleLogStrategy()
    {
        _strategy = std::make_unique<ConsoleLogStrategy>();
    }

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

    //形成一条完整的日志信息,利用内部类LogMessage
    class LogMessage
    {
    public:
        LogMessage(LogLevel level, std::string filename, int line, Logger &logger)
        :_curr_time(GetCurrentTime())
        ,_level(level)
        ,_pid(getpid())
        ,_filename(filename)
        ,_line(line)
        ,_logger(logger)
        {
            std::stringstream ss;
            ss << "[" << _curr_time << "] "
            << "[" << Level_to_string(_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()
        {
            //如果外部类的_strategy的成员变量不为空,
            //就可以将_loginfo的内容通过_strategy刷新出去
            if(_logger._strategy)
            {
                _logger._strategy->SyncLog(_loginfo);
            }
        }

    private:
        std::string _curr_time;   //日志时间
        LogLevel _level;  //日志等级
        pid_t _pid; //进程pid
        std::string _filename; //对应的文件名
        int _line; //对应的文件行号

        std::string _loginfo; //一条合并完成的,完整的日志信息
        Logger &_logger; //对外部类引用,利用Logger为LogMessage提供刷新策略
    };

    //Logger提供的写日志方法
    LogMessage operator()(LogLevel level, std::string filename, int line)
    {
        //返回LogMessage的匿名对象
        return LogMessage(level, filename, line, *this);
    }
    //上面LogMessage operator()实现引出的一些知识点:
    //1、直接返回LogMessage临时对象,和先显式创建对象再返回的写法
    //在绝大多数场景下效果完全一致,返回临时对象的写法
    //编译器会直接在 "接收返回值的位置" 构造 LogMessage 对象,无任何拷贝/移动
    //先显式创建对象再返回的写法编译器会触发 RVO 优化,同样省略 log_msg 的拷贝 / 移动,
    //最终效果和写法 1 完全一致;即使关闭优化,C++11 也会通过移动构造函数
    //(LogMessage 无自定义移动构造时,编译器自动生成)完成返回,性能损耗可忽略。
    //2、的生命周期并非简单的 "只有一行",它的存活时间会根据上下文被编译器延长,
    //尤其是在函数返回的场景下,例如上面代码的LogMessage匿名对象
    //3、这里写日志方法完全可以不重载括号运算符,而是写成一个具体函数如WriteLog,
    //这样就只在:#define LOG(level) logger.WriteLog(level, __FILE__, __LINE__)
    //需要修改一下调用方法,而重载括号运算符的会更优雅,小编更推荐

    ~Logger()
    {
    }

private:
    std::unique_ptr<LogStrategy> _strategy;
};

//先定义一个Logger对象,后面定义宏会用到
Logger logger;

//定义调用日志的宏
#define LOG(level) logger(level, __FILE__, __LINE__)
//定义选择日志模式的宏(也就是选择日志刷新策略)
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()
cpp 复制代码
//main.cc
#include "Logger.hpp"
#include <unistd.h>

int main()
{
    //预处理阶段时EnableConsoleLogStrategy()会被宏替换为logger.EnableConsoleLogStrategy()
    EnableConsoleLogStrategy();
    // EnableFileLogStrategy();

    LOG(LogLevel::ERROR) << "hello world" << 1234 << ", 3.14 " << 'c';
    LOG(LogLevel::WARING) << "hello world" << 1234 << ", 3.14 " << 'c';
    LOG(LogLevel::ERROR) << "hello world" << 1234 << ", 3.14 " << 'c';
    LOG(LogLevel::ERROR) << "hello world" << 1234 << ", 3.14 " << 'c';

    // std::string test = "hello logger";
    // //测试策略1,显示器写入
    // //智能指针,本质就是对 LogStrategy * 原生指针做了封装
    // std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<ConsoleLogStrategy>();
    // logger_ptr->SyncLog(GetCurrentTime());
    // sleep(1);
    // logger_ptr->SyncLog(GetCurrentTime());
    // sleep(1);
    // logger_ptr->SyncLog(GetCurrentTime());
    // sleep(1);
    // logger_ptr->SyncLog(GetCurrentTime());
    // sleep(1);
    // logger_ptr->SyncLog(GetCurrentTime());
    // sleep(1);

    // //测试策略2,文件写入
    // //智能指针,本质就是对 LogStrategy * 原生指针做了封装
    // std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<FileLogStrategy>();
    // logger_ptr->SyncLog(test);
    // logger_ptr->SyncLog(test);
    // logger_ptr->SyncLog(test);
    // logger_ptr->SyncLog(test);
    // logger_ptr->SyncLog(test);
    return 0;
}

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

相关推荐
拾贰_C2 小时前
【centos7 | Linux | redis】Redis安装
linux·运维·redis
lihaihui19912 小时前
Linux C++知识梳理
linux·c++
浩子智控2 小时前
zynq嵌入式开发(1)—开发准备和流程
linux·嵌入式硬件·硬件架构
仰泳的熊猫2 小时前
题目2086:蓝桥杯算法提高VIP-最长公共子序列
数据结构·c++·算法·蓝桥杯·动态规划
Xzq2105092 小时前
Linux 进程管理:从终端控制到守护进程
linux·运维·服务器
0 0 02 小时前
CCF-CSP 36-2 梦境巡查(dream)【C++】考点:前缀和
开发语言·c++·算法
熊文豪2 小时前
完整卸载 OpenClaw — 各平台卸载完全指南(Windows/macOS/Linux/npm/pnpm)
linux·windows·macos·openclaw
Cx330❀2 小时前
Linux ELF格式与可执行程序加载全解析:从磁盘文件到运行进程
linux·运维·服务器·人工智能·科技
CheungChunChiu2 小时前
USB‑C PD 充电系统完整解析(SC8886 + FUSB302)
linux·usb·type-c·充电