【问题标题】:How to communicate changes to static member variable between threads in C++如何在 C++ 中的线程之间传递对静态成员变量的更改
【发布时间】:2012-09-25 22:08:00
【问题描述】:

我有一个使用静态成员变量作为标志的类。该程序是多线程的,并且对静态变量值的更改不会在线程之间一致地进行通信。

代码如下:

MyClass.h 文件:

class MyClass 
{
private:
    void runLoop();
    static bool shutdownRequested;
};

MyClass.cpp 文件:

bool MyClass::shutdownRequested = false;  // static variable definition

void MyClass::runLoop()
{
    // much code omitted 

    if (isShutdownNecessary() && !shutdownRequested)
    {
        shutdownRequested = true;  // Race condition, but that's OK
        MyLog::Error("Service shutdown requested");
        // more code omitted
    }
}

我预计上面显示的日志行可能只会出现一次,但由于竞争条件,理论上每个线程可能会出现一次。 (在我的情况下,竞争条件是可以接受的。)但是,我看到日志行在每个线程中出现了几十次。我可以说出来,因为 MyLog 类还记录每个日志行的线程 ID、进程 ID 等。

到目前为止,我仅在 Windows 版本构建中观察到此问题。我还没有在 Windows 调试版本或 Linux 版本中观察到它。

由于在多核处理器的不同内核上运行不同的线程,我可以理解每个线程查看一次日志行。我很惊讶地看到相同的线程一遍又一遍地执行日志行。

任何人都可以阐明可能导致这种情况发生的具体机制,以及我可以做什么(例如同步)来强制更新静态变量的值以被识别?

【问题讨论】:

  • 使用std::atomic<bool> 而不是bool - 问题已解决。 (根本问题是你需要一个内存屏障。)
  • 我应该提到:我不能使用 Boost 或 C++ 的最新添加。我必须使用 Visual Studio 2008。
  • 我看不出线程无法看到它自己对变量的更改。所以 AFAICS 你有一些其他问题。很可能在您未显示的代码中。
  • @echawkes :那么您最简单的选择(如果您想避免锁定)是将变量从bool 更改为LONG,并且只能使用InterlockedXXX 之一访问它功能。
  • 只是一个关于你的问题的问题。如果不同的线程在程序的不同内核上运行,这是否意味着每个线程都会有不同的堆?如果每个程序确实有不同的堆,静态变量将存储在哪里?幕后的静态变量有什么事情吗?

标签: c++ multithreading static-members


【解决方案1】:

一般来说,从不“比赛没问题”是真的。在我所知道的所有线程模型(包括 Visual C++、POSIX 线程和 C++11)下,定义为同时写入和读取普通变量的数据竞争是未定义的行为

也就是说,既然您提到您使用的是 Visual C++,您可以不必将共享变量声明为“volatile”。 Microsoft's documentation says:

当使用 /volatile:ms 编译器选项时——默认情况下 针对 ARM 以外的体系结构——编译器生成额外的 用于维护对 volatile 对象的引用之间的顺序的代码 除了保持对其他全局引用的排序 对象。特别是:

对 volatile 对象的写入(也称为 volatile 写入)具有 释放语义;即对全局或静态对象的引用 在指令中写入易失性对象之前发生 序列将发生在编译后的二进制文件中的易失性写入之前。

对 volatile 对象的读取(也称为 volatile 读取)具有 Acquire 语义;也就是说,对全局或静态对象的引用 在指令序列中读取易失性存储器后发生 将在已编译的二进制文件中进行 volatile 读取之后发生。

这使得 volatile 对象可以用于内存锁定和释放 在多线程应用程序中。

这至少使行为定义明确。从多个线程都可以记录消息的意义上讲,您仍然存在竞争条件,但它不是“未定义行为”意义上的“数据竞争”。

至于为什么一个线程可能不会“看到它自己的更新”,如果没有同步,一个线程可能会“推测性地存储”到地址以获得性能。也就是说,编译器可能会发出如下代码:

bool tmp = shutdownRequested;
shutdownRequested = true;
if (isShutdownNecessary() && !tmp)
{
    MyLog::Error("Service shutdown requested");
    // more code omitted
}
else
    shutdownRequested = false;

只要编译器能证明isShutdownNecessary()不访问shutDownRequested,这对单线程程序来说是合法的转换。编译器(或 CPU)可能认为这个推测版本更快。但是在多线程的情况下,它可能会导致您看到的行为。反汇编肯定会让你知道...

这种推测性的执行往往在每一代编译器和 CPU 中变得更加激进,这是“数据竞争”非常具体地引发未定义行为的原因之一。如果你的代码有任何机会活到下周之后,你只是不想去那里。

volatile 声明将阻止 Visual Studio 进行这种转换。但是跨平台解决此问题的唯一方法是使用互斥锁进行适当的锁定(如果这是一个繁忙的循环,可能还有一个条件变量)。这些细节在 C++11 之前的平台之间有所不同。

【讨论】:

  • 感谢您的解释:这澄清了很多事情。我认为 32 位对齐的原语(例如 int、bool)上的读/写操作在我们支持的硬件上保证是原子的,但也许我不应该这样假设。 (stackoverflow上有很多关于这个问题的讨论。)我应该在原帖中提到:我的代码是在Windows(VS 2008)和Linux(gcc 4.4.5)上编译的,所以我将重写这段代码,将shutdownRequested 的测试和写入放在同一个同步块中。
【解决方案2】:

最简单的解决方案可能是将变量声明为静态 volatile bool。 volatile 声明将阻止编译器进行任何导致变量被缓存的优化。

【讨论】:

    【解决方案3】:

    你可能想要一个互斥+共享变量。

    【讨论】:

      【解决方案4】:

      如果您不能使用 boost 或 C++11 中的任何原子特性,那么您可以使用读/写锁来避免竞争条件。这应该有助于减少互斥锁可能发生的锁定争用。当您有很多读取和偶尔(很少)写入时,读/写锁对您的情况特别有用,因为可能有多个同时读取。至于写,一次只能有一个,这和读也是互斥的。

      在 Linux 中,可以使用 pthread_rwlock_t 获得读/写锁,在 Windows 中,这里有两个参考:

      http://msdn.microsoft.com/en-us/library/windows/desktop/aa904937(v=vs.85).aspx

      http://www.codeproject.com/Articles/16411/Ultra-simple-C-Read-Write-Lock-Class-for-Windows

      【讨论】:

        猜你喜欢
        • 2015-03-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-01-25
        • 1970-01-01
        相关资源
        最近更新 更多