【问题标题】:Do I need a memory barrier for a change notification flag between threads?线程之间的更改通知标志是否需要内存屏障?
【发布时间】:2016-03-01 14:04:13
【问题描述】:

为了更新读取缓存,我需要一个非常快速(在某种意义上“读取器成本低”,而不是“低延迟”)更改通知机制:

情况

线程 W (Writer) 只偶尔更新一次数据结构 (S)(在我的例子中是地图中的设置)。

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

架构是 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 会得到更新最终吗?

我目前的理解

数据一致性安全吗?

这看起来有点像“双重检查锁定”。根据http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html,可以使用内存屏障在 C++ 中修复它。

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

R 会收到更新吗?

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

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

【问题讨论】:

  • 代码有未定义的行为,句号。添加volatile 不会改变这一点。
  • 你能详细说明一下吗?我看到它未定义的唯一方法是如果 CPU 不能可靠地将存储传播到其他内核。 volatile 应确保编译器实际上向内存地址发出存储(无论 CPU 决定如何处理,都是另一回事...... - 它可以保存在写入队列中、缓存中或到达主内存之前的任何东西中)
  • 对另一个线程同时修改的变量的非原子读取是未定义的行为。添加volatile 根本不会改变这一点。标准对此很清楚,违反该规则的是UB,因此您无法预测编译器将对您的代码做什么。该标准没有提及 CPU 或其他内核,它说在这种情况下确保正确行为的唯一方法是使用原子操作。 isvolatileusefulwiththreads.com
  • @JonathanWakely 这就是我陈述架构并使用编译器的原因。我可以轻松地检查我的代码并提供通用替代方案,该替代方案对于实现未知的环境而言性能较低。标准中未定义的东西可以很好地为特定的架构和编译器定义。

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


【解决方案1】:

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

除此之外,您可能会摆脱锁定,例如,如果生产者仅在 updateAvailable 为 false 时生成数据,您可以摆脱互斥锁,因为 std::atomic<> 强制执行正确的读取顺序并写道。如果不是这种情况,您仍然需要锁。

【讨论】:

  • std::atomic 在 C++03 中不可用。由于很少更新,因此不需要解除锁定
  • @HannoS。那么您需要使用编译器/平台的内置函数来提供原子操作。 Volatile 仅适用于信号处理程序。
【解决方案2】:

你必须在这里使用内存栅栏。如果没有栅栏,则无法保证在其他线程上永远会看到更新。在 C++03 中,您可以选择使用特定于平台的 ASM 代码(Intel 上的mfence,不了解 ARM)或使用操作系统提供的原子设置/获取函数。

【讨论】:

  • C++ 标准要求 保证,写入任何位置的内容都会在 合理的时间内被其他线程看到。如果您想要精确的措辞 - 我会在标准中找到它。
  • @Tsyvarev 不。你很困惑。这些保证仅在使用原子时适用。
  • 29.3.13:Implementations should make atomic stores visible to atomic loads within a reasonable amount of time.。是的,这不是关于访问 any 变量,而是 atomic store 包括 .storememory_order_relaxed 顺序,即没有围栏。至于给定示例中的栅栏,它们由 .lock() 和 .unlock() 操作提供。
  • 一位同事向我指出,ever 部分由 CPU 缓存一致性协议保证。 x86 和 armv7mp 都有缓存一致性协议。你能对此发表评论吗?
  • @HannoS。只有当您试图解释事情如何在特定平台上运行时,这才是相关的。这与您的保证无关。
【解决方案3】:

我是否需要将updateAvailable 限定为volatile

由于 volatile 与 C++ 中的线程模型无关,因此您应该使用原子来使您的程序严格符合标准:

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-100 毫秒的时间快得多。所以你可以使用自己的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
    ...
}

关于数据一致性是否安全?

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

在阅读器快速路径中永远不会接触/读取共享资源。


这不是双重检查锁定!

问题本身已明确说明。

如果updateAvailable 为假,Reader 线程使用变量myDataCache,它是线程的本地(没有其他线程使用它)。使用双重检查锁定方案,所有线程都直接使用 shared 对象。

为什么这里不需要内存栅栏/屏障

唯一同时访问的变量是updateAvailablemyData 变量是通过互斥保护访问的,它提供了所有需要的栅栏。 myDataCache 是 Reader 线程的本地

当阅读器线程看到updateAvailable 变量为false,它使用myDataCache 变量,该变量由线程本身更改程序顺序保证在这种情况下更改的正确可见性。

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

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

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

【讨论】:

  • @SergeyA:Atomics 只需要根据 C++11 标准规避 未定义的行为。但是所有常用的编译器(包括gcc)即使没有它也能生成正确的代码。此外,asker 明确提到了 C++03,它缺乏原子性。
  • 首先,volatile 在 ARM 上不会以这种方式工作。时期。由于严格的订购保证,它适用于英特尔,而 ARM 没有提供这些保证。其次,原子确实存在于 C++ 之外 - 请参阅我的回答。
  • 我认为 volatile 指示编译器不要将变量缓存在寄存器中,也在 ARM 上。由于缓存一致性协议(ARM 也有),如果 CPU 的读/写队列在合理的时间内被清除,这应该就足够了。
  • @JustSid:为什么负载也不能轻松操作?根据提问者的描述,正是加载操作,应该尽可能快。
  • @DavidSchwartz:这里没有双重检查锁定!这在问题帖子本身中有明确说明。我已将此注释添加到我的答案中,请检查。
猜你喜欢
  • 2012-09-08
  • 2017-08-23
  • 1970-01-01
  • 2011-10-12
  • 2014-01-20
  • 2012-05-27
  • 2017-01-05
  • 2018-08-24
  • 2021-03-19
相关资源
最近更新 更多