【问题标题】:Better way to make following logger implementation thread safe?使以下记录器实现线程安全的更好方法?
【发布时间】:2019-08-08 04:30:11
【问题描述】:

我想写一个带有cout 接口的基本线程安全记录器。我想出了以下课程设计。这绝对不是最好的设计,因为如果使用不当,它可能会陷入死锁,如int main() 所示。

#include <iostream>
#include <sstream>  // for string streams 
#include <mutex>
#include <memory>

typedef std::ostream&(*endl)(std::ostream&);

class BasicLogger {

public:
  enum SEVERITY {
    CRITICAL,
    ERROR,
    WARNING
  };

  explicit BasicLogger(SEVERITY _s): s(_s) {
    streamMutex.lock();
    logStream.reset(new std::ostringstream);
  }

  ~BasicLogger() {
    std::cout << logStream->str();
    streamMutex.unlock();
  }

  std::ostringstream& operator<< (const endl eof) {
    (*logStream) << eof;
    return (*logStream);
  }

  template<typename T>
  std::ostringstream& operator<< (const T& obj) {
    (*logStream) << obj;
    return (*logStream);
  }

  static std::unique_ptr<std::ostringstream> logStream;

  BasicLogger(const BasicLogger&) = delete;
  BasicLogger& operator=(const BasicLogger&) = delete;

private:
  SEVERITY s;          //TODO
  static std::mutex streamMutex;

};

/*=======================================================*/
std::unique_ptr<std::ostringstream> BasicLogger::logStream;
std::mutex BasicLogger::streamMutex;
/*=======================================================*/

int main() {

  int a = 9;
  int b = 8;

  // BasicLogger l(BasicLogger::ERROR); //Deadlock situation

  BasicLogger(BasicLogger::ERROR) << "Linux" << " " << a << " " << b << std::endl;
  BasicLogger(BasicLogger::ERROR) << "MyMachine";
  BasicLogger(BasicLogger::ERROR) << std::endl;

}

【问题讨论】:

  • 最好将日志消息的产生和实际的日志记录与从线程安全的生产者/消费者队列馈送的后台线程解耦。
  • @πάνταῥεῖ 实际上很有趣。我一定会玩弄它的。谢谢。

标签: c++ multithreading c++11 logging


【解决方案1】:

您将在构造函数中锁定互斥体并在析构函数中解锁它。 因此,不能同时创建多个BasicLogger 实例。

BasicLogger l(BasicLogger::ERROR); 将调用构造函数,从而获得锁。在l 超出范围之前不会调用析构函数,这意味着互斥锁保持锁定状态直到l 超出范围。

如果您尝试构造另一个BasicLocker,您的构造函数尝试获取直到l 被销毁才可用的锁会导致死锁。

当您创建临时BasicLogger 实例BasicLogger(BasicLogger::ERROR) 时,将调用构造函数,使用对象然后立即销毁。因此,被锁定的互斥体被解锁。


由于您为每个BasicLogger 实例创建独立的std::stringstream,因此您需要一个锁来保护对std::stringstream 的访问,以便多个线程可以写入同一个记录器。因此,每个实例都应该拥有一个互斥锁。

您还需要一个静态互斥锁来保护同时访问std::cout。在打印日志并立即释放时获得锁。当然,这要求对std::cout的所有访问都必须通过BasicLogger进行。

class BasicLogger {
public:
    BasicLogger() = default;
    ~BasicLogger() {
        std::lock_guard<std::mutex> lLock(localMutex); /* the order of locking is important */
        std::lock_guard<std::mutex> gLock(globalMutex);
        std::cout << stream.str();
    }

    /* TODO: satisfying the rule of 5 */

    template <class T>
    BasicLogger& operator<< (const T& item) {
        std::lock_guard<std::mutex> lLock(localMutex);
        stream << item;
        return *this;
    }

private:
    std::ostringstream stream;
    std::mutex localMutex;
    static std::mutex globalMutex;
};

【讨论】:

  • 我知道这一点。对不起,如果不清楚,但我想知道是否有更好的设计选择可以避免这个问题。正确使用不会出现此问题,但我不能依赖用户始终正确使用界面。
  • 我添加了一种方法来解决您的问题。
  • 啊。这看起来很有希望。只是一个问题,localMutex 真的有必要吗?多个线程写入同一个 ostringstream 即使在 localMutex 之后也不是线程安全的。
  • localMutex 不允许多个线程同时写入。如果一个写入使用ostringstream,其他线程将不得不等待。
  • 如果您没有注意到,streamlocalMutex 是非静态成员。
【解决方案2】:

我只考虑一个operator&lt;&lt;,并且只在该成员函数中锁定互斥锁。所以只有在你要写的时候才锁住。

而不是一个静态变量(这与一个全局变量基本相同,所以你不能有多个记录器)来保存std::ostringstream,有一个成员变量来保存std::ostream&amp;。这意味着通过多个BasicLoggers 写入多个东西会使它们看起来混合在一起,但这已经是多个线程通过同一个BasicLogger 写入的问题。

要解决如下所示的问题:

BasicLogger l;

// Thread 1:
l << 1 << 2;

// Thread 2:
l << 3 << 4;

// Output is one of:
1234
1324
1342
3124
3142
3412
// Ideally it should only be
1234
3412
// (Pretend `1` is something like "x is: " and `3` is "y is: ")
// (You wouldn't want "x is: {y}" or "x is: y is: {x} {y}")

你可以有一个函数写很多东西然后锁定,接受可变参数。 (在我的例子中写成BasicLogger::write

看起来像这样:

#include <iostream>
#include <utility>
#include <mutex>
#include <thread>

class BasicLogger {
public:
    enum SEVERITY {
        CRITICAL,
        ERROR,
        WARNING
    };

    // Consider logging to std::cerr by default instead
    explicit BasicLogger(SEVERITY s = BasicLogger::ERROR, std::ostream& out = std::cout)
        : severity(s), output(&out) {}

    explicit BasicLogger(std::ostream& out = std::cout)
        : severity(BasicLogger::ERROR), output(&out) {}

    BasicLogger(const BasicLogger&) = default;

    template<typename T>
    BasicLogger& operator<<(T&& obj) {
        std::lock_guard<std::mutex> lock(stream_mutex);
        (*output) << std::forward<T>(obj);
        return *this;
    }

    template<typename... T>
    void write(T&&... obj) {
        std::lock_guard<std::mutex> lock(stream_mutex);
        ((*output) << ... << std::forward<T>(obj));
    }

    std::ostream& get_output() noexcept {
        return *output;
    }
    const std::ostream& get_output() const noexcept {
        return *output;
    }

    BasicLogger& operator=(const BasicLogger&) = default;

    SEVERITY severity;
private:
    std::ostream* output;
    static std::mutex stream_mutex;
};

std::mutex BasicLogger::stream_mutex;


int main() {
    BasicLogger l(std::cerr);
    int x = 0, y = 1;

    std::thread t1([&]() {
        l.write("x is: ", x, '\n');
    });
    std::thread t2([&]() {
        l.write("y is: ", y, '\n');
    });
    t1.join();
    t2.join();
}

或者你甚至可以有一个operator&lt;&lt;(std::tuple&lt;T...&gt;),而不是l.write(...)l &lt;&lt; std::tie(...)

但是请注意这个和你的班级之间的区别。你的类只会写一次,使用空间来拥有一个临时的ostringstream,而这会直接多次写入所需的ostream

【讨论】:

    猜你喜欢
    • 2020-12-29
    • 1970-01-01
    • 1970-01-01
    • 2010-12-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-23
    • 2018-06-03
    相关资源
    最近更新 更多