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));
Run Code Online (Sandbox Code Playgroud)
所以我的问题是:
编辑:如果这些问题依赖于处理器/体系结构,那么x86/x64处理器的结论是什么?
Ant*_*ams 25
该reinterpret_cast会产生不确定的行为.你的变量是a std::atomic<uint8_t>或plain uint8_t; 你不能在他们之间施放.例如,尺寸和对准要求可以不同.例如,某些平台仅对单词提供原子操作,因此std::atomic<uint8_t>将使用完整的机器字,其中plain uint8_t只能使用一个字节.非原子操作也可以以各种方式进行优化,包括与周围操作重新排序,并与相邻存储器位置上的其他操作相结合,从而可以提高性能.
这确实意味着如果您想对某些数据进行原子操作,那么您必须事先知道,并创建合适的std::atomic<>对象而不是仅仅分配通用缓冲区.当然,您可以分配一个缓冲区,然后使用placement new来初始化该缓冲区中的原子变量,但是您必须确保大小和对齐方式是正确的,并且您将无法在该缓冲区上使用非原子操作宾语.
如果你真的不关心对原子对象的排序约束,那么就使用memory_order_relaxed非原子操作.但请注意,这是高度专业化的,需要非常小心.例如,对不同变量的写入可以由与其编写的顺序不同的其他线程读取,并且不同的线程可以以不同的顺序彼此读取值,即使在程序的相同执行中也是如此.
如果CAS是不是一个字一个字节慢,你可以更好使用std::atomic<unsigned>,但是这将有空间点球,你当然不能只使用std::atomic<unsigned>访问原始字节序列---这些数据必须全部操作通过相同的std::atomic<unsigned>对象.通常,您最好编写能够满足您需求的代码,并让编译器找到最佳方法.
对于x86/x64,带有std::atomic<unsigned>变量a,a.load(std::memory_order_acquire)并且a.store(new_value,std::memory_order_release)对于非原子变量的加载和存储并不比实际指令更昂贵,但它们确实限制了编译器优化.如果您使用默认值,std::memory_order_seq_cst那么这些操作中的一个或两个将产生LOCKed指令或围栏的同步成本(我的实现将价格放在商店上,但其他实现可能选择不同).但是,memory_order_seq_cst由于它们施加的"单一总排序"约束,操作更容易推理.
在许多情况下,使用锁而不是原子操作同样快,并且更不容易出错.如果由于争用导致互斥锁的开销很大,那么您可能需要重新考虑您的数据访问模式---无论如何,缓存ping pong可能会让您遇到原子.
你的代码肯定是错误的,并且必然会做一些有趣的事情 如果事情变得非常糟糕,它可能会按照您的想法去做.我不会理解如何正确使用例如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));
Run Code Online (Sandbox Code Playgroud)
到目前为止,我的个人政策是远离任何这种无锁的东西,并留给那些知道自己在做什么的人.我会使用atomic_flag和可能的计数器,这就是我要去的地方.从概念上讲,我理解这种无锁的东西是如何工作的,但我也明白,如果你不是非常小心的话,有很多东西可能会出错.
| 归档时间: |
|
| 查看次数: |
23919 次 |
| 最近记录: |