Linux系统与系统编程(16)——日志、线程池的实现与线程安全

前言

**欢迎观看Linux系列文章!!**第16篇主要讲述了实现日志和线程池以及线程安全,其中包含了重入问题、死锁和STL、智能指针的线程安全。

日志与策略模式

日志

相当于程序的日记,用来记录程序的运行情况。

一般格式如下(实际要什么格式是可以自己设计的,这里给出一个样例)

bash 复制代码
[可读性好的时间][日志等级][进程pid][打印对应日志的文件名][行号] -- 消息内容(支持可变参数)
[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

日志等级------标志消息类型,一般有如下几个(也是可以自定义的)。

DEBUG:调试消息。

INFO:常规消息。

WARNING:告警消息,不影响运行过程和运行结果,但是有些调用有问题需要让程序员知道。

ERROR:错误消息,不影响运行过程,但是运行结果出现问题的情况。

FATAL:致命消息,出现导致程序无法再正常运行的错误,需要直接终止程序进行修复。

策略模式

其实就是决定好要把日志信息刷新到哪里(文件、显示器······),即刷新策略。

日志代码实现

代码如下:

Log.hpp

cpp 复制代码
#ifndef __LOG_HPP__
#define __LOG_HPP__

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

using namespace std;

namespace LogMod
{
    using namespace MutexModule;

    const string gsep = "\r\n";
    // 刷新策略

    // 利用C++的多态,实现根据不同情况执行不同策略叫做策略模式
    //  刷新策略基类
    class LogStrategy
    {
    public:
        ~LogStrategy() = default;
        virtual void SyncLog(const string &message) = 0;
    };
    // 显示器策略子类
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {
        }
        // 重载信息刷新函数
        void SyncLog(const string &message) override
        {
            // 加锁
            LockGuard lockguard(_mutex);
            cout << message << gsep;
        }
        ~ConsoleLogStrategy()
        {
        }

    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)
        {
            // 判断当前路径下文件是否存在
            if (filesystem::exists(_path))
            {
                return;
            }
            // 为了预防创建失败,使用try-catch捕捉错误
            try
            {
                // 不存在就创建文件
                filesystem::create_directories(_path);
            }
            catch (const filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
        }
        // 重载信息刷新函数
        void SyncLog(const string &message) override
        {
            string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
            // 以追加写入(app)的方式打开输出流out
            ofstream out(filename, ios::app);
            if (!out.is_open())
            {
                return;
            }
            // 向输出流输出信息
            out << message << gsep;
            // 关闭输出流
            out.close();
        }
        ~FileLogStrategy()
        {
        }

    private:
        string _path; // 日志所在路径
        string _file; // 日志文件本身
    };
    // 日志等级
    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 "UNKNOWN";
        }
    }

    // 消息时间
    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,
        curr_tm.tm_mday,
        curr_tm.tm_hour,
        curr_tm.tm_min,
        curr_tm.tm_sec);
        return timebuffer;

        //struct tm{
        //int tm_sec;
        //int tm_min;
        //int tm_hour;
        //int tm_mday;
        //int tm_mon;
        //int tm_year;//这里是距离1900的差值
        //int tm_wday;
        //int tm_yday;
        //int tm_isdst;
        //}
    }
    // 日志类:由它来形成日志消息,并根据不同策略进行刷新
    class Logger
    {
    public:
        Logger()
        {
            EnableConsoleLogStrategy();
        }
        //文件策略
        void EnableFileLogStrategy()
        {
            _fflush_strategy = make_unique<FileLogStrategy>();
        }
        //显示器策略
        void EnableConsoleLogStrategy()
        {
            _fflush_strategy = make_unique<ConsoleLogStrategy>();
        }
        //内部类,表示未来的一条日志消息
        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)
            {
                //合并以上参数,完成日志消息的左半部分
                //字符串流
                stringstream ss;
                ss << "[" << _curr_time << "]"
                << "[" << Level2Str(_level) << "]"
                << "[" << _pid << "]"
                << "[" << _src_name << "]"
                << "[" << _line_number << "]"
                << "- ";
                //把字符串流转为字符串
                _loginfo = ss.str();
            }
            template<typename T>
            LogMessage &operator << (const T &info)
            {
                //日志右边的可变参数部分。
                //LogMessage() << "hello world" << "XXXX" << 3.14 << 1234
                stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this;
            }

            ~LogMessage()
            {
                if(_logger._fflush_strategy)
                {
                    _logger._fflush_strategy->SyncLog(_loginfo);
                }
            }
        private:
            string _curr_time;//当前时间
            Loglevel _level;//日志等级
            pid_t _pid;//进程ID
            string _src_name;//源文件名
            int _line_number;//行号
            string _loginfo;//合并后的完整消息
            Logger &_logger;
        };
        LogMessage operator()(Loglevel level, string name, int line)
        {
            return LogMessage(level, name, line, *this);
        }
        ~Logger()
        {
        }

    private:
        unique_ptr<LogStrategy> _fflush_strategy;
    };

    //全局日志对象
    Logger logger;

    //使用宏,简化用户操作,获取文件名和行号
    #define Log(level) logger(level, __FILE__, __LINE__)
    #define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
    #define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
}

#endif

Mutex.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }
        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }
        pthread_mutex_t *Get()
        {
            return &_mutex;
        }
    private:
        pthread_mutex_t _mutex;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex):_mutex(mutex)
        {
            _mutex.Lock();
        }
        ~LockGuard()
        {
            _mutex.Unlock();
        }
    private:
        Mutex &_mutex;
    };
}

Main.cpp

cpp 复制代码
#include "Log.hpp"
#include <memory>

using namespace LogMod;

int main()
{
    //C++14,构建对象并返回智能指针
    //unique_ptr<LogStrategy> strategy = make_unique<FileLogStrategy>();
    //strategy->SyncLog("hello log!");


    //logger(Loglevel::DEBUG, "main.cc", 10) << "hello world";

    Enable_Console_Log_Strategy();
    Log(Loglevel::DEBUG) << "hello world" << 3.14;
    Log(Loglevel::DEBUG) << "hello world" << 3.14;
    Log(Loglevel::DEBUG) << "hello world" << 3.14;
    Log(Loglevel::DEBUG) << "hello world" << 3.14;
    return 0;
} 

线程池

当线程池退出时,内部的线程状态:

1.等待

2.等待唤醒

3.处理任务

当线程池要退出的时候,内部的任务应该被完全取完,并且是非运行状态,才能让对应的线程退出。

线程池代码实现

关键代码:ThreadPool.hpp

cpp 复制代码
#pragma once

#include <vector>
#include "Thread.hpp"
#include <string>
#include <queue>
#include "Log.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"

namespace ThreadPoolMod
{
    using namespace CondModule;
    using namespace ThreadModlue;
    using namespace LogMod;

    static const int gnum = 5;
    template <typename T>
    class ThreadPool
    {
    private:
        void WakeUpAllThread()
        {
            LockGuard locgkuard(_mutex);
            if (_sleepernum)
                _cond.Broadcast();
            Log(Loglevel::INFO) << "唤醒所有休眠线程";
        }
        void WakeUpOne()
        {
            _cond.Signal();
            Log(Loglevel::INFO) << "唤醒一个休眠线程";
        }
    public:
        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) << "创建新线程成功";
            }
        }
        // 启动线程
        void Start()
        {
            if (_isrunning)
                return;
            _isrunning = true;
            for (auto &thread : _threads)
            {
                thread.Start();
                Log(Loglevel::INFO) << "启动新线程成功:" << thread.Name();
            }
        }
        void Stop()
        {
            if (!_isrunning)
                return;
            _isrunning = false;
            // 唤醒所有的线程
            WakeUpAllThread();
        }
        void Join()
        {
            for(auto &thread:_threads)
            {
                thread.Join();
            }
        }
        // 处理任务
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T t;
                {
                    LockGuard lockguard(_mutex);
                    // 队列为空,等待任务 或 线程池没有退出
                    while (_taskq.empty() && _isrunning)
                    {
                        _sleepernum++;
                        _cond.Wait(_mutex);
                        _sleepernum--;
                    }
                    // 内部线程已经被唤醒
                    if (!_isrunning && _taskq.empty())
                    {
                        Log(Loglevel::INFO) << name << "退出了,线程池退出&&任务队列为空";
                        break;
                    }

                    // 出循环则有任务
                    t = _taskq.front();
                    _taskq.pop();
                    //t(); // 处理任务
                }
            }
        }
        bool Enqueue(const T &in)
        {
            if(_isrunning)
            {
                LockGuard lockgurad(_mutex);
                _taskq.push(in);
                if(_threads.size() - _sleepernum == 0)
                    WakeUpOne();
                return true;
            }
            return false;
        }
        ~ThreadPool()
        {
        }

    private:
        vector<Thread> _threads;
        int _num; // 线程池中线程的个数
        queue<T> _taskq;
        Cond _cond;
        Mutex _mutex;
        bool _isrunning;
        bool _isdetach;
        int _sleepernum;
    };
}

Task.hpp:模拟需处理的任务

cpp 复制代码
#pragma once
#include <iostream>
#include <unistd.h>
#include <functional>
#include "Log.hpp"


using namespace LogMod;
// 任务形式2
// 我们定义了一个任务类型,返回值void,参数为空
using task_t = std::function<void()>;

void Download()
{
    Log(Loglevel::DEBUG) << "我是一个下载任务...";
}

// 任务形式1
class Task
{
public:
    Task(){}
    Task(int x, int y):_x(x), _y(y)
    {
    }
    void Execute()
    {
        _result = _x + _y;
    }
    int X() { return _x; }
    int Y() { return _y; }
    int Result()
    {
        return _result;
    }
private:
    int _x;
    int _y;
    int _result;
};

Main.cpp

cpp 复制代码
#include "Log.hpp"
#include "ThreadPool.hpp"
#include <memory>
#include "Task.hpp"

using namespace LogMod;
using namespace ThreadPoolMod;

int main()
{
    //C++14,构建对象并返回智能指针
    //unique_ptr<LogStrategy> strategy = make_unique<FileLogStrategy>();
    //strategy->SyncLog("hello log!");


    //logger(Loglevel::DEBUG, "main.cc", 10) << "hello world";

    //Enable_Console_Log_Strategy();
    //Log(Loglevel::DEBUG) << "hello world" << 3.14;
    //Log(Loglevel::DEBUG) << "hello world" << 3.14;
    //Log(Loglevel::DEBUG) << "hello world" << 3.14;
    //Log(Loglevel::DEBUG) << "hello world" << 3.14;
    
    Enable_Console_Log_Strategy();
    ThreadPool<task_t> *tp = new ThreadPool<task_t>();
    tp->Start();
    int count = 10;
    while(count)
    {
        tp->Enqueue(Download);
        sleep(1);
        count--;
    }
    sleep(5);
    //tq->Equeue()t;
    tp->Stop();
    sleep(1);
    tp->Join();
    return 0;
} 

Thread.hpp:对线程的封装

cpp 复制代码
#ifndef _THREAD_H_
#define _THREAD_H_

#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <functional>
#include "Log.hpp"

namespace ThreadModlue
{
    using namespace LogMod;
    static u_int32_t number = 1; // bug

    class Thread
    {
        using func_t = std::function<void()>; // 暂时这样写,完全够了
    private:
        void EnableDetach()
        {
            _isdetach = true;
        }
        void EnableRunning()
        {
            _isrunning = true;
        }
        static void *Routine(void *args) // 属于类内的成员函数,默认包含this指针!
        {
            Thread *self = static_cast<Thread *>(args);
            self->EnableRunning();
            if (self->_isdetach)
                self->Detach();
            pthread_setname_np(self->_tid, self->_name.c_str());
            self->_func(); // 回调处理

            return nullptr;
        }
        // bug
    public:
        Thread(func_t func)
            : _tid(0),
              _isdetach(false),
              _isrunning(false),
              res(nullptr),
              _func(func)
        {
            _name = "thread-" + std::to_string(number++);
        }
        void Detach()
        {
            if (_isdetach)
                return;
            if (_isrunning)
                pthread_detach(_tid);
            EnableDetach();
        }

        string Name()
        {
            return _name;
        }
        
        bool Start()
        {
            if (_isrunning)
                return false;
            int n = pthread_create(&_tid, nullptr, Routine, this);
            if (n != 0)
            {
                return false;
            }
            else
            {
                return true;
            }
        }
        bool Stop()
        {
            if (_isrunning)
            {
                int n = pthread_cancel(_tid);
                if (n != 0)
                {
                    return false;
                }
                else
                {
                    _isrunning = false;
                    return true;
                }
            }
            return false;
        }
        void Join()
        {
            if (!_isdetach)
                return;
            int n = pthread_join(_tid, &res);
            if (n != 0)
            {
                Log(Loglevel::DEBUG) << "Join线程失败";
            }
            else
            {
                Log(Loglevel::DEBUG) << "Join线程成功";
            }
        }
        ~Thread()
        {
        }

    private:
        pthread_t _tid;
        std::string _name;
        bool _isdetach;
        bool _isrunning;
        void *res;
        func_t _func;
    };
}

#endif

线程安全

线程安全的单例模式

什么是单例模式

一个类在系统中只有一个实例,并提供一个全局访问点来获取这个实例.

简单理解:某个类只允许创建一次对象,之后所有地方拿到的都是同一个对象。比如配置管理器、日志对象、线程池、连接池等。

所以单例模式,指的就是单个实例的创建模式。

实现方式

饿汉模式:就像是吃完饭立刻洗碗,下一顿吃的时候就可以立刻拿碗吃饭。

懒汉模式:就像是吃完饭先放下碗,下一顿吃的时候要用到这个碗里,再洗了用。

饿汉模式
cpp 复制代码
template<typename T>
class Singleton
{
    static T data;
public:
    static T* GetInstance()
    {
        return &data;
    }
};

因为利用了static静态加载的规则,创建进程时就创建好了这个静态对象,而且只有这一个静态对象。

一般单例的体积不大,不需要再创建新的对象时才用这种模式。绝大多数情况用的都是懒汉方式。

懒汉模式
cpp 复制代码
template<typename T>
class Singleton
{
    static T* inst;
public:
    static T* GetInstance()
    {
        if(inst = null)
            inst =  new T();
        return inst;
    }
};

这里不需要想饿汉模式一样,只需要一个静态指针,不需要再加载进程时就把这个具体对象的空间创建出来。再需要这个具体对象(单例)的时候,才回去new一个对象。

如果已经new过了,就不会再创建一个对象了。

懒汉模式的核心思想是"延时加载",从而能够优化服务器的启动速度 。其次,在"延迟"的时候, 因为内存还未申请给懒汉使用,所以再给懒汉使用之前可以给他其他线程先用着,从而提高内存使用率。

但是上述代码存在严重问题:线程不安全。 如果两个线程同时调用GetInatance(),且该函数还未被调用过,就可能会创建出两个T对象的实例,这就破坏了单例模式的规定。当然了,要解决也很简单,加入互斥同步的代码即可。

把上述线程池的代码稍作修改和添加,就可以得到懒汉方式实现的单例模式线程安全的线程池。

ThreadPool.hpp:

cpp 复制代码
class ThreadPool
{
    ······

    public:
        static ThreadPool<T> *GetInstance()
        {
        //单例对象也是全局的临界资源
        LockGuard lockguard(_lock);
            Log(Loglevel::DEBUG)<<"获取单例···";
            if(inc == nullptr)
            {
                Log(Loglevel::DEBUG) << "首次使用单例,创建之···";
                inc = new ThreadPool<T>();
                inc -> Start();
            }
            return inc;
        }

    ······

    private:
            ······

            static ThreadPool<T> *inc;//懒汉模式单例指针
            static Mutex _lock;//单例对象的锁
        
};
template<typename T>
ThreadPool<T> *ThreadPool<T>::inc = nullptr;//指针初始化

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

Main.cpp:

cpp 复制代码
int main()
{
    
    Enable_Console_Log_Strategy();

    int count = 10;
    while(count)
    {
        sleep(1);
        //调用静态成员函数,不需要创建对象
        //tq->Equeue()t;
        ThreadPool<task_t>::GetInstance()->Enqueue(Download);
        count--;
    }
    sleep(5);
    ThreadPool<task_t>::GetInstance()->Stop();
    sleep(1);
    ThreadPool<task_t>::GetInstance()->Join();
    return 0;
}

线程安全和重入问题

概念

**线程安全:**多个线程在访问共享资源的时候能够正确地执行,不会互相干扰或破坏彼此的执行结果。再多全局变量或者静态变量进行操作,并没有锁的保护时,就会有该方面问题出现。

**重入:**同一个函数被不同的执行流调用,当前有一个流程还没有执行完,就有其他的执行流再次进入,就叫做重入。一个函数在重入的情况下,运行结果不会出现任何问题的,就叫这个函数为可重入函数,否则是不可重入函数。

**函数是可重入的,那就是线程安全的。但是线程安全了,不代表函数一定是重入的。**线程安全不保证同一线程内函数执行到一半时被再次进入也正确。比如:用锁实现线程安全时,如果函数持有锁期间被信号打断,信号处理函数又调用同一个函数,就会再次尝试获取同一个非递归锁,导致死锁(下文细说)。所以它线程安全,但不可重入。

死锁问题

什么是死锁

不管是单线程还是多线程都可能会出现死锁,单线程连续申请了同一把锁就会出现死锁(通常说的都是多执行流的场景,下文只讨论多执行流)。

死锁指的是一组进程中的各个进程均占用不会释放的资源,但因互相申请 ++被其他进程所占用不会释放的资源++而处于的一种永久等待状态。

如图,双方都持有自己的锁且不会释放,并且都在申请对方的锁,由于双方都不会释放自己的锁,所以双方都只能挂起,什么都做不了。

死锁的四个必要条件

1️⃣互斥条件:一个资源只能被一个执行流使用,就是锁。(资源)

2️⃣请求与保持条件:一个执行流因为请求资源而阻塞时,对已获得的资源保持不放。(自己不愿意放弃资源)

3️⃣不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺。(别人不能抢夺资源)

4️⃣循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。(都期望获得他人资源,而且形成了环路)

避免死锁的方法

避免死锁,就破坏死锁的必要条件。

1️⃣破坏循环等待问题:资源一次性分配,使用超时机制,加锁顺序一致。

资源一次性分配:要么都没有锁,要么就一次性申请完所有锁。

加锁顺序一致:规定执行流只能按顺序申请锁,先申请锁a再申请锁b,这样就不会出现线程2先有锁b,再申请锁a的情况。

2️⃣破坏请求与保持条件

使用超时机制:一定时间内拿不到锁,就主动释放锁。

STL、智能指针和线程安全

STL容器是否线程安全?

C++的STL容器不是线程安全的。

为了将性能挖掘到极致,设计时舍去了加锁来保证线程安全的做法。对于不同的容器,加锁方式不同,性能可能也不同。所以默认STL容器不是线程安全的。如果需要再多线程环境下使用STL容器,往往需要调用者自行保证线程安全。

智能指针是否线程安全?

对于uniqu_ptr本身,因为只在当前的代码块范围内生效,因此不涉及线程安全的问题。

对于shared_ptr,多个对象需要共用一个引用计数变量,这个计数变量是临界资源,所以会存在线程安全问题,但是标准库在实现时已经考虑到这个问题了。设计shared_ptr时,设计者基于原子操作的前提下,最大程度保证了shared_ptr的高效性,因此也基本不会存在线程安全问题。

❤~~本文完结!!感谢观看!!接下来更精彩!!欢迎来我博客做客~~❤

相关推荐
杨云龙UP1 小时前
DB2 HADR 主备架构活动日志参数优化操作手册
linux·运维·服务器·数据库·db2·db2 hadr·日志参数调整
csdn_aspnet1 小时前
如何从电脑主机拷东西到VWmare虚拟机
linux·运维·服务器·windows·vmware·虚拟机
Vcaker1 小时前
Linux学习28-Kubernetes service
linux·运维·学习
有梦想的咕噜2 小时前
Newtonsoft.Json (Json.NET) 常用方法汇总
linux·json·.net
码农小韩2 小时前
Linux应用开发(五)——线程
linux·操作系统·linux驱动·嵌入式软件开发·嵌入式操作系统·linux应用
Polevne2 小时前
C# GRPC 一元与双向流
linux·算法·c#
拂拉氏2 小时前
【知识讲解】 Linux程序替换相关接口讲解
linux·程序替换
星源~2 小时前
zephyr-Linux环境下搭建步骤
linux·mcu·嵌入式开发·zephyr
Huangjin007_2 小时前
【Linux 系统篇(二十)】进程(八):进程创建、进程退出
linux·运维·服务器