我有一个使用静态成员变量作为标志的类。该程序是多线程的,并且不一致地在线程之间传递对静态变量值的更改。
代码如下:
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版本中观察到它。
由于在多核处理器上的不同核心上运行的线程不同,我可以理解每个线程看一次日志行。我很惊讶地看到相同的线程一遍又一遍地执行日志行。
任何人都可以了解可能导致这种情况发生的具体机制,以及我可以做些什么(例如同步)来强制更新静态变量的值来识别?
答案 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