Linux -日志 | 线程池 | 线程安全 | 死锁
文章目录
- 1.日志
- 1.1日志介绍
- 1.2策略模式
- 1.3实现日志类
- 2.线程池
- 2.1线程池介绍
- 2.2线程池的应用场景
- 2.3线程池的设计
- 2.4代码实现
- 2.5修改为单例模式
- 3.线程安全和函数重入问题
- 3.1线程安全和函数重入的概念
- 3.2总结
- 4.死锁
- 4.1什么是死锁
- 4.2产生死锁的必要条件
- 4.3避免死锁
1.日志
1.1日志介绍
计算机中的⽇志是记录系统和软件运⾏中发⽣事件的⽂件,主要作⽤是监控运⾏状态、记录异常信
息,帮助快速定位问题并⽀持程序员进⾏问题修复。它是系统维护、故障排查和安全管理的重要⼯ 具。
⽇志格式以下⼏个指标是必须得有的
• 时间戳
• 日志等级
• 日志内容
以下几个指标是可选的 • 文件名行号 • 进程,线程相关id信息等。
1.2策略模式
1.2.1介绍
策略模式(Strategy Pattern)是一种对象行为型设计模式,它定义了一系列算法,并将每个算法封装在独立的类中,使得它们可以相互替换。这样,算法的变化就不会影响到使用算法的客户端代码,从而提高了代码的灵活性和可维护性。
1.2.2策略模式的结构和角色
策略模式通常由以下几个角色组成:
抽象策略类(Strategy):
• 定义了一个公共接口,用于封装具体的算法。
• 不同的具体策略类会实现这个接口,提供不同的算法实现。
具体策略类(Concrete Strategy):
• 实现了抽象策略类定义的算法接口,具体实现了具体的算法逻辑。
• 每个具体策略类都代表了一种具体的算法或策略。
上下文类(Context):
• 持有一个策略类的引用,在客户端代码中通过该引用调用具体策略的方法。
• 上下文类还可以维护一些公共的状态或行为,这些状态或行为可以在不同的策略之间共享。
1.2.3策略模式的优点
提高了代码的灵活性和可维护性:
• 由于算法被封装在独立的策略类中,因此可以方便地添加、删除或修改算法,而不需要修改客户端代码。
• 这使得系统更加易于维护和扩展。
遵循了开闭原则:
• 策略模式允许在不修改现有代码的情况下添加新的策略类,从而实现了对扩展的开放和对修改的关闭。
减少了条件语句的使用:
• 在不使用策略模式的情况下,客户端代码中可能会包含大量的条件语句来根据不同的算法进行选择。
• 而使用策略模式后,这些条件语句可以被封装在策略类中,客户端只需要选择合适的策略类进行调用即可。
实现了算法的定义、选择和使用的分离:
• 策略模式将算法的定义、选择和使用分离开来,使得算法可以独立变化,而不影响使用算法的客户端代码。
1.2.4策略模式的缺点
增加了系统中的类和对象数量:
• 由于每个具体策略类都需要一个单独的类进行实现,因此这可能会增加系统的复杂性。
客户端需要了解不同的策略类:
• 客户端需要了解并选择合适的策略类进行调用,这可能会增加客户端的复杂性。
可能导致客户端代码变得复杂
1.3实现日志类
1.3.1实现的格式
[具体时间] [日志的类型] [进程id] [源文件名][行号] 其他内容
1.3.2策略
采用策略模拟实现,实现输出到屏幕和输出到文件两种策略。
1.3.3代码实现
//Mutex.hpp -- 互斥锁#pragma once
#include <iostream>
#include <pthread.h>namespace LockModule
{class Mutex{Mutex(const Mutex &) = delete;const Mutex &operator=(const Mutex &) = delete;public:Mutex(){int n = pthread_mutex_init(&_lock, nullptr); // 初始化锁(void)n; // 防止报警告}// 上锁void Lock(){int n = pthread_mutex_lock(&_lock);(void)n;}// 返回锁指针pthread_mutex_t *LockPtr(){return &_lock;}// 解锁void Unlock(){int n = pthread_mutex_unlock(&_lock);(void)n;}~Mutex(){int n = pthread_mutex_destroy(&_lock);(void)n;}private:pthread_mutex_t _lock;};//二次封装 -- 这样可以不用解锁了class LockGuard{public:LockGuard(Mutex &mtx):_mtx(mtx){_mtx.Lock();}~LockGuard(){_mtx.Unlock();}private:Mutex &_mtx;};}/
//log.hpp --日志#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <fstream>
#include <sstream>
#include <memory>
#include <filesystem>
#include <unistd.h>
#include <time.h>
#include "Mutex.hpp"namespace LogMudule
{using namespace LockModule;// 获取时间 格式 年-月-日 时-分-秒std::string CurrentTime(){// 获取时间搓time_t _time = time(nullptr);// 转化为具体时间struct tm curr;localtime_r(&_time, &curr);// 转化为字符串char buff[1024];snprintf(buff, sizeof(buff), "%d-%d-%d %d-%d-%d",curr.tm_year + 1900, // 从1900开始算curr.tm_mon + 1, // 从0开始算curr.tm_mday,curr.tm_hour,curr.tm_min,curr.tm_sec);return buff;}// 日志文件的默认路径和文件名const std::string defaultlogpath = "./";const std::string defaultlogname = "log.txt";// 日志等级enum class LogLevel{DEBUG = 1,INFO,WARNING,ERROR,FATAL};// 映射日志等级std::string Level2String(LogLevel level){switch (level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "INFO";case LogLevel::WARNING:return "WARNIGN";case LogLevel::ERROR:return "ERROR";case LogLevel::FATAL:return "FATAL";default:break;}//不存在return "None";}// 刷新策略.class LogStrategy{public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0; // 纯虚函数};// 输出到屏幕策略class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy(){}~ConsoleLogStrategy(){}void SyncLog(const std::string &message) //message - 日志内容{// 加锁 -- 防止乱序LockGuard _lock(_mutex);std::cout << message << std::endl;}private:Mutex _mutex;};// 文件级(磁盘)策略class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &logpath = defaultlogpath, const std::string &logname = defaultlogname): _logpath(logpath),_logname(logname){// 加锁 -- 线程安全LockGuard _lock(_mutex);// 当前目录存在if (std::filesystem::exists(_logpath)){return;}// 不存在则创建目录try{std::filesystem::create_directories(_logpath);}catch (std::filesystem::filesystem_error &e){std::cerr << e.what() << "\n";}}// 向磁盘文件输出void SyncLog(const std::string &message) //message - 日志内容{// 加锁 -- 防止乱序LockGuard _lock(_mutex);// 以最加式打开文件std::string log = _logpath + _logname;std::ofstream _ofs(log, std::ios::app);if (!_ofs.is_open()){return;}_ofs << message << '\n';_ofs.close();}~FileLogStrategy(){}private:std::string _logpath;std::string _logname;// 锁Mutex _mutex;};// 日志类 - 根据刷新策略进行刷新class Logger{public:Logger(){// 默认向屏幕刷新_strategy = std::make_shared<ConsoleLogStrategy>();}// 设置向屏幕输出void EnableConsoleLog(){_strategy = std::make_shared<ConsoleLogStrategy>();}// 设置向文件输出void EnableFileLog(){_strategy = std::make_shared<FileLogStrategy>();}~Logger() {}//内部类class LogMessage{public://初始化成员变量LogMessage(LogLevel level, const std::string &filename, int line, Logger &logger): _currtime(CurrentTime()),_level(level),_pid(getpid()),_filename(filename),_line(line),_logger(logger){// 将数据构成一条完整的字符串std::stringstream ssbuff;ssbuff << "[" << _currtime << "]"<< "[" << Level2String(_level) << "]"<< "[" << _pid << "]"<< "[" << _filename << "]"<< "[" << _line << "]";_loginfo = ssbuff.str();}template <typename T>LogMessage &operator<<(const T &info){// 添加字符串内容std::stringstream s;s << info;_loginfo += s.str();return *this;}//在析构时调用~LogMessage(){if (_logger._strategy != nullptr){_logger._strategy->SyncLog(_loginfo);}}private:std::string _currtime; // 当前日志的时间LogLevel _level; // 日志等级pid_t _pid; // 进程pidstd::string _filename; // 源文件名称int _line; // 日志所在的行号Logger &_logger; // 负责根据不同的策略进行刷新std::string _loginfo; // 一条完整的日志记录};// 就是要拷贝,故意的拷贝-- 这样执行完语句自动销毁就会调用析构实行策略LogMessage operator()(LogLevel level, const std::string &filename, int line){return LogMessage(level, filename, line, *this);}private:std::shared_ptr<LogStrategy> _strategy; // 日志刷新的策略方案};//实例
Logger logger;//使用宏替换
#define LOG(Level) logger(Level, __FILE__, __LINE__) // 调用重载() __FILE__, __LINE__ -- 获取源文件名和当前行号//切换策略
#define ENABLE_CONSOLE_LOG() logger.EnableConsoleLog()
#define ENABLE_FILE_LOG() logger.EnableFileLog()}
//Main.cc
#include"log.hpp"using namespace LogMudule;int main()
{ENABLE_CONSOLE_LOG()LOG(LogLevel::DEBUG)<<"我是其他内容";return 0;
}
执行效果
2.线程池
2.1线程池介绍
⼀种线程使⽤模式。线程过多会带来调度开销,进⽽影响缓存局部性和整体性能。⽽线程池维护着多 个线程,等待着监督管理者分配可并发执⾏的任务。这避免了在处理短时间任务时创建与销毁线程的 代价。线程池不仅能够保证内核的充分利⽤,还能防⽌过分调度。可⽤线程数量应该取决于可⽤的并发处理器、处理器内核、内存、⽹络sockets等的数量。
2.2线程池的应用场景
- 需要⼤量的线程来完成任务,且完成任务的时间比较短。
- 对性能要求苛刻的应⽤,⽐如要求服务器迅速响应客⼾请求。
- 接受突发性的⼤量请求,但不⾄于使服务器因此产⽣⼤量线程的应⽤。
2.3线程池的设计
创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执⾏任务对象中的任务接⼝。
2.4代码实现
//条件变量封装
//cond.hpp
#pragma once#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"namespace CondModule
{using namespace LockModule;class Cond{public:Cond(){int n = pthread_cond_init(&_cond, nullptr); // 初始化条件变量(void)n;}// 等待void Wait(Mutex &mutex) // 让我们的线程释放曾经持有的锁!Mutex -- 封装的锁{int n = pthread_cond_wait(&_cond, mutex.LockPtr()); // 进行等待(void)n;}// 唤醒至少一个线程void Notify(){int n = pthread_cond_signal(&_cond);(void)n;}// 唤醒全部线程void NotifyAll(){int n = pthread_cond_broadcast(&_cond);(void)n;}~Cond(){int n = pthread_cond_destroy(&_cond);(void)n;}private:pthread_cond_t _cond; // 条件变量};
}
//Thread.hpp -- 创建线程封装
#ifndef _THREAD_HPP__
#define _THREAD_HPP__#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include <sys/types.h>
#include <unistd.h>namespace ThreadModule
{//函数包装using func_t = std::function<void(std::string)>;//线程个数static int number = 1;//状态enum class TSTATUS{NEW,RUNNING,STOP};class Thread{private://执行线程函数static void *Routine(void *args){//强制类型转换Thread* thread = (Thread*)args;std::cout<<"我是"<<thread->_name<<" :";//调用任务thread->_func(thread->_name);return nullptr;}public:Thread(func_t func) : _func(func), _status(TSTATUS::NEW), _joinable(false){_name = "Thread-" + std::to_string(number);_pid = getpid();number++;}//创建线程bool Start(){if (_status != TSTATUS::RUNNING) // 保证线程处于非运行状态{int n = pthread_create(&_tid, nullptr, Routine, (void *)this); // 创建线程if (n != 0){return false;}_status = TSTATUS::RUNNING; // 更新状态return true;}return false;}//取消线程bool Stop(){if(_status == TSTATUS::RUNNING) //保证线程处于运行状态{int n = pthread_cancel(_tid); //取消线程if(n != 0){return false;}_status = TSTATUS::STOP; // 更新状态return true;}return false;}//等待线程bool Join(){//保证线程不处于分离状态且处于运行状态if(!_joinable && _status == TSTATUS::RUNNING) { //等待线程,默认不关心线程状态int n = pthread_join(_tid,nullptr);if(n != 0){return false;}_status = TSTATUS::STOP; // 更新状态return true;}return false;}//线程分离void Detach(){//保证线程不处于分离状态且处于运行状态if(!_joinable && _status == TSTATUS::RUNNING) {int n = pthread_detach(_tid); //进行线程分离if(n != 0){return ;}std::cout<<"完成线程分离\n"<<std::endl;_joinable = true; //更新分离状态}}bool IsJoinable(){return _joinable;}std::string Name(){return _name;}~Thread() {}private:std::string _name; // 线程namepthread_t _tid; // 线程idpid_t _pid; // 进程idbool _joinable; // 是否是分离的,默认不是func_t _func; // 线程执行的任务TSTATUS _status; // 线程状态};
}#endif
/
//ThreadPool.hpp 线程池
namespace ThreadPoolModule
{//展开命名空间using namespace LogMudule;using namespace ThreadModule;using namespace LockModule;using namespace CondModule;//重命名using thread_t = std::shared_ptr<Thread>;//线程池类template <typename T>class ThreadPool{private: const static int defaultnum = 5; //默认的线程数量 //注意:要加static,不然defaultnum需要实例化才生成,这会导致编译时失败//判断任务队列是否为空bool IsEmpty(){return _taskq.empty();}//线程同一执行的函数void HandlerTask(std::string name){//日志LOG(LogLevel::DEBUG)<<name<<"进入主逻辑";//1.拿任务while(true){T t; //任务LockGuard l(_lock); //访问临界资源 - 加互斥锁//为空且为运行状态时才进行等待while(IsEmpty() && _isrunning){LOG(LogLevel::DEBUG)<<name<<"进入等待";_wait_num++;_cond.Wait(_lock);_wait_num--;LOG(LogLevel::DEBUG)<<name<<"完成等待";} //走到这里只有任务队列不为空或者_isrunning == false -- 这种情况需要结束线程if(IsEmpty() && !_isrunning){break;}//2.取任务t = _taskq.front();_taskq.pop();//3.执行任务t(name);}}public://初始化成员变量ThreadPool(int num = defaultnum) : _num(num), _wait_num(0), _isrunning(false){for(int i = 0; i < _num; i++){//初始化线程池 - 此时还未正式创建线程_threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HandlerTask,this,std::placeholders::_1)));LOG(LogLevel::DEBUG)<<"对象"<<i<<"完成创建";}}//进入任务队列void Equeue(T &&in){//线程池不在运行状态if(_isrunning == false)return;访问临界资源 - 加互斥锁LockGuard l(_lock);_taskq.push(std::move(in));//当有等待线程时需要唤醒if(_wait_num > 0){_cond.Notify();LOG(LogLevel::DEBUG)<<"唤醒一个线程";}}//启动线程池 -- 正式创建线程void Start(){//已经是运行的状态了if(_isrunning)return ;//第一次设置为运行状态_isrunning = true;for(auto &e : _threads){e->Start();}}//进程线程等待void Wait(){for(auto &e : _threads){e->Join();} }//暂停线程池void Stop(){访问临界资源 - 加互斥锁LockGuard l(_lock);if(_isrunning == false)return;//修改状态 -- 此时不能添加任务了_isrunning = false;//唤醒其他线程,再让其他线程将剩下的任务执行完了,最后再自己退出。if(_wait_num > 0){_cond.NotifyAll();}}~ThreadPool(){}private:std::vector<thread_t> _threads; //线程组sint _num; //线程数量int _wait_num; //正在等待的线程数量std::queue<T> _taskq; // 临界资源Mutex _lock; //锁Cond _cond; //条件变量bool _isrunning; //当前线程状态};}
//
//Main.cc
#include"ThreadPool.hpp"using namespace ThreadPoolModule;void add(std::string name)
{LOG(LogLevel::DEBUG)<<name<<"进入计算";
}int main()
{ThreadPool<func_t> tp;//启动tp.Start();int n = 10;while(n--){//入任务tp.Equeue(add);sleep(1);}//暂停tp.Stop();//等待tp.Wait();return 0;
}
执行效果
2.5修改为单例模式
2.5.1什么是单例模式
该类的实例化对象在整个代码中只能有一个。
2.5.2实现单例模式的方式
- 饿汉模式:不管什么,一上来先创建好。
- 懒汉模式:需要用时才进行创建。
2.5.3使用懒汉模式实现线程池单例
//1。将构造函数私有化和删除拷贝构造和赋值重载 -这样外部就无法创建了
private:ThreadPool(int num = defaultnum) : _num(num), _wait_num(0), _isrunning(false){for(int i = 0; i < _num; i++){_threads.push_back(std::make_shared<Thread> (std::bind(&ThreadPool::HandlerTask,this,std::placeholders::_1)));LOG(LogLevel::DEBUG)<<"对象"<<i<<"完成创建";}}ThreadPool(const ThreadPool<T> & tp) = delete;ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;//2.提供一个静态线程池对象指针和一个互斥锁static ThreadPool<T> *tp; //线程池指针static Mutex _mutex; //为单例的锁public://3.提供静态函数-全局可用,需要用线程池时就调用获取线程池对象static ThreadPool<T> *getInstance(){//双if判断可以对减少对锁的申请if(tp == nullptr){//因为被多线程调用时,当多个线程同时进入时可能会被创建多个,所以加互斥锁LockGuard l(_mutex);if(tp == nullptr){ //创建线程池tp = new ThreadPool<T>();LOG(LogLevel::DEBUG)<<"第一次完成创建";}}return tp;}
///
//调用方式
//main.cc
void add(std::string name)
{LOG(LogLevel::DEBUG)<<name<<"进入计算";
}
int main()
{ThreadPool<func_t>::getInstance()->Start();int n = 10;while(n--){ThreadPool<func_t>::getInstance()->Equeue(add);sleep(1);}ThreadPool<func_t>::getInstance()->Stop();ThreadPool<func_t>::getInstance()->Wait();return 0;
}
3.线程安全和函数重入问题
3.1线程安全和函数重入的概念
- 线程安全就是多个线程在访问共享资源时,能够正确地执⾏,不会相互⼲扰或破坏彼此的执行结果。⼀般⽽⾔,多个线程并发同⼀段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进⾏操作,并且没有锁保护的情况下,容易出现该问题。
- 重⼊:同⼀个函数被不同的执⾏流调⽤,当前⼀个流程还没有执⾏完,就有其他的执⾏流再次进⼊,我们称之为重⼊。⼀个函数在重⼊的情况下,运⾏结果不会出现任何不同或者任何问题,则该函数被 称为可重⼊函数,否则,是不可重⼊函数。
一般的可重入:
多线程访问函数
信号导致一个执行流多次进入同一个函数
3.2总结
可重入函数一定是线程安全的,线程安全的函数不一定是可重入的(如一个执行流正在执行一个加了锁的线程安全函数并且此时拿着锁,但是由于信号(该信号的处理函数也是当前函数)导致该线程中断并去执行信号处理函数,当该执行流申请锁时被锁阻塞,而该执行流又无法回去释放锁,这使当前线程一直被阻塞,从而出现死锁)。
补充:信号是由进程其中的一个线程处理的,每个线程中都有信号屏蔽字,信号屏蔽字可以设置屏蔽信号。
4.死锁
4.1什么是死锁
死锁是指在⼀组进程或者线程中的各个进程或者线程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的⼀种永久等待状态。如:假设临界资源同时需要两把锁才能访问,线程A申请了其中一把锁,线程B申请了另一把锁,此时线程A和B都会去申请没拿到的锁,但是线程A和B都不释放自己手里的锁,这导致线程A和B都被一直阻塞住了。
4.2产生死锁的必要条件
- 互斥条件:⼀个资源每次只能被⼀个执行流使用。
- 请求与保持条件:⼀个执行流因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:⼀个执行流已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若⼲执行流之间形成⼀种头尾相接的循环等待资源的关系。
4.3避免死锁
破坏其中的一个条件即可。
如:破坏循环等待条件问题:资源⼀次性分配, 使⽤超时机制、加锁顺序⼀致