Mar*_*ram 0 c++ winapi multithreading atomic lock-free
鉴于以下情况:
class Foo
{
public:
    void Increment()
    {
        _InterlockedIncrement(&m_value); // OSIncrementAtomic
    }
    long GetValue()
    {
        return m_value;
    }
private:
    long m_value;
};
Run Code Online (Sandbox Code Playgroud)
读取时是否需要内存屏障m_value?我的理解是,这_InterlockedIncrement将生成完整的内存屏障,并确保在发生任何后续加载之前存储该值。因此,从这方面来看,这听起来很安全,但是,m_value完全可以缓存,即可以GetValue()返回陈旧的值,即使在原子递增时也是如此?
Jeff Preshing 的优秀文章供参考:https://preshing.com/20120515/memory-reordering-caught-in-the-act/
其他上下文:
我正在关注一系列有关无锁编程的文章,特别是查看变量的用法unfinishedJobs以及此处的潜在实现HasJobCompleted:
 https: //blog.molecular-matters.com/2015/08 /24/job-system-2-0-lock-free-work-stealing-part-1-basics/
void Wait(const Job* job)
{
  // wait until the job has completed. in the meantime, work on any other job.
  while (!HasJobCompleted(job))
  {
    Job* nextJob = GetJob();
    if (nextJob)
    {
      Execute(nextJob);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)
可以通过将 unfinishedJobs 与 0 进行比较来确定作业是否已完成。
那么,考虑到这种情况,可能的实现是否HasJobCompleted需要内存屏障?
不,您不需要屏障,但如果读者和编写者在不同的线程中调用这些函数,您的代码无论如何都会被破坏。特别是当读者在循环中调用 read 函数时。
TL:DR:在增量和读取器中使用 C std::atomic<long> m_value++ return m_value++11 return m_value。这将为您提供无数据争用程序中的顺序一致性:执行将像线程以源顺序的某种交错运行一样工作。(除非您违反规则并拥有其他非共享数据。)如果您希望执行增量的线程知道它们产生了什么值,那么atomic您肯定希望从 中返回一个值。对于诸如另一个线程的增量在 之间可见的Increment用例来说,在之后进行单独的加载是完全破坏的。int sequence_num = shared_counter++;count++; tmp = count;
如果您不需要对与读取器/写入器在同一线程中的其他return m_value.load(std::memory_order_acquire)对象进行的操作进行如此强的排序,那么对于大多数用途来说就足够了,并且m_value.fetch_add(1, std::memory_order_acq_rel). 实际上很少有程序在任何地方都需要 StoreLoad 屏障;即使使用 .atomic RMW 实际上也不能进行太多重新排序acq_rel。(在 x86 上,它们的编译方式与您使用的相同seq_cst。)
你不能在线程之间强制排序;负载要么看到该值,要么看不到该值,具体取决于读取线程在获取/尝试获取负载值之前或之后是否看到来自编写器的无效。线程的全部要点是它们不会彼此同步运行。
循环读取m_value可以将负载提升出循环,因为它不是atomic(或者甚至volatile作为黑客)。这是数据竞争 UB,编译器会破坏你的阅读器。看到这个和多线程程序陷入优化模式但在-O0中正常运行
障碍不是这里的问题/解决方案,只是强制重新检查内存(或当前 CPU 看到的内存的缓存一致性视图;L1d 和 L2 等实际的 CPU 缓存对此不是问题)。这并不是障碍真正的作用。他们命令该线程访问一致缓存。C++ 线程仅跨具有一致缓存的核心运行。
但说真的,如果没有非常令人信服的理由,不要推出自己的原子。 何时在多线程中使用 易失性?几乎从来没有。该答案解释了缓存一致性,并且您不需要障碍来避免看到过时的值。
在许多现实世界的 C++ 实现中,类似的东西std::atomic_thread_fence()也将是一个“编译器屏障”,它迫使编译器从内存中重新加载非原子变量,即使没有volatile,但这是一个实现细节。因此,在某些 ISA 的某些编译器上,它可能运行得足够好。并且仍然不能完全安全地防止编译器发明多个加载;请参阅 LWN 文章谁害怕一个糟糕的优化编译器?有关详细信息的示例;主要针对 Linux 内核如何使用 来滚动自己的原子volatile,这实际上是由 GCC/clang 支持的。
初学者常常对此感到恐慌,并认为 RMW 操作由于其指定方式而在某种程度上更好。由于它们是捆绑在一起的读 + 写,并且每个内存位置都有单独的修改顺序,因此 RMW 操作必须等待对缓存行的写访问,这意味着在单个位置上序列化所有写操作和 RMW。
原子变量的普通加载仍然保证(通过实际实现)立即看到值。(ISO C++ 仅建议应在有限时间内及时查看值,但当然实际实现可以做得更好,因为它们运行在缓存一致的 CPU 硬件上。)
两个线程之间不存在“立即”这样的东西;另一个线程中的加载看到存储的值,或者它在存储对其他线程可见之前运行但不可见。通过线程调度等,线程总是可能会加载一个值,但很长一段时间不会使用它;装载时它是新鲜的。
因此,这与正确性几乎无关,剩下的就是担心线程间延迟。在某些情况下,这可能会受到屏障的帮助(为了减少以后内存操作的争用,而不是主动更快地刷新存储,屏障只是等待这种情况以正常方式发生)。因此,这通常是一个非常小的影响,而不是使用额外障碍的理由。
请参阅MESI 协议和 std::atomic - 它是否确保所有写入对其他线程立即可见?。请参阅我对https://github.com/dotnet/runtime/issues/67330#issuecomment-1083539281和硬件内存屏障除了提供必要的保证之外还能使原子操作的可见性更快吗? 通常不会,即使有,也不会太多。
atomic当然,如果您不需要这种顺序来确保正确性,那么仅仅为了使其比其他变量晚于其他变量查看该变量atomic,那么用许多额外的障碍来减慢读者的速度当然是不够的。或者减慢写入器的速度,让它坐在那里什么也不做,也许让它更快地完成几个周期的 RFO,而不是完成其他有用的工作。
如果您对线程的使用在核心间延迟方面遇到了如此严重的瓶颈,以至于值得这样做,那么这可能表明您需要重新考虑您的设计。
如果没有障碍或排序,只需std::atomic使用memory_order_relaxed,您通常仍然会在大约 40 纳秒内看到其他内核上的数据(在现代 x86 台式机/笔记本电脑上),大约与两个线程都使用原子 RMW 相同。而且它不可能被延迟任何显着的时间,比如一微秒,如果你为许多早期的存储创建了大量的争用,那么它们都需要很长时间才能在这个存储之前提交。您绝对不必担心走很长时间看不到商店。(当然,这仅适用于带有atomic. 或手动滚动的原子volatile。普通的非易失性加载可能只在循环开始时检查一次,然后就不再检查了。这就是为什么它们不能用于多线程。)
|   归档时间:  |  
           
  |  
        
|   查看次数:  |  
           929 次  |  
        
|   最近记录:  |