使以下记录器实现线程安全的更好方法?

时间:2019-03-17 18:42:17

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

我想用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;

}

2 个答案:

答案 0 :(得分:2)

您正在将互斥锁锁定在构造函数中,并在析构函数中对其进行解锁。 因此,不可能同时创建多个BasicLogger实例。

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

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

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


由于您要为每个std::stringstream实例创建独立的BasicLogger,因此您需要一个锁来保护对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;
};

答案 1 :(得分:1)

我只考虑一个operator<<,并且只考虑锁定互斥体的那个成员函数。因此,只有在要写时才握住锁。

可以使用一个std::ostringstream而不是一个静态变量(与全局变量基本相同,因此您不能有多个记录器)来保存一个std::ostream&。这意味着通过多个BasicLogger编写多个事物会使它们看起来混合在一起,但这已经是多个线程通过同一个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<<(std::tuple<T...>),而不是l.write(...)l << std::tie(...)

但是请注意本课程与您的课程之间的区别。您的班级只写一次,使用空格来拥有一个临时ostringstream,而直接多次写入所需的ostream