线程之间的更改通知标志是否需要内存屏障?

时间:2015-11-27 11:17:08

标签: c++ multithreading c++03 lock-free

我需要一个非常快的(在某种意义上"读者的低成本"而不是"低延迟")线程之间的更改通知机制以更新读取缓存:

情况

线程W(Writer)仅在一段时间内更新数据结构(S)(在我的情况下是地图中的设置)。

线程R(Reader)维护S的缓存,并且经常读取此内容。当线程W更新S时,需要在合理的时间(10-100ms)内通知线程R

架构是ARM,x86和x86_64。我需要使用gcc 4.6及更高版本支持C++03

代码

是这样的:

// variables shared between threads
bool updateAvailable;
SomeMutex dataMutex;
std::string myData;

// variables used only in Thread R
std::string myDataCache;

// Thread W
SomeMutex.Lock();
myData = "newData";
updateAvailable = true;
SomeMutex.Unlock();

// Thread R

if(updateAvailable)
{
    SomeMutex.Lock();
    myDataCache = myData;
    updateAvailable = false;
    SomeMutex.Unlock();
}

doSomethingWith(myDataCache);

我的问题

在线程R中,"快速路径"中没有锁定或障碍。 (没有可用的更新)。 这是一个错误吗?这种设计有什么后果?

我是否需要将updateAvailable限定为volatile

R最终会获得更新 吗?

到目前为止我的理解

数据一致性是否安全?

这看起来有点像" Double Checked Locking"。根据{{​​3}},可以使用内存屏障在C ++中修复它。

然而,这里的主要区别是在读取器快速路径中永远不会触摸/读取共享资源。更新缓存时,互斥锁保证了一致性。

R会获得更新吗?

这是棘手的地方。据我了解,运行线程R的CPU可以无限期地缓存updateAvailable,有效地在实际的if语句之前移动Read方式。

因此,更新可能会持续到下一次缓存刷新,例如,当计划另一个线程或进程时。

3 个答案:

答案 0 :(得分:2)

使用C ++原子并使updateAvailable成为std::atomic<bool>。这样做的原因是,不仅CPU可以看到旧版本的变量,而且特别是编译器没有看到另一个线程的副作用,因此从不麻烦重新获取变量所以你永远不会在线程中看到更新的值。此外,通过这种方式,您可以获得有保证的原子读数,如果您只是读取该值,则无法获得。

除此之外,你可能会摆脱锁定,如果例如生产者只在updateAvailable为假时生成数据,你可以摆脱互斥锁,因为std::atomic<>强制执行读写的排序。如果情况并非如此,您仍然需要锁定。

答案 1 :(得分:2)

你必须在这里使用记忆围栏。没有围栏,无法保证在其他线程上永远看到更新。在C ++ 03中,您可以选择使用特定于平台的ASM代码(英特尔上的mfence,不了解ARM)或使用OS提供的原子集/获取函数。

答案 2 :(得分:1)

  

我是否需要将updateAvailable限定为volatile

由于 volatile 与C ++中的线程模型无关,因此您应该使用atomics来使您的程序严格标准化 - 确认:

C++11或更新首选的方式是atomic<bool>使用memory_order_relaxed存储/加载:

atomic<bool> updateAvailable;

//Writer
....
updateAvailable.store(true, std::memory_order_relaxed); //set (under mutex locked)

// Reader

if(updateAvailable.load(std::memory_order_relaxed)) // check
{
    ...
    updateAvailable.store(false, std::memory_order_relaxed); // clear (under mutex locked)
    ....
}

gcc,因为4.7支持atomic builtins中的类似功能。

至于gcc 4.6,在访问updateAvailable变量时似乎没有严格确认的方法来逃避围栏。实际上,内存栅栏通常比10-100ms的时间顺序快得多。因此,您可以使用自己的atomic builtins

int updateAvailable = 0;

//Writer
...
__sync_fetch_and_or(&updateAvailable, 1); // set to non-zero
....

//Reader
if(__sync_fetch_and_and(&updateAvailable, 1)) // check, but never change
{
    ...
    __sync_fetch_and_and(&updateAvailable, 0); // clear
    ...
}
  

数据一致性是否安全?

是的,这是安全的。你的理由绝对正确:

  

永远不会在Reader快速路径中触摸/读取共享资源。

这不是双重检查锁定!

在问题本身中明确说明了这一点。

如果updateAvailable为false, Reader 线程使用变量myDataCache local 到线程(没有其他线程使用它) 。使用双重检查锁定方案,所有线程都直接使用共享对象。

为什么这里不需要记忆围栏/障碍

同时访问的唯一变量是updateAvailable。使用互斥保护访问myData变量,该保护提供所有需要的围栏。 myDataCache本地到Reader线程。

当Reader线程看到updateAvailable变量为 false 时,它使用myDataCache变量,该变量由线程本身更改计划订单保证在这种情况下对变更的正确可见性。

对于变量updateAvailable的可见性保证,C ++ 11标准即使没有围栏,也为原子变量提供了这样的保证。 29.3 p13说:

  

实现应该使原子存储在合理的时间内对原子载荷可见。

Jonathan Wakely已确认,此段落甚至适用于memory_order_relaxed次访问in chat