我正在编写一个记录器,并希望使其成为线程安全的。我这样做是通过以下方式完成的:
class Logger
{
public:
virtual ~Logger();
LogSeverity GetSeverity() const;
void SetSeverity(LogSeverity s);
protected:
std::mutex mutex;
private:
LogSeverity severity;
};
void Logger::SetSeverity(LogSeverity s)
{
std::lock_guard<std::mutex> lock(mutex);
severity = s;
}
LogSeverity Logger::GetSeverity() const
{
std::lock_guard<std::mutex> lock(mutex);
return severity;
}
void Logger::SetSeverity(LogSeverity s) const
{
std::lock_guard<std::mutex> lock(mutex);
severity = s;
}
// StreamLogger inherits from Logger
void StreamLogger::SetStream(ostream* s)
{
std::lock_guard<std::mutex> lock(mutex);
stream = s;
}
ostream* StreamLogger::GetStream() const
{
std::lock_guard<std::mutex> lock(mutex);
return stream;
}
但是,对该类的所有公共访问都需要这种极其冗余的锁定。
我看到两个选项:
1)这些公共函数的调用者将使用类
中的互斥锁来锁定整个对象Logger l = new Logger();
std::lock_guard<std::mutex> lock(l->lock());
l->SetSeverity(LogDebug);
2)Wrapper锁定类
中的每个变量template typename<T> struct synchronized
{
public:
synchronized=(const T &val);
// etc..
private:
std::mutex lock;
T v;
};
class Logger
{
private:
synchronized<LogSeverity> severity;
};
但是,此解决方案非常耗费资源,锁定每个项目。
我是在正确的轨道上还是有什么我想念的?
答案 0 :(得分:2)
首先,您需要仔细重新考虑可能的用例:
我有一种奇怪的感觉,你从很小的角度思考你的课程:“好吧,它是一个记录器,所以我会将所有可能有用的功能放入其中”(我可能是错误)。类应具有完整但最小的接口,明确表示特定类负责的内容。想一想。
至于你的多线程问题:我不认为共享记录器是个好主意。 就个人而言,在这种情况下我总是喜欢特定于线程的原语(每个线程一个记录器)。为什么呢?
如果您的编译器支持C ++ 11,则上述解决方案基本上正确使用thread_local
,__declspec(thread)
或__thread
,具体取决于编译器支持的内容。
如果仍然想要实施共享记录器,请从设计审核开始。例如:您确定,更改单个属性需要锁定互斥锁吗?像severity
成员这样的人是std::atomic
的完美候选人。它可能需要更多工作,但可以更多更快。
class Logger
{
//cut
private:
std::atomic<LogSeverity> severity;
};
void Logger::SetSeverity(LogSeverity s)
{
severity.store(s, std::memory_order_release);
}
LogSeverity Logger::GetSeverity() const
{
return severity.load(std::memory_order_acquire);
}
std::memory_order_acquire/release
只是一个示例 - 您可能希望使用更强大的排序,例如memory_order_seq_cst
(如果您需要全局排序)。但是acquire/release
对通常足以确保加载和存储之间的正确同步以及小额奖励 - 它们不会在x86上产生任何围栏。
如果您认为您可能想要阅读Anthony Williams的C++ Concurrency in Action
。它是学习线程,原子,同步,内存排序等的最佳资源。
Bartosz Milewski's blog上还有一篇非常好的文章。像这样:C++ atomics and memory ordering 。
如果您不熟悉原子,栅栏,排序等主题,那么这些资源就非常适合开始。
答案 1 :(得分:0)
让我们假设您需要在不同的线程中访问这些setter和getter是合理的。
我可能错了。但是根据你所展示的有限代码,锁定这些成员的方式是错误的。锁定配对的setter和getter并不是那么简单。考虑一下:
void tYourClass::thread_1()
{
..
m_streamLogger.SetStream(/*new stream*/);
}
void tYourClass::thread_2()
{
ostream *stream = m_streamLogger.GetStream();
// access the returned stream
// stream->whatever()
}
在这种情况下,在您获取流句柄并访问它之间,另一个线程启动并设置流。会发生什么?你会得到一个&#34; 悬空&#34; stream:您可以访问已删除的对象,或记录其他人永远不会看到的内容(取决于SetStream
内的逻辑)。你的锁无法保护它。根本原因是你应该锁定应该作为单个&#34; atomic&#34;执行的语句。没有其他线程可以在完成之前启动的程序。
我有两个建议。
答案 2 :(得分:0)
首先,我怀疑你是否真的需要那些制定者。使类线程安全的最简单方法是使其不可变。
即使您确实让它们成为“安全”的线程,您是否真的希望一个线程更改目标流,而另一个线程正在记录消息?
在这种情况下,您可以简单地在构造函数中设置severity和steam:
StreamLogger(LogSeverity severity, ostream& steam);
如果构造函数参数的数量变得无法管理,您可以创建构建器或工厂,或者只将参数分组到一个对象中:
StreamLogger(const StreamLoggerArgs& arguments);
或者,您可以将实际需要线程安全的记录器部分分离到接口中。例如:
class Logger {
protected:
~Logger(){};
public:
virtual void log(const char* message) = 0;
virtual LogSeverity GetSeverity() const = 0;
};
这是您传递给多个线程的接口,如果您愿意,具体实现仍然可以具有setter(不一定是线程安全的),但是它们仅在首次设置对象时从一个线程使用。