c++ - 了解原子变量和操作

标签 c++ multithreading atomic memory-fences

我一遍又一遍地阅读关于 boost 和 std 的 (c++11) 原子类型和操作,但我仍然不确定我是否理解正确(在某些情况下我根本不理解)。所以,我有几个问题。

我用于学习的资源:


考虑以下代码段:

atomic<bool> x,y;

void write_x_then_y()
{
    x.store(true, memory_order_relaxed);
    y.store(true, memory_order_release);
}

#1:它等同于下一个吗?

atomic<bool> x,y;

void write_x_then_y()
{
    x.store(true, memory_order_relaxed);
    atomic_thread_fence(memory_order_release);    // *1
    y.store(true, memory_order_relaxed);          // *2
}

#2:以下陈述是否正确?

*1 行确保,当在该行下完成的操作(例如 *2)可见时(对于使用获取的其他线程),*1 之上的代码也将可见(具有新值)。


下一个片段扩展了上面的片段:

void read_y_then_x()
{
    if(y.load(memory_order_acquire))
    {
        assert(x.load(memory_order_relaxed));
    }
}

#3:它等同于下一个吗?

void read_y_then_x()
{
    atomic_thread_fence(memory_order_acquire);    // *3
    if(y.load(memory_order_relaxed))              // *4
    {
        assert(x.load(memory_order_relaxed));     // *5
    }
}

#4:以下陈述是否正确?

  • 第 *3 行确保如果释放顺序下的某些操作(在其他线程中,如 *2)可见,则释放顺序之上的每个操作(例如 *1)也将可见。
  • 这意味着 assert at *5 永远不会失败(默认值为 false)。
  • 但这并不能保证即使物理上(在处理器中)*2 发生在 *3 之前,它也会通过上面的片段可见(在不同的线程中运行)——函数 read_y_then_x() 仍然可以读取旧值。唯一可以确定的是,如果 y 为真,则 x 也为真。

#5:原子整数的递增(加1操作)可以是memory_order_relaxed并且没有数据丢失。唯一的问题是结果可见的顺序和时间。


根据 boost,以下片段是工作引用计数器:

#include <boost/intrusive_ptr.hpp>
#include <boost/atomic.hpp>

class X {
public:
  typedef boost::intrusive_ptr<X> pointer;
  X() : refcount_(0) {}

private:
  mutable boost::atomic<int> refcount_;
  friend void intrusive_ptr_add_ref(const X * x)
  {
    x->refcount_.fetch_add(1, boost::memory_order_relaxed);
  }
  friend void intrusive_ptr_release(const X * x)
  {
    if (x->refcount_.fetch_sub(1, boost::memory_order_release) == 1) {
      boost::atomic_thread_fence(boost::memory_order_acquire);
      delete x;
    }
  }
};

#6 为什么要减少使用的 memory_order_release?它是如何工作的(在上下文中)?如果我之前写的是真的,是什么让返回值成为最新值,尤其是当我们在读取之后而不是之前/期间使用获取时?

#7 为什么引用计数器归零后会有acquire order?我们只是读到计数器为零并且没有使用其他原子变量(指针本身没有被标记/使用)。

最佳答案

1:否。释放栅栏与所有获取操作和栅栏同步。如果有第三个atomic<bool> z在第三个线程中被操作,栅栏也会与第三个线程同步,这是不必要的。也就是说,它们在 x86 上的行为相同,但那是因为 x86 具有非常强的同步性。 1000 核心系统上使用的架构往往更弱。

2:是的,这是正确的。栅栏确保如果您看到后面的任何内容,您也会看到前面的所有内容。

3:一般来说它们是不同的,但实际上它们是相同的。允许编译器对不同变量的两个宽松操作重新排序,但不得引入虚假操作。如果编译器有任何方法确信它需要读取 x,它可能会在读取 y 之前这样做。在您的特定情况下,这对编译器来说非常困难,但在许多类似的情况下,这种重新排序是公平的游戏。

4:这些都是真的。原子操作保证了一致性。它们并不总是保证事情按照您想要的顺序发生,它们只是防止破坏您的算法的病态顺序。

5:正确。宽松的操作是真正原子的。他们只是不同步任何额外的内存

6:对于任何给定的原子对象 M , C++ 保证对 M 的操作有一个“官方”顺序.您看不到 M 的“最新”值与 C++ 和处理器一样,所有线程都将看到一系列一致的 M 值。 .如果两个线程递增引用计数,然后递减它,则无法保证哪个线程会将其递减为 0,但可以保证其中一个线程会看到它已将其递减为 0。它们都无法做到看到它们递减 2->1 和 2->1,但不知何故,refcount 将它们合并为 0。一个线程将始终看到 2->1,而另一个将看到 1->0。

请记住,内存顺序更多的是围绕原子同步内存。无论您使用什么内存顺序,原子都会得到正确处理。

7:这个比较棘手。 7 的简短版本是递减是释放顺序,因为某些线程将不得不运行 x 的析构函数,我们希望确保它看到所有线程上对 x 进行的所有操作。在析构函数上使用释放顺序可以满足此需求,因为您可以证明它有效。负责删除 x 的人在删除之前获取所有更改(使用栅栏确保删除器中的原子不会向上漂移)。在线程释放它们自己的引用的所有情况下,很明显所有线程在调用删除器之前都会有一个释放顺序递减。在一个线程递增引用计数而另一个线程递减它的情况下,您可以证明这样做的唯一有效方法是线程彼此同步,以便析构函数看到两个线程的结果。无论如何,同步失败都会产生竞争情况,因此用户有义务正确处理。

关于c++ - 了解原子变量和操作,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17345680/

相关文章:

c++ - 如果 volatile 是不必要的,为什么 std::atomic 方法提供 volatile 重载?

ios - 如何从内部类正确转发原子属性?

c++ - Win32 C++ 在第二个监视器中创建窗口

android - 上载图片时,ProgressDialog不显示

spring - 阻止 Spring MVC Controller ?

c++ - 为什么存储到原子 unique_ptr 会导致崩溃?

c++ - 美化 Win32 应用程序中的工具提示

c++ - 回文在 C++ 函数中返回 false

c++ - 抑制警告 : comparison is always false due to limited scope of data type

multithreading - FIFO队列的共识数量