我想用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;
}
答案 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
。