2

我实现了一个 Logger,所以它可以像 ostream 一样使用。例如,如果有人想写入日志 - 他可以这样做:

LOG << "hello world " << 6 << 8.6 << "\n";

日志将被写入屏幕、日志文件以及用户喜欢的任何其他来源(这不是问题)。

为了实现这个目标,我为 LOG 创建了一个宏:

#define LOG Logger::GetInstance()(__FILENAME__, __func__, __LINE__)

和重载的运算符()和运算符<<:

template <typename T>
inline Logger& operator << (const T& msg) {
    std::stringstream ss;
    ss << msg;
    PrintToFile(ss.str());
    PrintToScreen(ss.str());
    return *this;
}

Logger& Logger::operator () (const std::string& sourceFile, const std::string& funcName, int lineNumber) {
    std::stringstream ss;
    ss << Utilities::GetFormattedTime("%d.%m.%y %H:%M:%S") << "::" << sourceFile << "(" << lineNumber << ")::" <<
            funcName << "::";
    PrintToFile(level, ss.str());
    PrintToScreen(level, ss.str());
    return *this;
}

问题是当我有多个线程在我的进程上运行时,打印可能会被剪切,因为上面示例行中间的线程之间的上下文切换(LOG << "hello world...")

使用互斥锁可能无济于事,因为 operator << 和 operator () 不是同一个函数。

这个问题有什么绝妙或简单的解决方案吗?

4

5 回答 5

2

就在我的头顶。如果您想保持使用流 io 运算符的方法不变,您可以使用某种代理对象来锁定\解锁互斥锁。

请不要提请注意编码风格(尤其是 swfull 和危险的 Logger 实现)。您可以在下面找到上述想法的简要说明。

template<class TLogger, class TLockObject>
class LoggerProxy
{
public:
    LoggerProxy(TLogger &logger, TLockObject &lockObj)
        : m_logger(logger),
        m_lockGuard(lockObj)
    {
    }

    template <typename T>
    inline LoggerProxy& operator << (const T& msg)
    {
        m_logger.Write(msg);
        return *this;
    }

private:
    TLogger & m_logger;
    std::lock_guard<typename TLockObject> m_lockGuard;
};

//Purpose of code below is just an illustration of usage LoggerProxy class. Don't use it in production code.
class Logger
{
public:
    static Logger& GetInstance()
    {
        static Logger instance;
        return instance;
    }
    static std::mutex& GetLockObject()
    {
        static std::mutex lockObj;
        return lockObj;
    }

    template <typename T>
    inline void Write (const T& msg) {
        std::cout << msg << std::endl;
    }
};

#define LOG LoggerProxy<Logger, std::mutex>(Logger::GetInstance(), Logger::GetLockObject())

int main()
{

    LOG << 10 << "HELLO" << 1.1;
    LOG << 101 << "HELLO2" << 11.1;

    return 0;
}
于 2018-02-19T16:21:44.690 回答
1

少分享,少锁。

如果您要在每个上进行同步,operator<<那么当您的应用程序在启用日志记录的情况下编译时,您将获得非常糟糕的性能。

这是我如何做的草图:

这个概念

namespace Logging {

    struct SinkConcept { void commit(std::string const&); }; // documentation only

让我们设计一个狭窄的合约,其中任何日志接收器都只有一个方法。

现在,让我们创建一个LogTx- 代表一个日志事务。

LogTx应该是一个移动感知临时对象在本地创建一条日志消息。这意味着缓冲区不共享,并且在您提交之前不需要同步。

提交是从析构函数完成的:

// movable log transaction (using arbitrary sink)
template <typename Sink> struct LogTx {
    LogTx(Sink& s) : _sink(s) {}
    LogTx(LogTx&&) = default;

    unique_flag        _armed;
    std::ostringstream _oss;
    Sink& _sink;

    ~LogTx() { if (_armed) _sink.commit(_oss.str()); }

    template <typename T> LogTx& operator<<(T&& rhs)&  { return (_oss << rhs), *this; }
    template <typename T> LogTx  operator<<(T&& rhs)&& { return (_oss << rhs), std::move(*this); }
};

就这样。该_armed标志确保析构函数不会在移出的实例中提交。

一些样品槽

现在,让我们添加简单的接收器,以便我们进行演示。让我们从最简单的开始:

struct NullSink { void commit(std::string const&) const {} };

现在,让我们变得更有用。将日志事务提交到任何ostream对象或引用的接收器:

template <typename Impl, bool Flush = true>
struct StreamSink {
    StreamSink(Impl stream_or_ref = {}) : _sor(std::move(stream_or_ref)) {}
    StreamSink(StreamSink&& rhs) : StreamSink(std::move(rhs._sor)) {}

    void commit(std::string const& msg) {
        std::lock_guard<std::mutex> lk(_mx);
        get() << msg << "\n";
        if (Flush) get() << std::flush;
    }

    std::ostream& get() { return _sor; }
  private:
    mutable std::mutex _mx;
    Impl _sor; // stream convertible to ostream&
};

而且,因为您在示例中写入了多个目的地:

template <typename A, typename B> struct TeeSink { // dispatch to two sinks
    A a; B b;
    void commit(std::string const& msg) { a.commit(msg); b.commit(msg); }
};

一些便利功能

除非您使用 C++17,否则欢迎使用一些工厂函数。

// factory functions (mostly not needed in c++17 with deduction guides)
template <typename A, typename B> 
TeeSink<A, B> tee(A&& a, B&& b) { return { std::forward<A>(a), std::forward<B>(b) }; }

StreamSink<std::ofstream, false> log_to(std::ofstream&& file) { return {std::move(file)}; }
StreamSink<std::reference_wrapper<std::ostream>, true> log_to(std::ostream& os) { return {os}; }

让我们还为标准流添加接收器的全局实例,以便您可以使用它们在任何地方获得相同的同步:

auto& log_to_stderr() {
    static StreamSink<std::reference_wrapper<std::ostream>, true> s_instance { log_to(std::cerr) };
    return s_instance;
}
auto& log_to_stdout() {
    static StreamSink<std::reference_wrapper<std::ostream>, true> s_instance { log_to(std::cout) };
    return s_instance;
}
auto& null_sink() {
    static NullSink const s_instance{};
    return s_instance;
}

template <typename Sink>
LogTx<Sink> make_tx(Sink& sink) { return {sink}; }

最后是阻力:为给定的水槽makeTx创建一个:LogTx

template <typename Sink>
LogTx<Sink> make_tx(Sink& sink) { return {sink}; }

演示时间

现在我们可以把它放在一起:

#define LOG_TO(sink) (Logging::make_tx(sink) << __FILE__ << ":" << __LINE__ << "\t" << __func__ << "\t")
#ifdef NOLOGGING
    #define LOG LOG_TO(Logging::null_sink())
#else
    static auto _file_sink = Logging::log_to(std::ofstream("demo.log"));
    static auto _both_sink = tee(_file_sink, Logging::log_to_stderr());
    #define LOG LOG_TO(_both_sink)
#endif

这几乎是您想要的:

Live On Coliru

#include <thread>
void worker(std::string id) {
    while (auto r = rand()%10) {
        std::this_thread::sleep_for(std::chrono::milliseconds(r));
        LOG << "Ping from " << id;
    }
}

int main() {
    LOG << "Hello";
    {
        std::thread a(worker, "A"), b(worker, "B");
        a.join();
        b.join();
    }
    LOG << "Bye";
}

打印到 stderr 和demo.log

main.cpp:104    main    Hello
main.cpp:99 worker  Ping from A
main.cpp:99 worker  Ping from B
main.cpp:99 worker  Ping from A
main.cpp:99 worker  Ping from B
main.cpp:99 worker  Ping from A
main.cpp:99 worker  Ping from B
main.cpp:99 worker  Ping from B
main.cpp:99 worker  Ping from A
main.cpp:99 worker  Ping from A
main.cpp:99 worker  Ping from A
main.cpp:99 worker  Ping from B
main.cpp:99 worker  Ping from A
main.cpp:99 worker  Ping from A
main.cpp:99 worker  Ping from A
main.cpp:99 worker  Ping from A
main.cpp:110    main    Bye

C++11 完整列表

添加了一个与 c++11 兼容的版本,其中包含完整列表以防止链接腐烂:

[C++11 Live On Coliru][http://coliru.stacked-crooked.com/a/6360aad26b037df2)

#include <functional> // for std::reference_wrapper
#include <iostream>
#include <sstream>
#include <fstream>
#include <mutex>

namespace Logging {

    // utility to safely implement movable log transactions
    struct unique_flag {
        bool value = true;
        unique_flag() = default;
        unique_flag(unique_flag&& rhs) : value(rhs.value) { rhs.value = false; }
        operator bool() const { return value; }
    };

    struct SinkConcept { void commit(std::string const&); }; // documentation only

    // movable log transaction (using arbitrary sink)
    template <typename Sink> struct LogTx {
        LogTx(Sink& s) : _sink(s) {}
        LogTx(LogTx&&) = default;

        unique_flag        _armed;
        std::ostringstream _oss;
        Sink& _sink;

        ~LogTx() { if (_armed) _sink.commit(_oss.str()); }

        template <typename T> LogTx& operator<<(T&& rhs)&  { return (_oss << rhs), *this; }
        template <typename T> LogTx  operator<<(T&& rhs)&& { return (_oss << rhs), std::move(*this); }
    };

    // Some sink models
    struct NullSink { void commit(std::string const&) const {} };

    template <typename Impl, bool Flush = true>
    struct StreamSink {
        StreamSink(Impl stream_or_ref = {}) : _sor(std::move(stream_or_ref)) {}
        StreamSink(StreamSink&& rhs) : StreamSink(std::move(rhs._sor)) {}

        void commit(std::string const& msg) {
            std::lock_guard<std::mutex> lk(_mx);
            get() << std::move(msg);
            if (Flush) 
                get() << std::endl;
            else
                get() << "\n";
        }

        std::ostream& get() { return _sor; }
      private:
        mutable std::mutex _mx;
        Impl _sor; // stream convertible to ostream&
    };

    template <typename A, typename B> struct TeeSink { // dispatch to two sinks
        A a; B b;
        void commit(std::string const& msg) { a.commit(msg); b.commit(msg); }
    };

    // factory functions (mostly not needed in c++17 with deduction guides)
    template <typename A, typename B> 
    TeeSink<A, B> tee(A&& a, B&& b) { return { std::forward<A>(a), std::forward<B>(b) }; }

    StreamSink<std::ofstream, false> log_to(std::ofstream&& file) { return {std::move(file)}; }
    StreamSink<std::reference_wrapper<std::ostream>, true> log_to(std::ostream& os) { return {os}; }

    StreamSink<std::reference_wrapper<std::ostream>, true>& log_to_stderr() {
        static StreamSink<std::reference_wrapper<std::ostream>, true> s_instance { log_to(std::cerr) };
        return s_instance;
    }
    StreamSink<std::reference_wrapper<std::ostream>, true>& log_to_stdout() {
        static StreamSink<std::reference_wrapper<std::ostream>, true> s_instance { log_to(std::cout) };
        return s_instance;
    }
    NullSink const& null_sink() {
        static NullSink const s_instance{};
        return s_instance;
    }

    template <typename Sink>
    LogTx<Sink> make_tx(Sink& sink) { return {sink}; }
}

#define LOG_TO(sink) (Logging::make_tx(sink) << __FILE__ << ":" << __LINE__ << "\t" << __func__ << "\t")
#ifdef NOLOGGING
    #define LOG      LOG_TO(Logging::null_sink())
#else
    static auto _file_sink = Logging::log_to(std::ofstream("demo.log"));
    static auto _both_sink = tee(_file_sink, Logging::log_to_stderr());
    #define LOG      LOG_TO(_both_sink)
#endif

#include <thread>
void worker(std::string id) {
    while (auto r = rand()%10) {
        std::this_thread::sleep_for(std::chrono::milliseconds(r));
        LOG << "Ping from " << id;
    }
}

int main() {
    LOG << "Hello";
    {
        std::thread a(worker, "A"), b(worker, "B");
        a.join();
        b.join();
    }
    LOG << "Bye";
}
于 2018-02-20T00:19:31.520 回答
1

您是对的,operator<<并且operator ()是两个不同的功能,但这并不意味着您不能使用互斥锁。
从我可以从你的例子中收集到的内容operator<<operator()你最后调用了相同的两个函数PrintToFilePrintToScreen. 这让我认为所有线程共有的流在这两个函数中,而不是在你的operator<<andoperator()中,所以你实际上可以在这些函数中锁定一个互斥锁,并对记录器进行线程安全访问。话虽这么说,仍然只需要决定您是否需要两个互斥锁或只需要一个,这取决于您是否希望日志记录作为操作是“原子的”,或者您想要拆分它。
在“原子”版本中,您应该在写入屏幕和文件时使用包含单个互斥锁的单个版本,因此您的日志将按顺序执行。
相反,如果您希望拆分版本具有两个不同的函数和两个不同的互斥锁,一个用于文件日志记录,一个用于屏幕登录,这个线程想要写入文件不必等待线程已经写入文件,现在正在写入屏幕。一如既往地记住,拥有两个互斥锁是有代价的。

于 2018-02-19T15:25:14.837 回答
0

第一感觉是,那operator()operator<<不相关。为此,最好使用两个互斥锁,一个 inPrintToFile和一个 in PrintToScreen

但是您也可以使用相同的互斥锁来记录您的两个运算符方法。由于第一个操作符是一个模板函数,我们没有两个而是 n 个函数。每个模板实例都是一个附加的。因此,对所有功能使用单个互斥锁正是您想要的。

如果您的 Logger 是一个类并且操作符是成员,您只需将 mutex 设置为可以在您的每个方法中(解锁)锁定的成员变量。

于 2018-02-19T15:40:38.993 回答
-1

使用 astd::mutex并锁定第一次使用operator()or operator<<。如果用户字符串以 . 结尾,则解锁\n。我假设您确定用户使用换行符完成每个日志条目。

class Logger {
     std::mutex mux;
     //...
   public:
     Logger& operator()(/*...*/) {
       mux.lock();
       // write to log
     }
     Logger& operator<<(const string& str) {
       // write to log
       if(str[str.size()-1] == '\n')
         mux.unlock();
     }
     //...
   }; 
于 2018-02-19T15:25:01.920 回答