C ++ 11线程安全的多态性,冗长度较低

时间:2015-07-29 04:01:21

标签: c++ multithreading c++11 mutex

我正在编写一个记录器,并希望使其成为线程安全的。我这样做是通过以下方式完成的:

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;
};

但是,此解决方案非常耗费资源,锁定每个项目。

我是在正确的轨道上还是有什么我想念的?

3 个答案:

答案 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;执行的语句。没有其他线程可以在完成之前启动的程序。

我有两个建议。

  1. 不要把任何锁定放在setter / getters中。这可能是因为它无法保护上述所有内容,或者因为效率。您可能希望在同一个线程中调用这些方法。如果是这样,您就不需要任何锁定。一般来说,制定者和吸气者本身不能(并且不应该)意识到他们将如何被访问。因此,更合理的地方是将锁定放在您认为多线程真正涉及的客户端代码中。
  2. 不要试图用一把锁来保护任何东西。锁应该尽可能短。如果过度使用单个锁来获取大量独立资源,那么并发程度(线程的一个主要好处)就会受到影响。

答案 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(不一定是线程安全的),但是它们仅在首次设置对象时从一个线程使用。