放松的订单和线程间可见性



我从放松的顺序中学到了一个信号,表明原子变量上的商店应在"在理性的时间范围内"中的其他线程可见。

那就是说,我很确定它应该在很短的时间内发生(有些纳米秒?(。但是,我不想依靠"在大量的时间内"。

所以,这里有一些代码:

std::atomic_bool canBegin{false};
void functionThatWillBeLaunchedInThreadA() {
    if(canBegin.load(std::memory_order_relaxed))
        produceData();
}
void functionThatWillBeLaunchedInThreadB() {
    canBegin.store(true, std::memory_order_relaxed);
}

线程A和B在ThreadPool中,因此在此问题中没有任何线程或任何内容。我不需要保护任何数据,因此这里不需要在原子商店/负载上获取/消费/发布订购(我认为?(。

我们确定functionThatWillBeLaunchedInThreadA功能将在functionThatWillBeLaunchedInThreadB结束后启动。

但是,在这样的代码中,我们无法保证该商店在线程A中可见,因此A线程A可以读取陈旧值(false(。

这是我想的一些解决方案。

解决方案1:使用波动率

只是在这里声明volatile std::atomic_bool canBegin{false};挥发性确保我们不会看到过时的价值。

解决方案2:使用静音或旋转锁

这里的想法是通过静音/旋转锁来保护CanBegin访问,该锁定通过释放/获取订购来保证我们不会看到过时的值。我也不需要canGo是原子。

解决方案3:完全不确定,但是内存围栏?

也许此代码不起作用,所以请告诉我:(。

bool canGo{false}; // not an atomic value now
// in thread A
std::atomic_thread_fence(std::memory_order_acquire);
if(canGo) produceData();
// in thread B
canGo = true;
std::atomic_thread_fence(std::memory_order_release);

在CPP参考上,对于这种情况,请写下:

所有非原子和放松的原子商店,它们是在FB之前测序的在线程中,B将发生 - 在所有非原子和放松的原子负载之前从fa

中的线程中制作的相同位置

您将使用哪种解决方案?

您无能为力使其他线程可见商店。看看我是否不使用围栏,看到另一个核心的写作需要多长时间? - 障碍不会加快对其他核心的可见性,它们只是让这个核心等到发生。

RMW的存储部分与纯商店没有什么不同。

(肯定在X86上;不确定其他ISA,放松的LL/SC可能会从商店缓冲区获得特殊处理,如果此核心能够获得CACHE的独家所有权,则可能更有可能在其他商店之前提交。线路。但是我认为它仍然必须从排序执行中退休,以便核心知道它不是投机性的。(

安东尼的回答在评论中是误导性的。正如我在那里评论的那样:

如果RMW在另一个线程的商店承诺缓存之前运行,则它看不到该值,就像它是纯负载一样。这是否意味着"陈旧"?不,这只是意味着商店还没有发生。

RMWS需要保证"最新"的唯一原因。值是它们本质上是在该内存位置上序列化操作。如果您希望100个不同步的fetch_add操作不互相踩踏而相当于 = 100,但这就是您所需要的,但是否则最佳效果/最新可用值是可以的,这就是您从普通原子负载中获得的。

如果您需要立即可见结果(纳米秒左右(,则只能在一个线程中,例如x = y; x += z;


另外,请注意,C/C 标准要求(实际上只是注释(使在合理时间内可见的商店除外,除了以及对操作订购的要求之外。这并不意味着SEQ_CST存储的可见性可以延迟到以后的加载后。所有SEQ_CST操作都发生在所有线程中的程序顺序中的某些交织中。

在现实世界中的C 实现中,可见性时间完全取决于硬件间延迟延迟。但是C 标准是抽象的,从理论上讲,可以在需要手动冲洗的CPU上实现,以使其他线程可见。然后,由编译器决定不懒惰,而要推迟"太长"。


volatile atomic<T>是没有用的;编译器已经没有优化atomic<T>,因此ASM中的每个atomic访问都将发生在ASM中。(为什么编译器不合并冗余的std :: Atomic写道?(。这就是全部挥发性的作用,因此volatile atomic<T>与ATOMIC相同的ASM与atomic<T>相同。

定义"陈旧"是一个问题,因为在单独的内核上运行的单独线程无法立即看到彼此的动作。从另一个线程中看到商店的现代硬件需要数十纳秒。

但是您无法阅读"陈旧"来自缓存的值;这是不可能的,因为真正的CPU具有连贯的缓存。(这就是为什么volatile int可以在C 11之前使用自己的原子来滚动自己的原子,但不再有用。(您可能需要比relaxed更强的订购来获得所需的语义,因为它比一个值比另一个值大(即。"重新排序","不是陈旧"。但是,对于一个值,如果您看不到商店,则意味着在另一个核心拿走了缓存线的独家所有权之前,您的负载是为了提交其商店的。即商店还没有真正发生。

在正式的ISO C 规则中,可以保证您允许哪些价值看到哪些价值有效地为您提供了一个对单个对象的高速缓存相干性的保证,例如读者看到商店之后,进一步该线程中的加载不会看到一些较旧的商店,然后最终返回最新的商店。(https://eel.is/c draft/intro..multithread#intro.races-19(。

(注意2位作家 2个具有非Seq_cst操作的读者,读者可能会不同意商店发生的顺序。这称为IRIW重新排序,但大多数硬件不能做;PowerPC。两个原子会以其他线程以相同顺序看到不同线程的不同位置吗?在别人之前。但是,您仍然无法加快可见度,例如,只有减慢读者的速度,所以他们都没有通过"早期机构"来看到它,即使用hwsync,可以使用PowerPC负载来排空存储缓冲液。(

我们确定functionThatWillBeLaunchedInThreadAfunction知道将在结束后启动 functionThatWillBeLaunchedInThreadB

首先,如果是这种情况,那么您的任务队列机制可能已经照顾了必要的同步。

答案...

到目前为止,最简单的事情是获取/发布订购。您提供的所有解决方案都更糟。

std::atomic_bool canBegin{false};
void functionThatWillBeLaunchedInThreadA() {
    if(canBegin.load(std::memory_order_acquire))
        produceData();
}
void functionThatWillBeLaunchedInThreadB() {
    canBegin.store(true, std::memory_order_release);
}

顺便说一句,这不应该是一段时间吗?

void functionThatWillBeLaunchedInThreadA() {
    while (!canBegin.load(std::memory_order_acquire))
    { }
    produceData();
}

我不需要保护任何数据,因此获取/消费/发布这里不需要在原子商店/负载上订购(我想?(

在这种情况下,需要订购以防止编译器/CPU/内存子系统在以前的读取/写入完成之前订购canBegin存储true。实际上,它应该拖延CPU,直到可以确保在程序顺序上之前的每条写入都会在商店之前传播到canBegin。在负载侧,它可以防止在canBegin读取为true之前的内存/书写。

但是,在这样的代码中,我们无法保证商店将在线程A中可见,因此螺纹A可以读取陈旧值(false(。

你说自己:

在原子变量上的商店应该可见&quot"在理性的时间内"。

即使有了放松的内存顺序,保证书写最终可以到达其他内核,所有核心最终都会就任何给定变量的商店历史记录达成一致,因此没有过时的值。只有尚未传播的值。"放松"关于它是与其他变量有关的商店顺序。因此,memory_order_relaxed解决了陈旧的读取问题(但没有解决上述所需的顺序(。

不要使用volatile。它没有在C 内存模型中提供原子原子所需的所有保证,因此使用它是未定义的行为。请参阅https://en.cppreference.com/w/cpp/atomic/memory_order_order_order.relaxed_ordering在底部进行阅读。

you 可以使用静音或旋转锁,但是静音操作比无锁的std :: atomic acceaire-load/发行店贵得多。一个自旋锁将至少进行一个原子读取 - 修改 - - 可能的操作……甚至许多。静音绝对是过度的。但是两者都具有C 源中简单性的好处。大多数人都知道如何使用锁,因此更容易证明正确性。

内存围栏也将起作用,但您的围栏位于错误的位置(是违反直觉的(,线程间的通信变量应为std::atomic。(在玩这些游戏时要小心...!很容易获得未定义的行为(放松订购还可以。

std::atomic<bool> canGo{false}; // MUST be atomic
// in thread A
if(canGo.load(std::memory_order_relaxed))
{
    std::atomic_thread_fence(std::memory_order_acquire);
    produceData();
}
// in thread B
std::atomic_thread_fence(std::memory_order_release);
canGo.store(true, memory_order_relaxed);

实际上,内存围栏比在std::atomic加载/存储上获取/发布订购更严格,因此这一无所获,可能更昂贵。

似乎您真的想避免使用信号机制开销。这正是std::atomic获取/发行语义的发明!您太担心过时的值。是的,原子RMW将为您提供"最新"。价值,但它们本身也非常昂贵。我想让您了解获取/发布的速度。您最有可能针对X86。X86的商店订单总额和单词大小的负载/商店是原子的,因此,负载将编译到常规负载,然后将Release Store汇编到常规商店。因此,事实证明,这篇长帖子中的几乎所有内容都可能会编译为完全相同的代码。

最新更新