2

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

代码如下所示:

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

4 回答 4

2

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

也就是说,由于您提到您使用的是 Visual C++,因此您可以避免将共享变量声明为“volatile”。 微软的文档说

当使用 /volatile:ms 编译器选项时(默认情况下,当针对 ARM 以外的体系结构时)编译器生成额外的代码来维护对 volatile 对象的引用之间的顺序,以及维护对其他全局对象的引用的顺序。尤其:

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

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

这使得 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 之前的平台之间有所不同。

于 2012-09-26T00:30:50.460 回答
1

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

于 2012-09-25T23:55:23.307 回答
0

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

于 2012-09-25T22:09:56.397 回答
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

于 2012-09-26T07:10:03.170 回答