如何有效地使用std :: atomic

时间:2012-01-05 20:08:25

标签: c++ c++11

std :: atomic是c ++ 11引入的新功能,但我找不到很多关于如何正确使用它的教程。那么以下的做法是否常见且有效?

我使用的一种做法是我们有一个缓冲区,我想在一些字节上使用CAS,所以我做的是:

uint8_t *buf = ....
auto ptr = reinterpret_cast<std::atomic<uint8_t>*>(&buf[index]);
uint8_t oldValue, newValue;
do {
  oldValue = ptr->load();
  // Do some computation and calculate the newValue;
  newValue = f(oldValue);
} while (!ptr->compare_exchange_strong(oldValue, newValue));

所以我的问题是:

  1. 上面的代码使用丑陋的reinterpret_cast,这是检索引用位置&amp; buf [index]的原子指针的正确方法吗?
  2. 单个字节上的CAS是否明显慢于机器字上的CAS,所以我应该避免使用它?如果我更改它以加载一个字,提取字节,计算并设置新值中的字节,并执行CAS,我的代码看起来会更复杂。这使得代码更加复杂,我还需要自己处理地址对齐。
  3. 编辑:如果这些问题依赖于处理器/体系结构,那么x86 / x64处理器的结论是什么?

3 个答案:

答案 0 :(得分:25)

  1. reinterpret_cast将产生未定义的行为。您的变量是std::atomic<uint8_t>或普通uint8_t;你不能在他们之间施放。例如,尺寸和对准要求可以不同。例如某些平台仅对单词提供原子操作,因此std::atomic<uint8_t>将使用完整的机器字,其中普通uint8_t只能使用一个字节。非原子操作也可以以各种方式进行优化,包括与周围操作重新排序,并与相邻存储器位置上的其他操作相结合,从而可以提高性能。

    这意味着如果你想对某些数据进行原子操作,那么你必须提前知道,并创建合适的std::atomic<>对象,而不是仅仅分配一个通用缓冲区。当然,您可以分配一个缓冲区,然后使用placement new来初始化该缓冲区中的原子变量,但是您必须确保大小和对齐方式是正确的,并且您将无法使用非 - 该对象的原子操作。

    如果您真的不关心对原子对象的排序约束,那么就使用memory_order_relaxed来进行非原子操作。但请注意,这是高度专业化的,需要非常小心。例如,对不同变量的写入可能由其他线程以与写入时不同的顺序读取,并且不同的线程可以以不同的顺序相互读取值,即使在程序的相同执行中也是如此。

    < / LI>
  2. 如果CAS的字节比单词慢,那么可能最好使用std::atomic<unsigned>,但这会有空间损失,你当然不能只需使用std::atomic<unsigned>来访问原始字节序列 - 对该数据的所有操作必须通过相同的std::atomic<unsigned>对象。通常情况下,编写满足您需要的代码并让编译器找到最佳方法可以做得更好。

  3. 对于x86 / x64,使用std::atomic<unsigned>变量aa.load(std::memory_order_acquire)a.store(new_value,std::memory_order_release)并不比加载和存储到非原子变量更昂贵实际的指令,但它们确实限制了编译器的优化。如果您使用默认std::memory_order_seq_cst,那么这些操作中的一个或两个将导致LOCK ed指令的同步成本或围栏(my implementation将价格置于商店,但其他实现可以选择不同)。但是,memory_order_seq_cst操作由于它们施加的“单个总排序”约束而更容易推理。

    在许多情况下,使用锁而不是原子操作同样快,并且更不容易出错。如果由于争用而导致互斥锁的开销很大,那么您可能需要重新考虑数据访问模式---无论如何,缓存ping pong可能会让您遇到原子。

答案 1 :(得分:5)

你的代码肯定是错误的,必然会做一些有趣的事情。如果事情变得非常糟糕,它可能会按照您的想法去做。我不会理解如何正确使用,例如CAS,但你会使用std::atomic<T>这样的东西:

std::atomic<uint8_t> value(0); 
uint8_t oldvalue, newvalue;
do
{
    oldvalue = value.load();
    newvalue = f(oldvalue);
}
while (!value.compare_exchange_strong(oldvalue, newvalue));

到目前为止,我个人的政策是远离任何这种无锁的东西,留给那些知道自己在做什么的人。我会使用atomic_flag和可能的计数器,这就是我要去的地方。从概念上讲,我理解这种无锁的东西是如何运作的,但我也明白,如果你不是非常小心的话,有很多事情可能会出错。

答案 2 :(得分:3)

你的reinterpret_cast<std::atomic<uint8_t>*>(...)绝对不是检索原子的正确方法,甚至不能保证工作。这是因为std::atomic<T>不能保证与T具有相同的大小。

关于CAS的字节速度较慢的第二个问题然后是机器字:这实际上与机器有关,它可能更快,可能更慢,或者甚至可能在目标架构上不存在CAS字节。在后一种情况下,实现很可能要么需要对原子使用锁定实现,要么在内部使用不同的(更大)类型(这是原子的一个例子,其大小与底层类型不同)。

从我看到的情况来看,实际上没有办法在现有值上获得std::atomic,特别是因为它们不能保证大小相同。因此,您真的应该直接buf std::atomic<uint8_t>*。此外,我相对确定即使这样的转换可以工作,通过非原子访问同一地址也不能保证按预期工作(因为即使对于字节,这种访问也不保证是原子的)。因此,使用非原子方法来访问要进行原子操作的内存位置并不是真的有意义。

请注意,对于常见体系结构,存储和字节加载都是原子的,因此只要对这些操作使用宽松的内存顺序,在那里使用atomics几乎没有性能开销。因此,如果您不关心某一点的执行顺序(例如,因为该程序尚未多线程),只需使用a.store(0, std::memory_order_relaxed)而不是a.store(0)

当然,如果你只是谈论x86,你的reinterpret_cast可能会起作用,但你的性能问题可能仍然依赖于处理器(我想,我没有查看{{1}的实际指令时间}})。