当我写这篇文章时,
std::atomic<int> * tmp = new std::atomic<int>();
Run Code Online (Sandbox Code Playgroud)
g++ 编译器返回一个错误说
Run Code Online (Sandbox Code Playgroud)invalid use of incomplete type "struct std::atomic<int>"
为什么会出现这个错误?我怎样才能避免这个?我是否需要将原子变量包装在类中并使用其指针?
智能指针也会发生同样的情况。
std::shared_ptr<std::atomic<int>> tmp = std::make_shared<std::atomic<int>> ();
Run Code Online (Sandbox Code Playgroud) 当谈到使用CAS 循环std::atomic实现时,此链接中的 cppreference给出了以下示例push:
template<typename T>
class stack
{
std::atomic<node<T>*> head;
public:
void push(const T& data)
{
node<T>* new_node = new node<T>(data);
new_node->next = head.load(std::memory_order_relaxed);
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release,
std::memory_order_relaxed /* Eh? */));
}
};
Run Code Online (Sandbox Code Playgroud)
现在,我不明白如何将 comestd::memory_order_relaxed用于失败情况,因为据我了解, (与-strongcompare_exchange_weak相同,但为了方便起见,我只是使用弱版本)是失败时的加载操作,这意味着它从另一个线程中成功的 CAS 操作加载,因此它应该使用同步…?std::memory_order_releasestd::memory_order_acquire
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release,
std::memory_order_acquire /* There you go! */));
Run Code Online (Sandbox Code Playgroud)
假设,如果“宽松负载”获得旧值之一,最终一次又一次失败,并在循环中停留额外的时间怎么办?
下面这张粗糙的图片是我的大脑被卡住的地方。
T2 的商店在 T1 上不应该可见吗?(通过相互之间具有同步关系)
总结一下我的问题,
std::memory_order_acquire,而不是std::memory_order_relaxed失败呢?std::memory_order_relaxed足够呢?std::memory_order_relaxed失败是否意味着 …我感觉这可能是一种非常普遍和常见的情况,对此有一个众所周知的无锁解决方案。
简而言之,我希望有一种像读取器/写入器锁这样的方法,但这不需要读取器获取锁,因此可以获得更好的平均性能。
相反,对于读取器来说会有一些原子操作(128 位 CAS),对于写入器来说会有一个互斥锁。我有数据结构的两个副本,一个用于正常成功查询的只读副本,以及一个要在互斥锁保护下更新的相同副本。将数据插入可写副本后,我们将其设为新的可读副本。一旦所有待处理的读取器都读完它,旧的可读副本就会被依次插入,写入器会旋转剩余的读取器数量,直到其为零,然后依次修改它,最后释放互斥体。
或类似的东西。
存在这样的东西吗?
我的静态断言有问题。静态断言完全是这样的:
static_assert(std::atomic<bool>::is_always_lock_free);
Run Code Online (Sandbox Code Playgroud)
并且代码在 Raspberry Pi 3 上失败(Linux raspberrypi 4.19.118-v7+ #1311 SMP Mon Apr 27 14:21:24 BST 2020 armv7l GNU/Linux)。
在cppreference.comatomic::is_always_lock_free 参考站点上指出:
如果此原子类型始终是无锁的,则等于 true;如果它从不或有时是无锁的,则等于 false。该常量的值与定义的宏 ATOMIC_xxx_LOCK_FREE、成员函数 is_lock_free 和非成员函数 std::atomic_is_lock_free 一致。
对我来说第一个奇怪的事情是“有时无锁”。它取决于什么?但问题过后,回到问题。
我做了一个小测试。写了这段代码:
#include <iostream>
#include <atomic>
int main()
{
std::atomic<bool> dummy {};
std::cout << std::boolalpha
<< "ATOMIC_BOOL_LOCK_FREE --> " << ATOMIC_BOOL_LOCK_FREE << std::endl
<< "dummy.is_lock_free() --> " << dummy.is_lock_free() << std::endl
<< "std::atomic_is_lock_free(&dummy) --> " << std::atomic_is_lock_free(&dummy) << std::endl
<< "std::atomic<bool>::is_always_lock_free --> " << std::atomic<bool>::is_always_lock_free << std::endl;
return 0; …Run Code Online (Sandbox Code Playgroud) 我试图了解std::atomic_thread_fence(std::memory_order_seq_cst);栅栏的用途,以及它们与栅栏有何不同acq_rel。
到目前为止,我的理解是,唯一的区别是 seq-cst 栅栏影响 seq-cst 操作的全局顺序 ( [atomics.order]/4)。并且只有在实际执行 seq-cst 加载时才能观察到所述顺序。
所以我想,如果我没有 seq-cst 负载,那么我可以用 acq-rel 栅栏替换所有 seq-cst 栅栏,而不改变行为。那是对的吗?
如果这是正确的,为什么我会看到这样的代码“使用栅栏实现 Dekker 算法”,它使用 seq-cst 栅栏,同时保持所有原子读/写宽松?这是该博客文章中的代码:
std::atomic<bool> flag0(false),flag1(false);
std::atomic<int> turn(0);
void p0()
{
flag0.store(true,std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
while (flag1.load(std::memory_order_relaxed))
{
if (turn.load(std::memory_order_relaxed) != 0)
{
flag0.store(false,std::memory_order_relaxed);
while (turn.load(std::memory_order_relaxed) != 0)
{
}
flag0.store(true,std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
}
}
std::atomic_thread_fence(std::memory_order_acquire);
// critical section
turn.store(1,std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_release);
flag0.store(false,std::memory_order_relaxed);
}
void p1()
{
flag1.store(true,std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
while (flag0.load(std::memory_order_relaxed))
{
if (turn.load(std::memory_order_relaxed) != 1)
{
flag1.store(false,std::memory_order_relaxed);
while …Run Code Online (Sandbox Code Playgroud) 当我看到这个 CPP Con 2017 网络研讨会时,Fedor Pikus 说:“它必须是直接初始化”
这是网络研讨会的链接。
这些初始化方法有什么区别?(随后,为什么它必须是“直接”初始化?为什么“间接”初始化是“不是”?)
// C++17 Compiler
#include <atomic>
class Example
{
std::atomic<bool> m_b1 = false; // 1-h
std::atomic<bool> m_b2{ false }; // 2-h
static void doSomethng()
{
std::atomic<bool> b1 = false; // 1-f
std::atomic<bool> b2{ false }; // 2-f
std::atomic<bool> b3(false); // 3-f
// Do something
}
};
Run Code Online (Sandbox Code Playgroud) 考虑std::atomic<int> x(0)。如果我理解正确,则std::memory_order_relaxed仅保证操作以原子方式发生,但不提供同步保证。因此x.fetch_add(1, std::memory_order_relaxed),来自 2 个线程的 1000 次将始终具有 2000 的最终结果。但是,这些调用中的任何一个的返回值都不能保证反映真实的当前值(例如,第 2000 次增量可能返回 1700 作为前一个值)。
但是 - 这就是我的困惑 - 鉴于这些增量是并行发生的,会x.load(std::memory_order_acquire)返回什么?或者x.fetch_add(1, std::memory_order_acq_rel)?这些是否返回真实的当前值,或者它们是否具有由于宽松的增量而导致的宽松排序所具有的过时答案的相同问题?
据我所知,该标准仅保证释放到获取(在同一变量上)同步并因此给出真实的当前值。那么如何轻松混合典型的获取-释放语义呢?
例如,我听说std::shared_ptr的引用计数以宽松的顺序递增并以 acq_rel 的顺序递减,因为它需要确保它具有真实值以便只删除一次对象。因此,我很想认为他们会给出真实的当前值,但我似乎找不到任何标准来支持它。
语境
我正在用 C++编写一个线程安全的原型线程/协程库,并且我正在使用原子来使任务切换无锁。我希望它尽可能高效。我对原子和无锁编程有一个大致的了解,但我没有足够的专业知识来优化我的代码。我做了很多研究,但很难找到我的具体问题的答案:不同内存顺序下不同原子操作的传播延迟/可见性是多少?
当前假设
我读到对内存的更改是从其他线程传播的,它们可能会变得可见:
我不确定这种延迟的可见性和不一致的传播是仅适用于非原子读取,还是也适用于原子读取,这可能取决于使用的内存顺序。当我在 x86 机器上开发时,我无法在弱有序系统上测试行为。
无论操作类型和使用的内存顺序如何,所有原子读取都总是读取最新值吗?
我很确定所有读-修改-写(RMW) 操作总是读取任何线程写入的最新值,而不管使用的内存顺序如何。对于顺序一致的操作似乎也是如此,但前提是对变量的所有其他修改也是顺序一致的。据说两者都很慢,这对我的任务不利。如果不是所有原子读取都获得最新值,那么我将不得不使用 RMW 操作来读取原子变量的最新值,或者在 while 循环中使用原子读取,以我目前的理解。
写入的传播(忽略副作用)是否取决于内存顺序和使用的原子操作?
(仅当上一个问题的答案是并非所有原子读取总是读取最新值时,此问题才有意义。请仔细阅读,我在这里不询问副作用的可见性和传播。我只关心原子变量本身的值。)这意味着根据用于修改原子变量的操作,可以保证任何后续原子读取接收变量的最新值。因此,我必须在保证始终读取最新值的操作之间进行选择,或者使用宽松的原子读取,以及这种特殊的写入操作,以保证对其他原子操作的修改的即时可见性。
int i = 0;
if(i == 10) {...} // [1]
std::atomic<int> ai{0};
if(ai == 10) {...} // [2]
if(ai.load(std::memory_order_relaxed) == 10) {...} // [3]
Run Code Online (Sandbox Code Playgroud)
在多线程环境中,语句 [1] 是否比语句 [2] & [3] 更快?
假设ai在执行 [2] 和 [3] 时,可能会或可能不会在另一个线程中写入。
附加:如果不需要底层整数的准确值,那么读取原子变量的最快方法是什么?
我基本上有以下代码片段:
size_t counter = atomic_fetch_sub_explicit(&atomicCounter, 1, memory_order_release);
if (counter - 1 == 0
&& atomic_load_explicit(&anotherAtomicCounter, 1, memory_order_relaxed) == 0 {
//Some code
}
Run Code Online (Sandbox Code Playgroud)
为了正确性,重要的是 的原子加载anotherAtomicCounter发生在 的 fetch-and-sub (FAS) 之后atomicCounter。对于给定的内存顺序,通常无法保证这一点,并且加载可能会在 FAS 之前发生。但是,我想知道序列点如何影响这个特定的代码。标准中提到
如果评估 A 在评估 B 之前排序,则 A 评估将在 B 评估开始之前完成。
与规则 2 结合
在以下二元运算符的第一个(左)操作数评估之后和第二个(右)操作数评估之前有一个序列点:&&(逻辑与)、|| (逻辑或),和,(逗号)。
这意味着原子加载必须在比较之后发生,但只有在知道 FAS 的结果后才能完成比较。
我的问题是这些规则是否保证原子加载始终发生在 FAS 之后,即使使用更宽松的内存顺序也是如此?
提前致谢!
stdatomic ×10
c++ ×9
atomic ×4
concurrency ×2
arm ×1
c ×1
c++11 ×1
c++17 ×1
gcc ×1
lock-free ×1
lockless ×1
memory-model ×1
performance ×1
pointers ×1
propagation ×1
raspberry-pi ×1
std ×1