C++標準庫的std::cout和std::ofstream重載了operator<<,單線程使用很是簡單。但因爲其並不是線程安全,在多線程中使用則須要本身加鎖同步,非常繁瑣。
形如「int printf ( const char * format, ... );」的 傳統C函數,雖然線程安全但使用上比 operator<< 麻煩的多。
本文將利用一些 C++11新特性 實現一個線程安全的日誌類(兼顧寫文件日誌和打印控制檯日誌),併力求代碼壓縮在200行以內。linux
源碼下載:
github: github.com/FutaAlice/cpp11logger
csdn: download.csdn.net/download/u014755412/10252806ios
外部接口須要儘量簡單,咱們先制定一個理想接口,而後完成其內部實現:git
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; }
若是使用形如 debug(const char *fmt, ...) 的不定參數,咱們能夠將鎖放在函數首尾,保證其線程安全:github
void debug(const char *fmt, ...) { static std::mutex m; m.lock(); // do something. m.unlock(); }
設計接口考慮到調用的方便,採用了 operator<< 的方式,而非形如 (const char *fmt, ...) 的不定參數。
也就是說,調用中每出現一次 << ,operator<< 就會被調用一次。express
ConsoleLogger debug; debug() << "Main thread " << 0; // operator<< 共調用2次
而對於一行中出現屢次 <<符號 的調用而言,若是在 operator<< 重載函數的首尾加鎖,兩次 operator<< 之間依然會混入其餘線程的日誌信息。windows
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<< 接收文本信息並暫時保存,在其析構函數中:"日誌類對象加鎖、寫日誌、解鎖" 順序進行。多線程
以此在保證調用接口簡單的同時實現線程安全。app
C++標準庫的 ostringstream 是一個理想的緩衝區,它完整實現了 operator<< 。
咱們只須要派生一個類,爲其重寫析構函數便可。async
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) 函數不是線程安全的。
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