【Linux】线程同步与互斥_日志与线程池

hello~ 很高兴见到大家! 这次带来的是Linux系统中关于线程这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页 : 默|笙


文章目录

  • 一、日志
    • [1.1 认识接口](#1.1 认识接口)
    • [1.2 代码实现](#1.2 代码实现)
      • [1. 策略模式](#1. 策略模式)
      • [2. 可变参数流式输出与创建内部类的作用](#2. 可变参数流式输出与创建内部类的作用)
  • 二、线程池
    • [2.1 介绍](#2.1 介绍)
    • [2.2 代码实现](#2.2 代码实现)
    • [2.3 单例模式](#2.3 单例模式)

一、日志

在实际工程项目中,日志系统几乎是必不可少的基础组件。所谓日志,就是程序在运行过程中,按一定格式持续输出的运行信息、状态记录、关键流程与异常提示,它能够完整保留程序的执行轨迹。通过日志,我们可以清晰复现问题现场,快速定位 bug、追踪异常、分析逻辑流程,从而高效完成问题排查与程序调试。

1.1 认识接口

cpp 复制代码
[可读性很好的时间] [⽇志等级] [进程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
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world
  1. 我们希望日志输出信息是上面的格式,第一步就是需要得到当前的时间,我们可以通过系统提供的接口来得到:
  1. gettimeofday 接口可以用来获取当前的高精度时间。第一个参数是输出型参数,类型为 struct timeval,该结构体包含两个成员变量:tv_sec 表示从 1970年1月1日 00:00:00 UTC 到现在的秒数 ;tv_usec 表示不足一秒的剩余微秒数。gettimeofday 的第二个参数是时区信息,类型为 struct timezone*,现在已经废弃、几乎不用,传 NULL /nullptr 即可。
  2. 使用要包含头文件 sys/time.h。
cpp 复制代码
struct tm
{
    int tm_sec;    // 秒:0-59
    int tm_min;    // 分:0-59
    int tm_hour;   // 时:0-23
    int tm_mday;   // 日:1-31
    int tm_mon;    // 月:0-11(注意:从0开始!)
    int tm_year;   // 年:从1900开始算
    int tm_wday;   // 星期几:0-6(周日是0)
    int tm_yday;   // 一年中的第几天:0-365
    int tm_isdst;  // 夏令时标识
};
  1. 获取到秒级时间戳之后,需要将其转换为我们日常使用的年、月、日、时、分、秒等格式。这个转换工作可以通过 localtime_r 接口完成。localtime_r 与 localtime 功能完全一致,区别在于:localtime 是不可重入函数,localtime_r 是线程安全的可重入函数。在 Linux 下,多线程程序必须使用 localtime_r。第一个参数:输入,需要转换的时间戳(time_t 类型);第二个参数:输出,传入一个 struct tm 结构体变量的地址,转换完成后的数据会存储在该结构体中。

  2. 使用必须包含头文件 time.h。

1.2 代码实现

cpp 复制代码
#ifndef _LOGGER_HPP
#define _LOGGER_HPP

#pragma once

#include <time.h>
#include <sys/time.h>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <sstream>
#include <unistd.h>
#include <cstdlib>
#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";
            default:
                return "DEBUG";
        }
    }
    std::string GetCurrentTime()
    {
        struct timeval ctime;
        int n = gettimeofday(&ctime, nullptr);
        (void)n;

        struct tm _tm;
        localtime_r(&(ctime.tv_sec), &_tm);
        char timestr[128];
        snprintf(timestr, sizeof timestr, "%04d-%02d-%02d %02d:%02d:%02d.%ld",
                 _tm.tm_year + 1900,
                 _tm.tm_mon + 1,
                 _tm.tm_mday,
                 _tm.tm_hour,
                 _tm.tm_min,
                 _tm.tm_sec,
                 ctime.tv_usec);

        return timestr;
    }

    //策略模式
    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(_lock);
            std::cout << message << std::endl;
        }
        ~ConsoleStrategy()
        {
        }

    private:
        Mutex _lock;
    };

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

    // 向指定文件输出
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &logfilename = defaultfilename, const std::string &logpath = defaultpath)
            : _logfilename(logfilename),
              _logpath(logpath)
        {
            {
                LockGuard lockguard(_lock);
                // 路径如果存在就返回
                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(_lock);
                if (!_logpath.empty() && _logpath.back() != '/')
                    _logpath += "/";

                std::string targetlog = _logpath + _logfilename; // 合成目标路径
                std::ofstream out(targetlog, std::ios::app);     // 以追加方式写入targetlog路径所在文件

                if (!out.is_open())
                {
                    std::cerr << "open" << targetlog << "failed" << std::endl;
                }

                out << message << "\n"; // 建议使用\n而不是endl,后者是换行+刷新,磁盘刷新会特别慢
                out.close();
            }
        }
        ~FileLogStrategy()
        {
        }

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

    //根据不同的策略,刷新到不同位置
    class Logger
    {
    public:
        Logger()
        {
            UseConsoleStrategy();
        }
        void UseConsoleStrategy()
        {
            _strategy = std::make_unique<ConsoleStrategy>();
        }
        void UseFileLogStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }
        class LogMessage
        {
        public:
            LogMessage(LogLevel level, std::string& filename, int line, Logger& logger)
            : _level(level),
            _current_time(GetCurrentTime()),
            _pid(getpid()),
            _filename(filename),
            _line(line),
            _logger(logger)
            {
                //先构建出左半部分
                std::stringstream ss;
                ss << "[" << _current_time << "] "
                    << "[" << LogLevel2Message(_level) << "] "
                    << "[" << _pid << "] "
                    << "[" << _filename << "] "
                    << "[" << _line << "] ";
                _loginfo = ss.str();
            }

            template<class T>
            LogMessage& operator<<(const T& info)
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this;
            }

            ~LogMessage()
            {
                if (_logger._strategy)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }
        private:
            LogLevel _level;
            std::string _current_time;
            pid_t _pid;
            std::string _filename;
            int _line;
            std::string _loginfo;//一条完整的日志信息

            //引用外部的logger对象
            Logger& _logger;
        };

        //采用拷贝
        LogMessage operator()(LogLevel level, std::string filename, int line)
        {
            return LogMessage(level, filename, line, *this);
        }

    private:
        std::unique_ptr<LogStrategy> _strategy; // 刷新策略
    };
    //日志对象,全局使用
    Logger logger;

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

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

#endif

1. 策略模式

  1. 策略模式就是将不同的算法或行为封装成独立的类,使它们可以在程序运行时动态替换 。比如我们日志模块中,向控制台打印和向文件打印就是两种不同的输出策略。通过继承 + 多态,让基类指针可以指向不同的子类对象,并根据需求动态切换具体策略,而不需要修改上层调用代码,从而达到灵活扩展、解耦的目的。
  2. 策略模式就像一个人去学校:他可以选择不同的交通工具,不同的交通工具就是不同的策略。人(要输出的日志内容)是不变的,变的只是去学校的方式(日志输出到哪里:控制台 / 文件)。通过继承 + 多态,我们把不同的输出方式封装成独立策略类,在程序运行时可以随时切换策略,而不用修改日志本身的逻辑
  3. 基类的作用只有两个:统一接口(规定所有策略必须做什么),实现多态(让基类指针可以指向任意子类)。

2. 可变参数流式输出与创建内部类的作用

  1. 日志左半部分格式固定,包含时间戳、日志等级、进程 ID、文件名、行号等信息。因此在创建 LogMessage 临时对象时,就可以直接预先构建好固定的日志头部,右半部分的自定义内容再通过流式 << 动态拼接,最终形成一条完整日志。
  2. operator<< 必须返回自身引用 LogMessage&,而不能返回值对象。如果返回值,会发生拷贝,导致后续内容拼接在临时拷贝对象上,而非原临时对象,最终造成日志内容断裂、丢失。只有返回引用,才能保证所有内容都拼接在同一个临时对象上。
  3. LogMessage 作为内部类,核心作用是:利用析构函数自动输出日志,实现用完即输出的机制;封装 operator<< 实现流式调用;同时与 Logger 职责分离,不污染 Logger 类结构,让设计更清晰、更模块化
  4. 全局 logger 的作用:让日志在程序任何地方都能直接使用,统一管理输出策略,不用传递、不用创建,简单、方便、统一。

二、线程池

2.1 介绍

  1. 线程池 = 预先创建好的一批常驻工作线程 + 线程安全的任务队列 + 统一管理工具。使用线程池时,用户将任务提交到任务队列中;工作线程会互斥访问任务队列这一临界资源(通过互斥锁保护,避免并发冲突),有序获取并执行任务;当队列为空时,线程会通过条件变量进入休眠状态(而非空转消耗 CPU),等待新任务到来时被唤醒,全程保持待命状态,实现线程复用。

2.2 代码实现

  1. 代码要好好看一遍,要注意的点都写在注释里面:
cpp 复制代码
#pragma once

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

namespace NS_THREAD_POOL_MOUDLE
{
    using namespace NS_THREAD_MODULE;
    using namespace NS_LOG_MODULE;

    const int defaultnum = 5;

    // void Task()
    // {
    //     char name[128];
    //     pthread_getname_np(pthread_self(), name, sizeof name);
    //     while (true)
    //     {
    //         LOG(LogLevel::DEBUG) << "我是一个线程,我正在运行。" << name;
    //     }
    // }

    template <class T>
    class ThreadPool
    {
    public:
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof name);
            //除非用户主动关闭线程池,那么线程池里面所有的线程都是要待命的,等待用户往任务队列里面丢任务
            while (true)
            {
                T task;
                {
                    LockGuard lockguard(_mutex);
                    //_mutex.Lock();
                    //任务池是空的且线程池还在运行才能够让线程去休眠
                    while (_tasks.empty() && _isrunning)
                    {
                        _slaver_sleep_num++;
                        _cond.Wait(_mutex);
                        _slaver_sleep_num--;
                    }

                    //当线程池终止且没有任务要处理的时候再退出循环
                    if (!_isrunning && _tasks.empty())
                    {
                        //_mutex.Unlock();
                        break;
                    }

                    //有任务就获得任务
                    task = _tasks.front();
                    _tasks.pop();

                    
                    //_mutex.Unlock();
                }

                LOG(LogLevel::INFO) << name << "线程执行任务";
                // 执行任务,不需要在锁里,不然效率太低
                task(); // 统一执行方式,类要求就重载(),函数,lambda表达式就正常用
                LOG(LogLevel::DEBUG) << task.Result();
            }

            // 线程执行完任务后退出
            LOG(LogLevel::INFO) << name << "线程即将退出";
        }
        ThreadPool(int slaver_num = defaultnum)
            : _slaver_num(slaver_num),
              _isrunning(false),
              _slaver_sleep_num(0)
        {
            for (int idx = 0; idx < _slaver_num; idx++)
            {
            //通过lambda表达式,让传递的函数符合要求,封装的Thread类里面要求是void(无参无返回值)函数
                _slavers.emplace_back([this]()
                                      { this->HandlerTask(); });
            }
        }

        void Start()
        {
            if (_isrunning)
            {
                LOG(LogLevel::WARNING) << "Thread Pool Is Already Running";
                return;
            }
            _isrunning = true; // 不能放到后面,否则可能导致重复启动问题

            // 启动所有的线程
            for (auto &slaver : _slavers)
            {
                slaver.Start();
            }
        }
        void Stop()
        {
            // version1,太过于粗暴
            // if (!_isrunning)
            // {
            //     LOG(LogLevel::WARNING) << "Thread Pool Is Not Running";
            //     return;
            // }

            // for (auto &slaver : _slavers)
            // {
            //     slaver.Die();
            // }
            // _isrunning = false; // 先做事后标记
            
            //version2
            _mutex.Lock();
            _isrunning = false;
            //把线程全部唤醒,让它们去处理任务
            if (_slaver_num > 0)
                _cond.Broadcast();
            _mutex.Unlock();
        }
        void Wait()
        {
            for (auto &slaver : _slavers)
            {
                slaver.Join();
            }
        }

        void Enqueue(const T &in)
        {
            _mutex.Lock();
            _tasks.push(in);

            if (_slaver_sleep_num > 0)
                _cond.Signal();

            _mutex.Unlock();
        }

        ~ThreadPool() {}

    private:
        int _slaver_num;              // 所要创建的线程的数量
        bool _isrunning;              // 线程池是否启动
        std::queue<T> _tasks;          // 任务队列,临界资源
        std::vector<Thread> _slavers; // 线程池
        Cond _cond;                   // 条件变量
        Mutex _mutex;                 // 保护任务队列的锁
        int _slaver_sleep_num;        // 进入等待队列的线程的数量
    };
}

2.3 单例模式

  1. 单例模式:一个类在程序的整个生命周期中,只能创建唯一的一个实例对象,不允许创建多个对象,这种设计模式就叫做单例模式。我们自己实现的线程池是最典型的单例应用场景:全局只需要一个线程池统一管理线程、任务队列和调度,绝不应该创建多个线程池实例。
  2. 单例模式有两种经典实现:饿汉模式和懒汉模式。程序中的全局变量、静态变量存储在静态存储区(数据段),会在程序启动、main 函数执行之前就完成初始化和内存分配;而栈、堆空间则是在程序运行时动态分配
  1. 饿汉实现:在程序启动之初就创建好单例对象(无论是否需要,直接提前创建),是一种空间换时间的设计。
  2. 懒汉实现:延迟加载,只有当用户第一次需要使用单例对象时,才真正创建实例,是一种时间换空间的设计。
  1. 懒汉实现单例模式代码,代码要好好看一遍,要注意的点都写在注释里面:
cpp 复制代码
#pragma once

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

namespace NS_THREAD_POOL_MOUDLE
{
    using namespace NS_THREAD_MODULE;
    using namespace NS_LOG_MODULE;

    const int defaultnum = 5;

    // void Task()
    // {
    //     char name[128];
    //     pthread_getname_np(pthread_self(), name, sizeof name);
    //     while (true)
    //     {
    //         LOG(LogLevel::DEBUG) << "我是一个线程,我正在运行。" << name;
    //     }
    // }

    template <class T>
    class ThreadPool
    {
        //私有化
    private:
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof name);
            while (true)
            {
                T task;
                {
                    LockGuard lockguard(_mutex);
                    //_mutex.Lock();
                    // 任务池是空的且线程池还在运行才能够让线程去休眠
                    while (_tasks.empty() && _isrunning)
                    {
                        _slaver_sleep_num++;
                        _cond.Wait(_mutex);
                        _slaver_sleep_num--;
                    }

                    // 当线程池终止且没有任务要处理的时候再退出循环
                    if (!_isrunning && _tasks.empty())
                    {
                        //_mutex.Unlock();
                        break;
                    }

                    // 有任务就获得任务
                    task = _tasks.front();
                    _tasks.pop();

                    //_mutex.Unlock();
                }

                LOG(LogLevel::INFO) << name << "线程执行任务";
                // 执行任务,不需要在锁里,不然效率太低
                task(); // 统一执行方式,类要求就重载(),函数,lambda表达式就正常用
                LOG(LogLevel::DEBUG) << task.Result();
            }

            // 线程执行完任务后退出
            LOG(LogLevel::INFO) << name << "线程即将退出";
        }
        ThreadPool(int slaver_num = defaultnum)
            : _slaver_num(slaver_num),
              _isrunning(false),
              _slaver_sleep_num(0)
        {
            for (int idx = 0; idx < _slaver_num; idx++)
            {
                _slavers.emplace_back([this]()
                                      { this->HandlerTask(); });
            }
        }

        //禁止拷贝和赋值构造,私有默认构造,这样用户只能通过我们提供的接口创建和调用线程池
        ThreadPool(const ThreadPool<T> &) = delete;
        ThreadPool &operator=(const ThreadPool<T> &) = delete;

    public:
        //提供创建和得到线程池的接口,设置成static
        //此函数属于一整个类,而不是某个实例,也就是说不需要创建实例就可以进行调用,不然用户无法创建单例。
        static ThreadPool* Instance()
        {
            //双重判断,防止在此线程申请的过程中,有另一个线程也过来进行申请。
            //外层 if → 为了提高效率(不加锁快速返回),防止已经有单例了结果多个线程还是在争抢锁去执行接下来的代码
            if (_instance == nullptr)
            {
                _lock.Lock();
                //内层 if → 为了保证线程安全(防止创建多个单例)
                if (_instance == nullptr)
                {
                    //第一次调用,需要创建单例
                    _instance = new ThreadPool<T>();
                    _instance->Start();
                    LOG(LogLevel::INFO) << "第一次创建线程池对象";
                }
                _lock.Unlock();
            }
            return _instance;
        }
        void Start()
        {
            if (_isrunning)
            {
                LOG(LogLevel::WARNING) << "Thread Pool Is Already Running";
                return;
            }
            _isrunning = true; // 不能放到后面,否则可能导致重复启动问题

            // 启动所有的线程
            for (auto &slaver : _slavers)
            {
                slaver.Start();
            }
        }
        void Stop()
        {
            // version1,太过于粗暴
            // if (!_isrunning)
            // {
            //     LOG(LogLevel::WARNING) << "Thread Pool Is Not Running";
            //     return;
            // }

            // for (auto &slaver : _slavers)
            // {
            //     slaver.Die();
            // }
            // _isrunning = false; // 先做事后标记

            // version2
            _mutex.Lock();
            _isrunning = false;
            // 把线程全部唤醒,让它们去处理任务
            if (_slaver_num > 0)
                _cond.Broadcast();
            _mutex.Unlock();
        }
        void Wait()
        {
            for (auto &slaver : _slavers)
            {
                slaver.Join();
            }
        }

        void Enqueue(const T &in)
        {
            _mutex.Lock();
            _tasks.push(in);

            if (_slaver_sleep_num > 0)
                _cond.Signal();

            _mutex.Unlock();
        }

        ~ThreadPool() {}

    private:
        int _slaver_num;              // 所要创建的线程的数量
        bool _isrunning;              // 线程池是否启动
        std::queue<T> _tasks;         // 任务队列,临界资源
        std::vector<Thread> _slavers; // 线程池
        Cond _cond;                   // 条件变量
        Mutex _mutex;                 // 保护任务队列的锁
        int _slaver_sleep_num;        // 进入等待队列的线程的数量

        static ThreadPool<T>* _instance;    //单例本身
        static Mutex _lock;             //这把锁用来保护单例,单例也是一种临界资源
    };

    //静态变量需要在类外进行初始化
    //模板类的静态成员 → 初始化必须带 template<class T>
    template<class T>
    ThreadPool<T> *ThreadPool<T>::_instance = nullptr;

    template<class T>
    Mutex ThreadPool<T>::_lock;
}
  1. 单例模式的核心实现规则:将类的构造函数私有化,从根源禁止外部直接创建对象;显式删除拷贝构造函数和赋值运算符重载,彻底杜绝通过拷贝、赋值生成新的实例;在此基础上,提供一个公有的静态成员函数作为全局唯一访问接口,该接口负责创建(懒汉 / 饿汉模式)并返回全局唯一的单例对象,最终保证程序整个生命周期中,该类有且仅有一个实例

今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!

相关推荐
fengci.1 小时前
蜀道山2024上半部分
android
一条咸鱼¥¥¥1 小时前
【运维笔记】华为防火墙远程接入用户开通与禁用方法
运维·网络·华为·远程用户
Asurplus1 小时前
【Ngrok】Linux运行内网穿透工具Ngrok
linux·运维·服务器·内网穿透·ngrok
ancktion2 小时前
ubuntu多gcc版本切换
linux·运维·ubuntu
热爱Liunx的丘丘人2 小时前
21.内核和内核参数
linux·运维·服务器
乐大师2 小时前
passwd修改密码提示“passwd:Moudle is unknown”
linux·修改密码报错·passwd命令报错
wanhengidc2 小时前
物理服务器的功能都有哪些
运维·服务器·网络·安全·web安全·智能手机
东北甜妹2 小时前
Docker 基础
linux·docker·开源
撩得Android一次心动2 小时前
Android DataBinding 全面解析【源码篇1】
android·android jetpack·databinding