【Linux庖丁解牛】— 日志&进程池 !
1. 日志与策略模式
1.1 认识日志
计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信 息,帮助快速定位问题并⽀持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。
日志格式以下几个指标是必须得有的 • 时间戳• 日志等级 • 日志内容
以下几个指标是可选的 • 文件名行号 • 进程,线程相关id信息等
日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采用自定义日志的方式。 这里我们采用设计模式-策略模式来进行日志的设计。
以下就是我们最终设计的日志格式:
1.2 什么是策略模式
官方说法:策略模式(Strategy Pattern)是一种行为型设计模式,其核心思想是将一系列算法封装成独立的类,并通过一个共同的接口使它们可以相互替换,从而实现算法的动态选择和解耦。
在我们的日志实现中,我们不仅想要将我们的日志信息打印到显示器中,还想要将我们的日志信息写入到指定文件中,我们希望可以根据自己的需求切换日志的写入方式。那么,我们可以采用策略模式来设计我们的日志。具体做法就是实现一个基类,实现两个子类继承基类重写写入方法。然后,在我们的日志类当中为外部提供选择刷新方案的接口【显示器或指定文件】。
2. 基于策略模式的自定义日志
2.1 策略实现
上面已经具体说了实现思路,这里直接上代码:
2.2 日志类logger实现
> 策略选择接口
在日志类内部我们需要基类指针,那我们这里选择用智能指针。
对外提供策略选择接口:
在构造日志类的时候,我们默认使用显示器策略:
> 内部类log_message实现
这里为什么要使用内部类呢??
主要是为了方便构建一条日志信息,我们在logger类中重载()方法,用户在外部传递需要的信息,比如:日志等级,行号,文件名。而在()方法中,我们构建一个临时log_message对象,我们将用户信息传给这个临时对象。而这个临时对象内部重载了<<方法,该方法负责获取用户的打印信息,最终这个临时对象会处理这些信息拼接起来形成一条日志。在这个临时对象析构的时候,我们在析构函数中调用刷新方法。至此,一条日志信息就可以按照我们的需求刷新了!
这样设计主要是方便上层的使用,应为我们还可以在类内部定义宏用__FILE__和__LINE__来获取行号和当前文件名。而上层只需要传自己想要的日志等级和debug信息即可。
如果我们直接在logger内部重载<<,那用户使用起来就太不方便了:每次都要传参构造对象,然后使用对象写入信息。虽然也可以使用匿名对象直接<<,但还是要传那么多参数,太龊了。也没有办法定义像上面的宏,因为这是全局的变量。每条日志都要使用的话,里面的日志信息每次使用都要清空【每次<<调用都刷新到文件中】,但这样打印到屏幕的信息就不是我们习惯的行刷新了……额,这里就不做考虑了。
2.3 日志log.hpp源码
主要的实现思路有了,下面就是实现的一些具体代码了,这考验的就是我们的编码能力了~~
#pragma once#include #include #include #include #include #include #include #include #include \"mutex.hpp\"namespace log_module{ using namespace mutex_module; const std::string default_path = \"./log/\"; const std::string default_name = \"my.log\"; // 基类 刷新策略 class log_strategy { public: virtual ~log_strategy() = default; virtual void sync_log(const std::string &message) = 0; }; // 子类 向显示器刷新 class screen_strategy : public log_strategy { public: void sync_log(const std::string &message) override { // 显示器是共享资源,加锁维护 lock_guard lg(_mutex); std::cout << message << std::endl; } ~screen_strategy() { } private: mutex _mutex; }; // 子类 向指定文件刷新 class file_strategy : public log_strategy { public: file_strategy(const std::string &path = default_path, const std::string &name = default_name) : _path(path), _name(name) { // 如果路径不存在则需要新建路径 lock_guard lockguard(_mutex); if (filesystem::exists(_path)) return; try { std::filesystem::create_directories(_path); } catch (const std::filesystem::filesystem_error &e) { std::cerr << e.what() << \'\\n\'; } } // 将⼀条日志信息写入到文件中 void sync_log(const std::string &message) override { lock_guard lockguard(_mutex); std::string log = _path + _name; std::ofstream out(log.c_str(), std::ios::app); // 追加式 if (!out.is_open()) return; out << message << \"\\n\"; out.close(); } ~file_strategy() { } private: mutex _mutex; std::string _path; std::string _name; }; // 日志等级 enum class log_level { Debug, // 测试 Info, // 普通信息 Warning, // 告警 Error, // 错误 Fatal // 致命 }; std::string level_to_string(log_level level) { switch (level) { case log_level::Debug: return \"Debug\"; case log_level::Info: return \"Info\"; case log_level::Warning: return \"Warning\"; case log_level::Error: return \"Error\"; case log_level::Fatal: return \"Fatal\"; default: return \"UnKnown\"; } } std::string get_cur_time() { time_t cur_time = time(nullptr); struct tm ret; localtime_r(&cur_time, &ret); char buffer[128]; snprintf(buffer, sizeof(buffer), \"%4d-%02d-%02d %02d:%02d:%02d\", ret.tm_year + 1900, ret.tm_mon + 1, ret.tm_mday, ret.tm_hour, ret.tm_min, ret.tm_sec); return buffer; } // 日志类 class logger { public: logger() { // 默认刷新策略 using_screen_strategy(); } ~logger() { } // 选择刷新策略接口 void using_screen_strategy() { _strategy = std::make_unique(); } void using_file_strategy() { _strategy = std::make_unique(); } // 内部日志类log_message->方便logger类创建一个临时log_message对象 // ->临时对象销毁时相对应文件中刷新内容 class log_message { private: std::string _cur_time; // 当前时间 log_level _level; // 日志等级 pid_t _pid; // 进程id std::string _file_name; // 对应的文件名 int _line_num; // 对应文件行号 std::string _log_info; // 日志信息 logger &_logger; // 拿到外部类方便采用策略刷新 // 这里加引用才行,不加引用的话,本质是要去构造一个logger对象,但是你的log_message在内部,并不能看到完整的logger声明,所有无法构造出完整的logger对象 public: // 构造函数 log_message(log_level level, std::string &file_name, int line_num, logger &log) : _cur_time(get_cur_time()), _level(level), _pid(getpid()), _file_name(file_name), _line_num(line_num), _logger(log) { // 构造信息左半部分 [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - std::stringstream ss; ss << \"[\" << _cur_time << \"] \" << \"[\" << level_to_string(_level) << \"] \" << \"[\" << _pid << \"] \" << \"[\" << _file_name << \"] \" << \"[\" << _line_num << \"] - \"; _log_info = ss.str(); } // 重载 <<,返回值必须为引用 template log_message &operator<<(const T &info) { std::stringstream ss; ss < 采用指定策略刷新 ~log_message() { if (_logger._strategy) { _logger._strategy->sync_log(_log_info); } } }; // 重载logger(),返回值故意写成拷贝构建临时对象 log_message operator()(log_level level, std::string file_name, int line_num) { return log_message(level, file_name, line_num, *this); // 传*this,就是传logger对象 } private: std::unique_ptr _strategy; }; // 定义全局的logger对象 logger log;#define LOG(level) log(level, __FILE__, __LINE__)#define using_screen_strategy() log.using_screen_strategy();#define using_file_strategy() log.using_file_strategy();};
3. 线程池
线程池:
⼀种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景:
• 需要大量的线程来完成任务,且完成任务的时间比较短。比如WEB服务器完成网页请求这样的任 务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象⼀个热门网站 的点击次数。但对于长时间的任务,比如⼀个Telnet连接请求,线程池的优点就不明显了。因为 Telnet会话时间比线程的创建时间大多了。
• 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
• 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
线程池的种类
a. 创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中 的任务接口
b. 浮动线程池,其他同上此处,我们选择固定线程个数的线程池。
此处,我们选择设计固定线程个数的线程池。
4. 单例模式
某些类,只应该具有⼀个对象(实例),就称之为单例.
在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中.此时往往要用⼀个单例的类来管理这些数据.
> 饿汉实现方式和懒汉实现方式
吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下⼀顿吃的时候可以立刻拿着碗就能吃饭. 吃完饭, 先把碗放下, 然后下⼀顿饭用到这个碗了再洗碗, 就是懒汉方式。
懒汉方式最核心的思想就是“延时加载”,这可以优化服务器的启动速度。我们其实早就见过“延时加载”机制了。Linux操作系统就是采用这种机制分配内存的,当我们使用malloc或new的时候,操作系统并没有立即为我们真正的分配内存空间【但是有虚拟地址】,而是在我们实际使用的时候才分配内存空间的。系统这样做可以让物理内存实实际际的被使用,而不会造成站着茅坑不拉屎的现象!!
但是具体到代码中,懒汉实现方式如何做到呢??
我们需要使用到静态成员和静态方法。静态成员和静态方法有一个特点:在程序加载到内存时,系统就已经为其分配好内存空间了【即静态变量已经完成定义和初始化】,即使我们没有定义初始化这个包含静态成员的对象。
所以,我们可以在类内定义一个指针,该指针的类型就是自己。我们在类外将该指针初始化为nullptr,然后,我们还要在类内提供public获取单例的方法,该方法也是静态的。方法内部会判断单例指针是否为空,为空则new出单例并返回该指针,不为空则说明该单例已近被创建直接返回指外部也可以获得单例指针。
template class Singleton{ static T *inst;public: static T *GetInstance() { if (inst == NULL) { inst = new T(); } return inst; }};
还有,就是为了确保该类型只有一个对象, 类中的构造和拷贝构造以及赋值重载都必须是私有的!!
5. 基于单例模式自定义线程池
线程池本身并不难设计,无非就是维护固定数量的线程,在访问临界资源时进行加锁,使用条件变量维护多线程的同步。下面就直接给代码了:
> pthread_pool.hpp
#pragma once#include \"log.hpp\"#include \"mutex.hpp\"#include \"cond.hpp\"#include \"pthread.hpp\"#include #include using namespace log_module;using namespace mutex_module;using namespace cond_module;using namespace pthread_module;static const int gnum = 5;namespace pthread_pool_module{ template class pthread_pool { private: void wake_up_all_pthread() { lock_guard lg(_mutex); if (_sleep_num <= 0) return; // 唤醒所有线程 _cond.broadcast(); LOG(log_level::Info) << \"唤醒所有休眠的线程\"; } void wake_up_one() { if (_sleep_num == 0) return; _cond.signal(); } // 线程池启动也私有,一旦单例创建成功就启动线程池 void start() { if (_isrunning == true) return; _isrunning = true; for (auto &e : _pthreads) { e.start(); LOG(log_level::Info) << e.name() << \"线程创建成功\"; } } // 构造私有 pthread_pool(int num = gnum) : _num(num), _isrunning(false), _sleep_num(0) { for (int i = 0; i < num; i++) { _pthreads.emplace_back( [this]() { handler_task(); }); } } // 拷贝构造和赋值重载私有【保证线程池只有一个对象】 pthread_pool(const pthread_pool &) = delete; pthread_pool &operator=(const pthread_pool &) = delete; public: // 静态获取单例 static pthread_pool *get_instance() { // 单例已经生成就没有必要竞争锁了 if (_pt == nullptr) { LOG(log_level::Info) << \"尝试获取单例\"; // 单例首次生成可能被多线程并发执行,加锁保护 lock_guard lg(_lock); if (_pt == nullptr) { _pt = new pthread_pool(); LOG(log_level::Info) <start(); } } return _pt; } void join() { for (auto &e : _pthreads) { e.join(); LOG(log_level::Info) << e.name() << \"线程join成功\"; } } void stop() { if (_isrunning == false) return; _isrunning = false; wake_up_all_pthread(); } void handler_task() { char name[128]; pthread_getname_np(pthread_self(), name, sizeof(name)); while (true) { T t; { lock_guard lg(_mutex); // 线程休眠条件:1.任务队列为空 2.线程池运行 while (_taskq.empty() && _isrunning == true) { _sleep_num++; _cond.wait(_mutex); _sleep_num--; } // 线程池退出条件 1.任务队列为空 2.线程池退出 if (_isrunning == false && _taskq.empty()) { LOG(log_level::Info) << name << \"退出了,\" << \"线程池退出了\"; break; } t = _taskq.front(); _taskq.pop(); } t(); // 处理任务不需要加锁 } } // 入任务 bool equeue(const T &t) { if (!_isrunning) return false; lock_guard lg(_mutex); _taskq.push(t); if (_sleep_num == num) wake_up_one(); return true; } ~pthread_pool() {} private: std::vector _pthreads; // 维护多线程 int _num; // 线程个数 std::queue _taskq; // 任务队列 mutex _mutex; // 互斥锁 cond _cond; // 条件变量 bool _isrunning; // 线程池运行状态 int _sleep_num; // 休眠线程个数 static pthread_pool *_pt; // 静态生成类内指针 static mutex _lock; // 静态生成锁 }; // 初始化类内静态成员 template pthread_pool *pthread_pool::_pt = nullptr; template mutex pthread_pool::_lock;}
> main.cc
#include \"log.hpp\"#include \"pthread_pool.hpp\"#include \"task.hpp\"using namespace log_module;using namespace pthread_pool_module;// using task_t = function;int main(){ LOG(log_level::Debug) << \"for test\"; // 获取单例 pthread_pool *p = pthread_pool::get_instance(); pthread_pool *p2 = pthread_pool::get_instance(); // printf(\"%p\\n\",p); // printf(\"%p\\n\",p2); int cnt = 5; while (cnt--) { p->equeue(download); } p2->stop(); p2->join(); return 0;}