[英]Better way to make following logger implementation thread safe?
我想用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;
}
您正在将互斥锁锁定在构造函数中,并在析构函数中对其进行解锁。 因此,不可能同时创建多个BasicLogger
实例。
BasicLogger l(BasicLogger::ERROR);
将调用构造函数,从而获得锁。 在l
超出范围之前,将不会调用析构函数,这意味着互斥体将保持锁定,直到l
超出范围。
如果您尝试构造另一个BasicLocker
,则构造函数尝试获取直到l
被销毁才可用的锁将导致死锁。
当您创建临时BasicLogger
实例BasicLogger(BasicLogger::ERROR)
,将调用构造函数,使用该对象,然后立即销毁该对象。 因此,被锁定的互斥锁被解锁。
由于您要为每个BasicLogger
实例创建独立的std::stringstream
, BasicLogger
需要一个锁来保护对std::stringstream
访问,以便多个线程可以写入同一记录器。 因此,每个实例都应拥有一个互斥量。
您还需要一个静态互斥锁,以保护同时访问std::cout
。 当日志被打印并立即释放时,将获得锁定。 当然,这需要通过BasicLogger
进行对std::cout
所有访问。
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;
};
我将只考虑一个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
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.