【问题标题】:fetch_sub with memory_order_relaxed for atomic reference counting?fetch_sub 和 memory_order_relaxed 用于原子引用计数?
【发布时间】:2016-07-19 02:59:01
【问题描述】:
std::atomic<int> cnt = {2};

thread 1:
    doFoo();
    if (cnt.fetch_sub(1, std::memory_order_relaxed) == 1) {
      doBazz();
    }

thread 2:
    doBar();
    if (cnt.fetch_sub(1, std::memory_order_relaxed) == 1) {
        doBazz();
    }

我们能保证doFoo()doBar() 总是在doBazz() 之前发生吗?

【问题讨论】:

  • 您正在使用 memory_order_relaxed,这意味着没有执行内存排序。没有这样的保证。

标签: c++ multithreading c++11 atomic


【解决方案1】:

如果线程 2 调用 doBazz(),则可以保证 doBazz() 看到 doBar() 的所有副作用,因为它们在同一个线程上运行并且有 sequenced-before 它们之间的关系。但不能保证它会看到 doFoo() 的副作用,或者根本不会调用 doFoo()。这是因为doFoo()doBazz() 之间没有形成happens-before 关系。如果 thread 1 在访问 cnt 时使用 std::memory_order_release 减少 cnt 并且 thread 2 使用 std::memory_order_acquire,则将形成这种关系,这将在两者之间创建一个同步点他们。

线程1调用doBazz()的情况是对称的。

因此,在两个线程中使用cnt.fetch_sub(1, std::memory_order_acq_rel)std::memory_order_acq_rel 结合了std::memory_order_acquirestd::memory_order_release)将提供您所询问的保证。

我们也可以使用cnt.fetch_sub(1, std::memory_order_release) 并在调用doBazz() 之前调用std::atomic_thread_fence(std::memory_order_acquire) 来达到同样的效果。

【讨论】:

    【解决方案2】:

    您显示的代码中根本没有内存排序,因此保证成立。但是,通过将fetch_sub 设为release sequence 的一部分,可以在您所在的位置使用轻松的排序并使其正常工作:

    std::atomic<int> cnt{0};
    cnt.store(2, std::memory_order_release); // initiate release sequence (RS)
    
    //thread 1:
        doFoo();
        if (cnt.fetch_sub(1, std::memory_order_relaxed) == 1) { // continue RS
            std::atomic_thread_fence(std::memory_order_acquire); // synchronizes with RS
            doBazz();
        }
    
    //thread 2:
        doBar();
        if (cnt.fetch_sub(1, std::memory_order_relaxed) == 1) { // continue RS
            std::atomic_thread_fence(std::memory_order_acquire); // synchronizes with RS
            doBazz();
        }
    

    void doBazz();
    
    std::atomic<int> cnt{0};
    cnt.store(2, std::memory_order_release); // initiate release sequence (RS)
    
    //thread 1:
        doFoo();
        if (cnt.fetch_sub(1, std::memory_order_relaxed) == 1) { // continue RS
            doBazz();
        }
    
    //thread 2:
        doBar();
        if (cnt.fetch_sub(1, std::memory_order_relaxed) == 1) { // continue RS
            doBazz();
        }
    
    void doBazz() {
        std::atomic_thread_fence(std::memory_order_acquire); // synchronizes with RS
        // ...
    }
    

    这些保证doFoo()doBar() 总是在doBazz() 之前发生。

    【讨论】:

    • Acquire 不会阻止代码在操作下方移动。
    • 我不明白你是如何防止这种情况发生的。
    • @2501 : 在 C++ 内存模型中,每个非松弛原子操作都充当编译器屏障。
    • 这是不正确的。 Acquire 仅充当阻止在其上方移动代码的屏障。反之亦然。你需要一个完整的屏障来防止代码双向移动。
    • 是的,引用是正确的,但并非每个原子操作都充当完整屏障。这是您没有看到的相关差异。获取操作具有我之前引用的特定语义。是障碍吗?是的。它是一个完整的障碍吗?不,请阅读:preshing.com/20120913/acquire-and-release-semantics/… 并观看:channel9.msdn.com/Shows/Going+Deep/…
    【解决方案3】:

    http://en.cppreference.com/w/cpp/atomic/memory_order

    即使使用宽松的内存模型,也不允许凭空产生的值循环依赖于它们自己的计算,例如,x 和 y 最初为零,

    // Thread 1:
    r1 = x.load(memory_order_relaxed);
    if (r1 == 42) y.store(r1, memory_order_relaxed);
    // Thread 2:
    r2 = y.load(memory_order_relaxed);
    if (r2 == 42) x.store(42, memory_order_relaxed);
    

    不允许产生 r1 == r2 == 42,因为仅当存储到 x 存储 42 时才可能将 42 存储到 y,这循环依赖于存储到 y 存储 42。请注意,直到 C++ 14,这在技术上是规范允许的,但不推荐给实现者。

    即使使用 memory_order_relaxed,仍然存在一些不允许的执行顺序。在我看来,

    cnt.fetch_sub(1, std::memory_order_relaxed) == 2
    

    应该发生在

    cnt.fetch_sub(1, std::memory_order_relaxed) == 1
    

    对吗?因此 doFoo() 和 doBar() 都应该在 doBazz() 之前发生。

    【讨论】:

      猜你喜欢
      • 2015-09-24
      • 1970-01-01
      • 2017-02-17
      • 1970-01-01
      • 2021-07-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-12
      • 1970-01-01
      相关资源
      最近更新 更多