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

问题描述 投票:2回答:5

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

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

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

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

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

并重载operator()和operator <<:

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()不是同一个函数。

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

c++ multithreading logging boost boost-mutex
5个回答
2
投票

就在我的头顶。如果您希望保持使用流io运算符的方法,可以使用锁定\ unlocks互斥锁的代理对象。

请不要注意编码风格(特别是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
投票

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


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

演示时间

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

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

0
投票

第一种感觉是,operator()operator<<是无关的。为此,使用两个互斥体感觉更好,一个在PrintToFile,一个在PrintToScreen

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

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


-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();
     }
     //...
   }; 
© www.soinside.com 2019 - 2024. All rights reserved.