博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C++11实战——多线程的日志类
阅读量:4337 次
发布时间:2019-06-07

本文共 8203 字,大约阅读时间需要 27 分钟。

C++11实战——多线程的日志类

C++标准库的std::coutstd::ofstream重载了operator<<,单线程使用非常简单。但由于其并非线程安全,在多线程中使用则需要自己加锁同步,很是繁琐。

形如“int printf ( const char * format, ... );”的 传统C函数,虽然线程安全但使用上比 operator<< 麻烦的多。
本文将利用一些 C++11新特性 实现一个线程安全的日志类(兼顾写文件日志和打印控制台日志),并力求代码压缩在200行之内。

源码下载:

github:
csdn:


接口设计

外部接口需要尽可能简单,我们先制定一个理想接口,然后完成其内部实现:

using namespace logger;int main(){    // 控制台日志    ConsoleLogger debug;    // 控制台输出,默认的安全等级(Debug)    // output: [2017-02-29 00:00:00][Debug] Main thread, Message 0    debug() << "Main thread, Message " << 0;    // 控制台输出,安全等级为 Warning    // output: [2017-02-29 00:00:00][Warning] Main thread, Message 1    debug(Level::Warning) << "Main thread, Message " << 1;        // 文件日志输出位置    FileLogger fl("message.log");    // 写文件日志,安全等级为 Info    // output: [2017-02-29 00:00:00][Info] Main thread, Message 2    fl(Level::Info) << "Main thread, Message num: " << 2;}

几个重点问题的解决方式

1) 在哪里加锁?

如果使用形如 debug(const char *fmt, ...) 的不定参数,我们可以将锁放在函数首尾,保证其线程安全:

void debug(const char *fmt, ...){    static std::mutex m;    m.lock();    // do something.    m.unlock();}

设计接口考虑到调用的方便,采用了 operator<< 的方式,而非形如 (const char *fmt, ...) 的不定参数。

也就是说,调用中每出现一次 <<operator<< 就会被调用一次。

ConsoleLogger debug;debug() << "Main thread " << 0; // operator<< 共调用2次

而对于一行中出现多次 <<符号 的调用而言,如果在 operator<< 重载函数的首尾加锁,两次 operator<< 之间依然会混入其他线程的日志信息。

ConsoleLogger debug;future
f(async([]{ debug() << "_fuck_"; }));f.get();debug() << "Main thread " << 0;// 可能输出结果 1:Main thread 0_fuck_// 可能输出结果 2:Main thread _fuck_0// 可能输出结果 3:_fuck_Main thread 0

那么保证线程安全锁放在那里?(这里比较绕,看代码比较清楚)

我们先重载日志类(ConsoleLogger)的 operator() ,使其返回一个文本缓冲区临时对象

即,debug() 返回一个缓冲区,它是临时的,没有左值或右值引用接收它在行末分号处被销毁
缓冲区重载 operator<< 接收文本信息并暂时保存,在其析构函数中:"日志类对象加锁、写日志、解锁" 顺序进行。

以此在保证调用接口简单的同时实现线程安全。

2) 文本缓冲区有现成的轮子么?

C++标准库的 ostringstream 是一个理想的缓冲区,它完整实现了 operator<<

我们只需要派生一个类,为其重写析构函数即可。

3) 获取日期时间有什么简单的方法?

C++11新增的 chrono 库,配合 localtime_r 函数(windows下为 localtime_s, 只有参数顺序不同)

int localtime_r ( time_t  *t,struct tm *tm ) // linuxint localtime_s ( struct tm *tm, time_t  *t ) // windows

注意通用的 struct tm *localtime(const time_t *clock) 函数不是线程安全的。

4) 如何防止用于指定日志等级的枚举(enum Level)污染命名空间?

C/C++ 中具名(有名字)的 enum 类型的名字,以及 enum 的成员的名字都是全局可见的。

如果在相同的代码域中的两个枚举类型具有相同名字的枚举成员,这会导致命名冲突。
针对这些缺点,C++11引入了一种新的枚举类型,即“枚举类”,又称“强类型枚举”(strong-typed enum)。

namespace testenum{// 传统的枚举enum Fuck { f1, f2, f3 };// C++11 强类型枚举enum class Shit { s1, s2, s3 };}int main(){    using namespace testenum;    auto a = f1; // 通过,命名空间被 "enum Fuck" 成员污染    auto b = s1; // 编译报错,未定义标识符 s1,"enum class Shit" 不会污染命名空间    auto c = Shit::s1; // 通过    int A = a; // 通过    int C = c; // 编译报错,不允许隐式转换为整型}

如上代码所示,强类型枚举不会污染 namespace 并且 不会隐式转换为整形。

日志类及测试工程实现代码

目录结构

-- logger

-- -- logger.h
-- -- logger.cpp
-- -- logger_test.cpp

源代码

logger.h

// logger.h#pragma once#include 
#include
#include
#include
struct tm;namespace logger{// 强类型枚举,指定日志等级enum class Level { Debug, Info, Warning, Error, Fatal };class FileLogger; // 写文档用的日志类class ConsoleLogger; // 控制台输出用的日志类class BaseLogger; // 纯虚基类class BaseLogger{ class LogStream; // 用于文本缓冲的内部类声明public: BaseLogger() = default; virtual ~BaseLogger() = default; // 重载 operator() 返回缓冲区对象 virtual LogStream operator()(Level nLevel = Level::Debug);private: const tm* getLocalTime(); // 供缓冲区对象析构时调用(函数加锁保证线程安全) void endline(Level nLevel, std::string&& oMessage); // 纯虚函数,预留接口,由派生类实现 virtual void output(const tm *p_tm, const char *str_level, const char *str_message) = 0;private: std::mutex _lock; tm _localTime;};// 用于文本缓冲区的类,继承 std::ostringstreamclass BaseLogger::LogStream : public std::ostringstream{ BaseLogger& m_oLogger; Level m_nLevel;public: LogStream(BaseLogger& oLogger, Level nLevel) : m_oLogger(oLogger), m_nLevel(nLevel) {}; LogStream(const LogStream& ls) : m_oLogger(ls.m_oLogger), m_nLevel(ls.m_nLevel) {}; ~LogStream() // 为其重写析构函数,在析构时打日志 { m_oLogger.endline(m_nLevel, std::move(str())); }};// 控制台输出用的日志类class ConsoleLogger : public BaseLogger{ using BaseLogger::BaseLogger; virtual void output(const tm *p_tm, const char *str_level, const char *str_message);};// 写文档用的日志类class FileLogger : public BaseLogger{public: FileLogger(std::string filename) noexcept; FileLogger(const FileLogger&) = delete; FileLogger(FileLogger&&) = delete; virtual ~FileLogger();private: virtual void output(const tm *p_tm, const char *str_level, const char *str_message);private: std::ofstream _file;};extern ConsoleLogger debug;extern FileLogger record;} // namespace logger

logger.cpp

// logger.cpp#include 
#include
#include
#include
#include
#include
#include
#include "logger.h"using namespace std;using namespace logger;ConsoleLogger logger::debug;FileLogger logger::record("build_at_" __DATE__ "_" __TIME__ ".log");#ifdef WIN32#define localtime_r(_Time, _Tm) localtime_s(_Tm, _Time)#endifstatic const map
LevelStr ={ { Level::Debug, "Debug" }, { Level::Info, "Info" }, { Level::Warning, "Warning" }, { Level::Error, "Error" }, { Level::Fatal, "Fatal" },};ostream& operator<< (ostream& stream, const tm* tm){ return stream << 1900 + tm->tm_year << '-' << setfill('0') << setw(2) << tm->tm_mon + 1 << '-' << setfill('0') << setw(2) << tm->tm_mday << ' ' << setfill('0') << setw(2) << tm->tm_hour << ':' << setfill('0') << setw(2) << tm->tm_min << ':' << setfill('0') << setw(2) << tm->tm_sec;}BaseLogger::LogStream BaseLogger::operator()(Level nLevel){ return LogStream(*this, nLevel);}const tm* BaseLogger::getLocalTime(){ auto now = chrono::system_clock::now(); auto in_time_t = chrono::system_clock::to_time_t(now); localtime_r(&in_time_t, &_localTime); return &_localTime;}void BaseLogger::endline(Level nLevel, string&& oMessage){ _lock.lock(); output(getLocalTime(), LevelStr.find(nLevel)->second, oMessage.c_str()); _lock.unlock();}void ConsoleLogger::output(const tm *p_tm, const char *str_level, const char *str_message){ cout << '[' << p_tm << ']' << '[' << str_level << "]" << "\t" << str_message << endl; cout.flush();}FileLogger::FileLogger(string filename) noexcept : BaseLogger(){ string valid_filename(filename.size(), '\0'); regex express("/|:| |>|<|\"|\\*|\\?|\\|"); regex_replace(valid_filename.begin(), filename.begin(), filename.end(), express, "_"); _file.open(valid_filename, fstream::out | fstream::app | fstream::ate); assert(!_file.fail());}FileLogger::~FileLogger(){ _file.flush(); _file.close();}void FileLogger::output(const tm *p_tm, const char *str_level, const char *str_message){ _file << '[' << p_tm << ']' << '[' << str_level << "]" << "\t" << str_message << endl; _file.flush();}

logger_test.cpp

// logger_test.cpp#include 
#include
#include "logger.h"using namespace std;using namespace logger;int main(){ list
> oThreads; ConsoleLogger ocl; ocl << "test" << "log"; FileLogger ofl("shit.log"); ofl << "test" << "log"; /* * 控制台输出 */ for (int i = 0; i < 10; i++) { oThreads.push_back(shared_ptr
(new thread([=]() { for (int j = 0; j < 100; ++j) debug() << "Thread " << i << ", Message " << j; }))); } for (int i = 0; i < 100; i++) debug() << "Main thread, Message " << i; for (auto oThread : oThreads) oThread->join(); debug(Level::Info) << "output to console, done."; debug(Level::Info) << "press any to continue this test."; getchar(); oThreads.clear(); /* * 日志文档输出 */ for (int i = 0; i < 10; i++) { oThreads.push_back(shared_ptr
(new thread([=]() { for (int j = 0; j < 100; ++j) record() << "Thread " << i << ", Message " << j; }))); } for (int i = 0; i < 100; i++) record() << "Main thread, Message " << i; for (auto oThread : oThreads) oThread->join(); debug(Level::Info) << "done."; getchar(); return 0;}

源码下载

github:

csdn:

转载于:https://www.cnblogs.com/FutaAlice/p/9041725.html

你可能感兴趣的文章
WebSocket & websockets
查看>>
openssl 升级
查看>>
ASP.NET MVC:通过 FileResult 向 浏览器 发送文件
查看>>
CVE-2010-2883Adobe Reader和Acrobat CoolType.dll栈缓冲区溢出漏洞分析
查看>>
使用正确的姿势跨域
查看>>
AccountManager教程
查看>>
Android学习笔记(十一)——从意图返回结果
查看>>
算法导论笔记(四)算法分析常用符号
查看>>
ultraedit激活
查看>>
总结(6)--- python基础知识点小结(细全)
查看>>
亿级曝光品牌视频的幕后设定
查看>>
ARPA
查看>>
JSP开发模式
查看>>
我的Android进阶之旅------&gt;Android嵌入图像InsetDrawable的使用方法
查看>>
Detours信息泄漏漏洞
查看>>
win32使用拖放文件
查看>>
Android 动态显示和隐藏软键盘
查看>>
raid5什么意思?怎样做raid5?raid5 几块硬盘?
查看>>
【转】how can i build fast
查看>>
null?对象?异常?到底应该如何返回错误信息
查看>>