【Linux系统编程】线程池项目实战与基于策略模式的日志系统

目录

一、准备工作

线程封装

锁封装

条件变量封装

二、日志

2.1、策略模式

设计模式: 是前人总结的"最佳实践模板",解决面向对象设计中反复出现的特定问题,让代码更灵活、可维护、可复用。

共有 23 种经典设计模式,策略模式就属于其中一种。

策略模式: 即定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换。C++中可以利用多态实现策略模式:

cpp 复制代码
// 策略接口(基类)
class PaymentStrategy {
public:
    virtual void pay(int amount) = 0; // 纯虚函数
    virtual ~PaymentStrategy() = default;
};

// 具体策略:支付宝
class Alipay : public PaymentStrategy {
public:
    virtual void pay(int amount) override {
        cout << "支付宝支付 " << amount << " 元" << endl;
    }
};

// 具体策略:微信支付
class WeChatPay : public PaymentStrategy {
public:
    virtual void pay(int amount) override {
        cout << "微信支付 " << amount << " 元" << endl;
    }
};

// 上下文:购物车
class ShoppingCart {
    PaymentStrategy* strategy;  // 持有策略(基类指针)
public:
    void setStrategy(PaymentStrategy* s) { strategy = s; }
    void checkout(int amount) {
        strategy->pay(amount);  // 调用当前策略(多态)
    }
};

// 使用
int main() {
    ShoppingCart cart;
    
    cart.setStrategy(new Alipay());
    cart.checkout(100);  // 支付宝支付 100 元
    
    cart.setStrategy(new WeChatPay());
    cart.checkout(200);  // 微信支付 200 元
}

未来我们的日志无非就是打印到显示器上,或者写入文件。所以,就可以实现向显示器打印和向文件写入两个子类。

2.2、日志设计

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

日志应当有以下几部分组成:

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

以下几个部分可选:

  • 进程,线程id
  • 文件名
  • 行号

日志等级:

等级 含义
DEBUG 调试信息
INFO 正常运行信息
WARN 警告,非致命
ERROR 错误,功能受损
FATAL 致命,系统崩溃

我们打算设计的日志格式如下:

plain 复制代码
[可读性很好的时间] [日志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,支持可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world

2.3、日志实现

2.3.1 策略模式实现日志显示

策略接口(基类):

cpp 复制代码
class LogStrategy {
public:
    virtual void SyncLog(const std::string message) = 0; // 纯虚函数
    ~LogStrategy() = default;
};

具体策略: 向显示器打印

**注意:**显示器属于临界资源,为了避免打印出现数据错乱,需要加锁。

cpp 复制代码
class ConsoleLogStrategy : public LogStrategy {
public:
    virtual void SyncLog(const std::string message) override
    {
        Mutex_RAII guard(_mutex); // 加锁
        std::cout << message << "\r\n";
    }
    ~ConsoleLogStrategy() {}
private:
    Mutex _mutex;
};

具体策略: 向文件打印

  • 细节一:我们需要一个已经存在文件路径,自己起一个文件名。那么就需要我们检查文件路径,可以用std::filesystem 中的exists方法检查,std::filesystemC++17标准库,统一了跨平台的文件系统操作。
  • 细节二:我们向文件写入日志,应该以追加方式写入。可以用std::ofstream方法以追加方式打开一个文件,不存在则创建。
  • 细节三:std::ofstream打开文件,我们可以以追加重定向的方式向文件写入日志。
  • 细节四:文件同样是共享资源,凡是涉及访问共享资源可能存在问题,我们必须加锁。
cpp 复制代码
std::string gdefaultPath = "./"; 			// 默认当前路径
std::string gdefaultName = "log.log";	// 默认文件名
class FileLogStrategy : public LogStrategy {
public:
    FileLogStrategy(std::string path = gdefaultPath, std::string name = gdefaultName)
        : _path(path), _name(name)
    {
        // 对路径做检查
        Mutex_RAII guard(_mutex); // 加锁
        if (std::filesystem::exists(_path))
        {
            return;
        }
        try
        {
            std::filesystem::create_directories(_path);
        }
        catch (const std::filesystem::filesystem_error &err)
        {
            std::cout << err.what() << std::endl;
        }
    }
    virtual void SyncLog(const std::string message) override
    {
        Mutex_RAII guard(_mutex); // 加锁
        // 构造文件完整路径
        std::string filename = _path + (_path.back() == '/' ? "" : "/") + _name; 
        // 以追加方式打开文件,不存在创建
        std::ofstream out(filename, std::ios::app); 
        if (!out)
        {
            return;
        }
        out << message << "\r\n";
        out.close();
    }
    ~FileLogStrategy() {}
private:
    std::string _path;  // 文件路径
    std::string _name;  // 文件名
    Mutex _mutex;       // 锁
};

2.3.2 日志等级处理

日志等级我们用应该联合体表示,在联合体中日志等级其实就是一个整数,而我们在拼接日志时,希望以字符串形式显示日志等级,所以,我们需要做一下转换。

cpp 复制代码
// 形成日志等级
enum class LogLevel
{
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};
// 将日志等级转化为字符串形式,LogLevel对象实际是一个整数
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 "UNKOWN";
    }
}

2.3.3 时间戳处理

我们想要一个可读性很好的日志,如,2024-08-04 12:27:03,可以用snprintf进行格式控制。

获取当前时间戳:timelocaltime_r可以将时间戳转化为一个结构体。通过这个结构体就可以获得年月日等等。

cpp 复制代码
// 获取时间
std::string Time()
{
    time_t now = time(nullptr); // 获取当前时间戳
    tm tm;
    localtime_r(&now, &tm);
    char buf[128];
    // 格式控制
    snprintf(buf, sizeof(buf), "%4d-%02d-%02d %02d:%02d:%02d",
             tm.tm_year + 1900,
             tm.tm_mon + 1,
             tm.tm_mday,
             tm.tm_hour,
             tm.tm_min,
             tm.tm_sec);
    return buf;
}

2.3.4 形成一条日志

  • 细节一:我们采用在Logger类中在实现一个LogMessage类。Logger类决定最终调用哪个策略,向显示器打印,或向文件打印。LogMessage类负责按照格式将所有数据形成日志。

为什么要在类中实现类?

这样LogMessage类形成日志之后就可以调用Logger的方法,显示日志。在LogMessage对象析构时,我们将日志打印出来,由指针的类型最终决定调用哪个方法(多态)。

  • 细节二:Logger类中可以用智能指针,基类函数指针类型,来帮我们调用策略函数,同时,主动内存管理。
  • 细节三:形成一条日志,可以用std::stringstream实现字符串和数据的流式转换,用重定向的方式将任意类型全部转化为字符串,非常方便。
  • 细节四:未来我们的日志输出形式:Logger() << "xxx" << xxx。所以,就需要重载 << 操作符。同时,可能连续向显示器或文件打印 <<, 所以,为了支持这种写法,可以返回this指针指向的对象。其次,类型不同,我们可以先将其全部转化为字符串类型。
  • 细节五:未来日志等级,打印日志的文件名,行号需要我们自己决定。但在C/C++ 中有两个预定义宏:__FILE____LINE__,由编译器在预处理阶段自动替换为当前文件名和行号。所以,我们可以再处理一下,将logger(level, filename, line)定义为一个宏Log(levle),未来直接Log(level) << "xxx" << xxx调用即可。

如何实现???

由于LogMessage类在Logger中,必须在Logger类中实例化LogMessage类对象,就需要重载(),当我们调用Log(levle),编译时替换为logger(level, filename, line),在重载函数内部LogMessage(level, file, line, *this)实例化LogMessage 对象。

cpp 复制代码
class Logger
{
public:
    Logger()
    {
        Enable_ConsoleLogStrategy();
    }
    // 创建实例化智能指针对象
    void Enable_ConsoleLogStrategy()
    {
        _fllush_strategy = std::make_unique<ConsoleLogStrategy>();
    }
    void Enable_FileLogStrategy()
    {
        _fllush_strategy = std::make_unique<FileLogStrategy>();
    }

    // 形成一条日志
    // 一条日志的形式:[时间] [日志等级] [pid] [文件名] [行数] - "内容"
    // [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
    class LogMessage
    {
    public:
        LogMessage(LogLevel &level, const std::string &file, size_t line, Logger &logger)
            : _cur_time(Time()),
              _loglevel(Level2Str(level)),
              _pid(getpid()),
              _file(file),
              _line_num(line),
              _logger(logger)
        {
            // 日志左半部分
            std::stringstream ss;
            ss << "[" << _cur_time << "] ["
               << _loglevel << "] ["
               << _pid << "] ["
               << _file << "] ["
               << _line_num << "]"
               << " - ";
            _log = ss.str();
        }

        // 日志输出形式: Logger() << "xxx" << xxx;
        // 重载 <<
        template <typename T>
        LogMessage &operator<<(const T &info)
        {
            // 字符串流
            std::stringstream ss;
            ss << info;
            _log += ss.str();
            return *this; // 返回LogMessage对象,处理连续的<<,如: << "xxx" << 123 << 'a'
        }

        // 析构
        ~LogMessage()
        {
            // 调用日志显示方法(多态)
            if (_logger._fllush_strategy)
            {
                // 由该智能指针类型决定调用哪个方法
                _logger._fllush_strategy->SyncLog(_log);
            }
        }

    private:
        std::string _cur_time; // 时间
        std::string _loglevel; // 日志等级
        pid_t _pid;            // 进程id
        std::string _file;     // 文件名
        size_t _line_num;      // 行号
        std::string _log;      // 整条日志
        Logger &_logger;       // 外部类对象,内部类用来调用日志显示方法
    };

    // 重载(),在内部类实例化Logger对象
    LogMessage operator()(LogLevel level, const std::string file, size_t line)
    {
        return LogMessage(level, file, line, *this);
    }

    ~Logger() {}

private:
    std::unique_ptr<LogStrategy> _fllush_strategy; // 日志显示方法(智能指针)
};

Logger logger;
#define Log(level) logger(level, __FILE__, __LINE__)

2.3.5 完整代码及效果演示

代码在这里!!!

日志测试:main.cc

cpp 复制代码
#include "Log.hpp"

using namespace LogModuel;

int main()
{
    logger.Enable_ConsoleLogStrategy(); // 向显示器打印
    logger(LogLevel::DEBUG, "main.cc", 9) << "hello" << 123;
    logger(LogLevel::DEBUG, "main.cc", 9) << "xxxxx" << 123;
    logger(LogLevel::DEBUG, "main.cc", 9) << "aaaaa" << 123;

    logger.Enable_FileLogStrategy(); // 向文件打印
    logger(LogLevel::INFO, "main.cc", 9) << "hello" << 123;
    logger(LogLevel::INFO, "main.cc", 9) << "xxxxx" << 123;
    logger(LogLevel::INFO, "main.cc", 9) << "aaaaa" << 123;
    return 0;
}

三、线程池

3.1、线程池设计

线程池:

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。

线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景:

  • 需要大量的线程来完成任务,且完成任务的时间比较短。
  • 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  • 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。

即我们先创建一批线程,用特定的数据结构维护起来。没有任务时,线程在对应的条件变量下阻塞等待,有任务时,唤醒线程来处理。

流程:

  1. 启动线程池:创建线程并维护
  2. 向线程池排放任务
  3. 唤醒线程处理任务
  4. 关闭线程池,回收线程

完整代码及效果演示:

线程池完整代码在这里!!!

任务代码在这里!!!

线程池测试: main.cc

cpp 复制代码
#include "Log.hpp"
#include "PthreadPool.hpp"
#include "Task.hpp"

using namespace PthreadPoolMoudel;
using namespace LogModuel;

int main()
{
    Logger logger;
    logger.Enable_ConsoleLogStrategy(); // 日志打印到显示器
    PthreadPool<task_t> *pool = new PthreadPool<task_t>();

    pool->Start(); // 启动线程池
    sleep(3);
    int cnt = 10;
    while(cnt)
    {
        pool->Equeue(Download);
        sleep(1);
        cnt--;
    }

    pool->Stop(); // 关闭线程池
    pool->Join(); // 回收线程
    return 0;
}

3.2、线程安全的单例模式

单例,即只有一个实例化的对象。

在很多服务器开发场景中,经常需要让服务器加载很多的数据 (上百G) 到内存中,为了节省空间,此时往往要用一个单例的类来管理这些数据。

单例模式有两种经典的实现模式:饿汉模式与懒汉模式。

举个例子:洗碗

  • 吃完饭,立刻洗碗,这种就是饿汉方式。因为下一顿吃的时候可以立刻拿着碗就能吃饭。
  • 吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式。

懒汉方式最核心的思想是 "延时加载"。从而能够优化服务器的启动速度,这不难理解。在操作系统的设计哲学中,绝大多数场景都采用这种方法,比如,我们申请空间,实例化对象等等。

3.2.1 饿汉模式

cpp 复制代码
class EagerSingleton
{
public:
    static EagerSingleton &getInstance() 
    { 
        return _instance; 
    }
private:
    EagerSingleton() = default; // 构造函数什么都不干
     // 禁止拷贝
    EagerSingleton(const EagerSingleton&) = delete;
    EagerSingleton& operator=(const EagerSingleton&) = delete;
    
    static EagerSingleton _instance; // 程序启动立即实例化对象
};

3.2.2 懒汉模式

cpp 复制代码
class LazySingleton 
{
public:
    static LazySingleton *GetInstance()
    {
        // 需要的时候创建
        if (inst == NULL)
        {
            inst = new LazySingleton();
        }
        return inst;
    }
private:
    LazySingleton() = default; // 构造函数什么都不干
    // 禁止拷贝
    LazySingleton(const LazySingleton&) = delete;
    LazySingleton& operator=(const LazySingleton&) = delete;
    static LazySingleton *inst;
};

3.3、完整代码及效果演示

我们以懒汉模式实现:

代码在这里!!!

测试: main.cc

cpp 复制代码
#include "Log.hpp"
#include "PthreadPool.hpp"
#include "Task.hpp"
using namespace PthreadPoolMoudel;
using namespace LogModuel;

int main()
{
    Logger logger;
    logger.Enable_ConsoleLogStrategy(); // 日志打印到显示器
    sleep(3);
    int cnt = 10;
    while(cnt)
    {
        // 使用时创建单例,但只有第一次才创建对象
        PthreadPool<task_t>::GetInstance()->Equeue(Download);
        sleep(1);
        cnt--;
    }
    PthreadPool<task_t>::GetInstance()->Stop(); // 关闭线程池
    PthreadPool<task_t>::GetInstance()->Join(); // 回收线程
    return 0;
}

线程池项目完整代码在这里:

今天的分享到此结束,如果感觉还不错点个赞支持一下吧,下次再见!!!

相关推荐
stanleyrain1 小时前
linux上无感操作Windows上的文件夹
linux·运维·windows
feng_you_ying_li1 小时前
liunx之信号介绍(3),各种中断的介绍和系统调用的本质以及用户态与内核态的具体介绍
linux
程序员Aries1 小时前
tcp-server 项目实现流程、细节与 muduo 对比分析
linux·网络协议·tcp/ip
染翰1 小时前
Linux 配置:应用用户执行 sudo su root 免密(运维标准配置)
linux·运维·服务器
茫忙然1 小时前
Claude Code 接入 DeepSeek 或 多模型 教程(Linux)
java·linux·数据库
hexu_blog2 小时前
Linux centos 安装向量数据库milvus
linux·centos·milvus
code monkey.3 小时前
【Linux之旅】Linux 应用层自定义协议与序列化:从粘包问题到网络计算器
linux·网络·c++
草莓熊Lotso3 小时前
【Linux网络】深入理解 HTTP 协议(二):从协议格式到手写工业级 HTTP 服务器
linux·运维·服务器·网络·c++·http
剑神一笑10 小时前
Linux pgrep 命令详解:按名称查找进程 PID 的高效方法
linux·运维·chrome