C++11实战——多线程的日志类
C++11实战——多线程的日志类
C++标准库的std::cout和std::ofstream重载了operator<<,单线程使用非常简单。但由于其并非线程安全,在多线程中使用则需要自己加锁同步,很是繁琐。
形如“int printf ( const char * format, ... );”的 传统C函数,虽然线程安全但使用上比 operator<< 麻烦的多。
本文将利用一些 C++11新特性 实现一个线程安全的日志类(兼顾写文件日志和打印控制台日志),并力求代码压缩在200行之内。
源码下载:
github: github.com/FutaAlice/cpp11logger
csdn: download.csdn.net/download/u014755412/10252806
接口设计
外部接口需要尽可能简单,我们先制定一个理想接口,然后完成其内部实现:
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<void> 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 ) // linux
int 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 <string>
#include <fstream>
#include <sstream>
#include <mutex>
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::ostringstream
class 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 <cassert>
#include <chrono>
#include <ctime>
#include <iostream>
#include <iomanip>
#include <map>
#include <regex>
#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)
#endif
static const map<Level, const char *> 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 <thread>
#include <list>
#include "logger.h"
using namespace std;
using namespace logger;
int main()
{
list<shared_ptr<thread>> 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<thread>(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<thread>(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: github.com/FutaAlice/cpp11logger
csdn: download.csdn.net/download/u014755412/10252806