13

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

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

如果存在原子操作 X 和 Y,则释放栅栏 A 与获取栅栏 B 同步,两者都对某个原子对象 M 进行操作,使得 A 在 X 之前排序,X 修改 M,Y 在 B 之前排序,并且 Y 读取值由 X 写入或由假设释放序列中的任何副作用写入的值 X 将在它是一个释放操作时开始。

它使用这些术语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>为使用锁通常是一个坏主意:与使用互斥锁设计的算法相比,使用互斥锁的原子算法将存在可怕的争用问题。

4

2 回答 2

17

Fences 提供对所有数据的排序。但是,为了保证一个线程的栅栏操作在一秒钟内可见,你需要对标志使用原子操作,否则你有一个数据竞争。

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_2读取readytrue,则栅栏确保data可以安全地读取 ,并且输出将为data=42。如果ready被读取为false,那么你不能保证thread_1已经发布了适当的栅栏,所以线程 2 中的栅栏仍然不能提供必要的排序保证——如果ifinthread_2被省略,访问data将是数据竞争并且未定义行为,即使有栅栏。

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

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

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

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

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

于 2011-04-05T08:09:36.397 回答
2

我的理解是它们是适当的围栏。间接证据是,毕竟,它们旨在映射到实际硬件中发现的特性,并允许有效地实现同步算法。正如您所说,仅适用于某些特定值的围栏是 1. 无用和 2. 在当前硬件上找不到。

话虽如此,您引用的 AFAICS 部分描述了栅栏和原子操作之间的“同步”关系。有关这意味着什么的定义,请参阅第 1.10 节多线程执行和数据竞争。同样,AFAICS,这并不意味着栅栏仅适用于原子对象,而是我怀疑其含义是,虽然普通的加载和存储可能会以通常的方式(仅一个方向)通过获取和释放栅栏,但原子加载/商店可能没有。

写。原子对象,我的理解是,在 Linux 支持的所有目标上,正确对齐的纯整数变量 sizeof() <= sizeof(*void) 是原子的,因此 Linux 使用普通整数作为同步变量(即 Linux 内核原子操作运行在普通整数变量上)。C++ 不想强加这样的限制,因此单独的原子整数类型。此外,在 C++ 中对原子整数类型的操作意味着障碍,而在 Linux 内核中,所有障碍都是显式的(这很明显,因为没有编译器对原子类型的支持,这是必须做的)。

于 2011-04-05T08:04:34.043 回答