C++11實戰——多線程的日誌類

C++11實戰——多線程的日誌類

C++標準庫的std::coutstd::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;
}

幾個重點問題的解決方式

1) 在哪裏加鎖?

若是使用形如 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

2) 文本緩衝區有現成的輪子麼?

C++標準庫的 ostringstream 是一個理想的緩衝區,它完整實現了 operator<<
咱們只須要派生一個類,爲其重寫析構函數便可。async

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

相關文章
相關標籤/搜索