同步写入以登录多线程进程

时间:2018-02-19 15:04:31

标签: c++ multithreading logging boost boost-mutex

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

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

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

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

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

并重载operator()和operator&lt;&lt;:

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&lt;&lt;“hello world ......“)

使用互斥锁可能无济于事,因为运营商&lt;&lt;和operator()不是同一个函数。

这个问题有没有明智或简单的解决方案?

5 个答案:

答案 0 :(得分: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;
}

答案 1 :(得分:1)

你是对的,operator<<operator ()是两个不同的功能,但这并不代表你不能使用互斥锁。
根据我在operator<<operator()中的示例中收集的内容,您最后会调用相同的两个函数PrintToFilePrintToScreen。 这让我觉得所有线程共有的流都在这两个函数中,而不是在operator<<operator()中,所以你可以在这些函数中实际锁定一个互斥锁并让线程安全访问记录器。 话虽如此,它仍然只是决定你是否需要两个互斥量或只有一个,这取决于你是否希望日志记录是&#34; atomic&#34;作为操作或你想拆分它。
在&#34;原子&#34;版本,你应该使用一个版本,在写入屏幕和文件时保存一个互斥锁,因此你的日志将连续执行。
相反,如果你想要分割版本,你想要两个不同的函数和两个不同的互斥体,一个用于文件记录,一个用于屏幕登录,这个想要写入文件的线程不必等待已经写入文件并正在写入屏幕的线程。一如既往地记住,有两个互斥量需要花费。

答案 2 :(得分: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}; }

最后 piècederesistance makeTx为给定的水槽创建LogTx

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

DEMO TIME

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

#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";
}

答案 3 :(得分:0)

第一种感觉是operator()operator<<无关。为此,最好使用两个互斥锁,一个位于PrintToFile,另一个位于PrintToScreen

但您也可以使用相同的互斥锁来记录两种运算符方法。由于第一个运算符是模板函数,因此我们没有两个函数。每个模板实例都是另一个。因此,对所有函数使用单个互斥锁就可以完全满足您的需求。

如果您的Logger是一个类并且运算符是成员,那么您只需将您的互斥锁变为一个成员变量,该变量可以在每个方法中被(取消)锁定。

答案 4 :(得分:-1)

使用std::mutex并锁定首次使用operator()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();
     }
     //...
   };