如何在C ++中的线程之间传递对静态成员变量的更改

时间:2012-09-25 22:08:00

标签: c++ multithreading static-members

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

代码如下:

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版本中观察到它。

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

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

4 个答案:

答案 0 :(得分:2)

一般情况下,“比赛没事”永远为真。在我知道的每个线程模型(包括Visual C ++,POSIX线程和C ++ 11)下,数据竞争(定义为普通变量的同时写入和读取)是未定义的行为

那就是说,既然你提到你使用的是Visual C ++,你就可以宣布你的共享变量“volatile”了。 Microsoft's documentation says

  

当使用/ volatile:ms编译器选项时 - 默认情况下   ARM以外的体系结构是目标 - 编译器生成额外的   用于维护对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之前的平台之间有所不同。

答案 1 :(得分:1)

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

答案 2 :(得分:0)

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

答案 3 :(得分:0)

如果您无法使用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