c++ - C++0x 中的栅栏,一般只保证原子或内存

标签 c++ multithreading c++11 memory-barriers memory-model

C++0x draft有一个栅栏的概念,这似乎与 CPU/芯片级别的栅栏概念非常不同,或者说 linux 内核人员对 fences 的期望.问题是草案是否真的暗示了一个极其受限的模型,或者措辞很差,实际上暗示了真正的围栏。

例如,在 29.8 Fences 下,它声明如下:

A release fence A synchronizes with an acquire fence B if there exist atomic operations X and Y, both operating on some atomic object M, such that A is sequenced before X, X modifies M, Y is sequenced before B, and Y reads the value written by X or a value written by any side effect in the hypothetical release sequence X would head if it were a release operation.

它使用这些术语 atomic operationsatomic object .草案中定义了这样的原子操作和方法,但是否仅指这些? 发布围栏听起来像商店围栏。不保证在栅栏之前写入所有数据存储栅栏几乎是无用的。加载(获取)栅栏和完整栅栏类似。

那么,C++0x 中的栅栏/障碍是正确的栅栏和措辞非常糟糕,还是如描述的那样受到严格限制/无用?


就 C++ 而言,假设我有这个现有代码(假设栅栏现在可用作高级构造——而不是说在 GCC 中使用 __sync_synchronize):

Thread A:
b = 9;
store_fence();
a = 5;

Thread B:
if( a == 5 )
{
  load_fence();
  c = b;
}

假设 a,b,c 的大小可以在平台上进行原子复制。以上表示c只会被分配 9 .请注意,我们不在乎线程 B 何时看到 a==5 , 只是当它执行时它也会看到 b==9 .

C++0x中保证相同关系的代码是什么?


回答:如果您阅读了我选择的答案和所有评论,您将了解情况的要点。 C++0x 似乎迫使您使用带栅栏的原子,而普通的硬件栅栏没有这个要求。在许多情况下,只要 sizeof(atomic<T>) == sizeof(T),它仍然可以用来替换并发算法。和 atomic<T>.is_lock_free() == true .

不幸的是is_lock_free不是 constexpr。这将允许它在 static_assert 中使用。 .有atomic<T>退化为使用锁通常是一个坏主意:与使用互斥锁设计的算法相比,使用互斥锁的原子算法将存在可怕的争用问题。

最佳答案

栅栏对所有数据提供排序。但是,为了保证一个线程的栅栏操作对第二个线程可见,需要对标志使用原子操作,否则会出现数据竞争。

std::atomic<bool> ready(false);
int data=0;

void thread_1()
{
    data=42;
    std::atomic_thread_fence(std::memory_order_release);
    ready.store(true,std::memory_order_relaxed);
}

void thread_2()
{
    if(ready.load(std::memory_order_relaxed))
    {
        std::atomic_thread_fence(std::memory_order_acquire);
        std::cout<<"data="<<data<<std::endl;
    }
}

如果 thread_2ready 读取为 true,则栅栏确保 data 可以安全地被读取,输出将是 data=42。如果 ready 被读取为 false,那么你不能保证 thread_1 已经发出了适当的栅栏,所以线程 2 中的栅栏仍然不会提供必要的排序保证 --- 如果 thread_2 中的 if 被省略,对 data 的访问将是数据竞争和未定义的行为,即使有栅栏。

澄清:std::atomic_thread_fence(std::memory_order_release) 通常等同于存储栅栏,并且很可能会这样实现。但是,一个处理器上的单个栅栏并不能保证任何内存排序:您需要在第二个处理器上设置相应的栅栏,并且您需要知道执行获取栅栏时释放栅栏的效果对第二个处理器可见。很明显,如果 CPU A 发出了一个获取围栏,然后 5 秒后 CPU B 发出了一个释放围栏,那么这个释放围栏就无法与获取围栏同步。除非您有某种方法可以检查是否已在其他 CPU 上发出了栅栏,否则 CPU A 上的代码无法判断它是在 CPU B 上的栅栏之前还是之后发出了栅栏。

您使用原子操作来检查是否已看到栅栏的要求是数据竞争规则的结果:您不能在没有排序关系的情况下从多个线程访问非原子变量,因此您不能使用用于检查排序关系的非原子变量。

当然可以使用更强大的机制,例如互斥锁,但这会使单独的围栏变得毫无意义,因为互斥锁会提供围栏。

宽松的原子操作可能只是现代 CPU 上的普通加载和存储,尽管可能需要额外的对齐要求以确保原子性。

如果用于检查同步的操作(而不是用于访问同步数据的操作)是原子的,那么使用特定于处理器的栅栏编写的代码可以很容易地更改为使用 C++0x 栅栏。现有代码很可能依赖于给定 CPU 上的普通加载和存储的原子性,但转换为 C++0x 将需要对这些检查使用原子操作以提供排序保证。

关于c++ - C++0x 中的栅栏,一般只保证原子或内存,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5547212/

相关文章:

C++ 无效反转函数

c++ - 为什么 x86/x86_64 上的顺序语义通过 MOV [addr], reg + MFENCE 而不是 + SFENCE 使用?

c++ - 推力 CUDA 找到每个组(段)的最大值

c++ - 尝试将数组大小调整为 -1

c++ - tinyxml 解析 xml 文件的大小是否有限制?

java - CompletableFuture没有得到执行。如果我使用ExecutorService池,则其工作正常,但不使用默认的forkJoin公共(public)池

multithreading - 需要在 Tomcat 中解释 Workmanager 的示例代码

c++ - 为什么 std::random_device 将其复制构造函数定义为已删除?

c++ - 为什么 Google Test/Mock 通过 std::unique_ptr 显示泄露的模拟对象错误?

c++ - 如何使用 <random> 替换 rand()?