我有这个:
单例.h
#ifndef SINGLETON_H
#define SINGLETON_H
#include <atomic>
#include <mutex>
class Singleton
{
public:
static std::atomic<Singleton*> Singleton::m_instance;
static std::mutex Singleton::m_mutex;
static Singleton* getInstance();
Singleton();
~Singleton();
};
#endif
Run Code Online (Sandbox Code Playgroud)
单例.cpp
#include "Singleton.h"
Singleton::Singleton()
{
}
Singleton* Singleton::getInstance()
{
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
if (tmp == nullptr)
{
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr)
{
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
Singleton::~Singleton() {}
Run Code Online (Sandbox Code Playgroud)
主程序
#include "Singleton.h"
#include <iostream>
int main()
{
Singleton* singleton …Run Code Online (Sandbox Code Playgroud) C++11 规定了六种内存顺序:
typedef enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
} memory_order;
Run Code Online (Sandbox Code Playgroud)
https://en.cppreference.com/w/cpp/atomic/memory_order
其中默认值为 seq_cst。
可以通过放宽操作的内存顺序来获得性能提升。但是,这取决于体系结构提供的保护。例如,Intel x86 是一种强大的内存模型,可以保证各种加载/存储组合不会被重新排序。
因此relaxed,acquire和release似乎是寻求在x86额外的性能时所需的唯一排序。
这样对吗?如果没有,是否需要在 x86 上使用consume,acq_rel和seq_cst?
我知道这memory_order_consume已被弃用,但我试图理解原始设计中的逻辑以及如何工作[[carries_dependency]]以及kill_dependency应该如何工作。为此,我想要一个在 IBM PowerPC 或 DEC alpha 上中断的具体代码示例,甚至是一个假设的架构,该架构具有一个在 C++11 或 C++14 中完全实现消耗语义的假设编译器。
我能想到的最好的例子是这样的:
int v;
std::atomic<int*> ap;
void
thread_1()
{
v = 1;
ap.store(&v, std::memory_order_release);
}
int
f(int *p [[carries_dependency]])
{
return v;
}
void
thread_2()
{
int *p;
while (!(p = ap.load(std::memory_order_consume)))
;
int v2 = f(p);
assert(*p == v2);
}
Run Code Online (Sandbox Code Playgroud)
我知道这段代码中的断言可能会失败。但是,如果您删除from ,断言是否应该失败?如果是这样,为什么会这样呢?毕竟,您请求了 a ,那么为什么您希望其他访问能够反映获取语义呢?如果删除不能使代码正确,那么有什么示例(或为所有变量设置默认值)会破坏正确的代码?[[carries_dependency]]fmemory_order_consumev[[carries_dependency]][[carries_dependency]][[carries_dependency]]
我唯一能想到的是,这也许与寄存器溢出有关?如果函数将寄存器溢出到堆栈上并稍后重新加载它,则可能会破坏依赖链。因此,[[carries_dependency]]在某些情况下可能会使事情变得高效(也就是说在调用此函数之前不需要在调用者中发出内存屏障),但也要求被调用者在任何寄存器溢出或调用另一个函数之前发出内存屏障,这在其他情况下可能效率较低案例?不过,我在这里抓住了救命稻草,所以仍然很想听听了解这些东西的人的意见......
我基本上有以下代码片段:
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 之后,即使使用更宽松的内存顺序也是如此?
提前致谢!
当在现代无锁内存上使用单侧 RDMA 时,会出现这样的问题:如果数据对象跨越多个缓存行,远程读取器如何安全地查看其传入数据。
\n在 Derecho 开源多播和复制日志库(位于https://GitHub.com/Derecho-Project上)中,我们有这种模式。写入器 W 被授予写入读取器 R 中的一系列内存的权限。内存已正确固定和映射。现在,假设写入涉及跨越许多缓存行的某种数据向量,这很常见。我们使用一个守卫:一个递增的计数器(也在 RDMA 可访问内存中,但在其他一些缓存行中)。R 旋转,当它看到变化时观察计数器\xe2\x80\xa6,这告诉 R \xe2\x80\x9c 你有一条新消息\xe2\x80\x9d,然后 R 读取向量中的数据。后来我们有第二种模式,R 对 W 说,\xe2\x80\x9c我已经处理完该消息,你可以发送另一条消息。\xe2\x80\x9d
\n我的问题:对于现代内存模型,应该使用哪种 C++ 原子风格来写入向量的内存?这会被称为宽松一致性吗?我希望我的代码能够在 ARM 和 AMD 上运行,而不仅仅是具有强大 TSO 内存模型的英特尔。
\n那么对于我的计数器,当 R 旋转监视计数器更新时,我希望如何声明计数器?是否需要将其声明为获取-释放原子?
\n最后,在 R 观察到计数器已增加之后,就速度或正确性而言,将所有内容声明为宽松的,然后在此处使用内存顺序栅栏是否有任何优点?我的想法是,通过第二种方法,我在所有 RDMA 内存上使用最小一致性模型(并对所有此类内存使用相同的模型),而且我只需要在观察到计数器增加后调用成本更高的内存顺序栅栏。因此,在访问我的向量之前,它只发生一次,而每次我的轮询线程循环时,获取释放原子计数器都会触发内存防护机制。对我来说,这听起来非常昂贵。
\n最后一个想法又引出了一个问题:我是否也必须将此内存声明为易失性,以便 C\xe2\x80\x94 编译器意识到数据可以在其脚下更改,或者编译器本身可以看到数据就足够了std::原子类型声明?在Intel上,对于全店订购,肯定需要TSO加上易失性。
\n[编辑:新信息](我试图在这里吸引一些帮助!)
\n一种选择似乎是将 RDMA 内存区域声明为 std::atomic<relaxed_consistency> 但每次我们的谓词评估线程重新测试防护时都使用锁(在 RDMA 内存中,将使用相同的宽松属性进行声明) )。我们将保留 C++ 易失性注释。
\n原因是,使用具有获取-释放语义的锁,内存一致性硬件将被警告它需要隔离先前的更新。锁本身(互斥体)可以声明为谓词线程本地的,然后将存在于本地 DRAM 中,这是便宜的,并且由于这不是任何东西争用的锁,因此锁定它可能与 test_and_set 一样便宜,并且解锁只是写入 0。如果谓词为 true,我们的触发代码体将在访问锁之后运行(可能是在锁释放之后),因此我们建立所需的顺序以确保硬件将获取受保护的对象使用实际的内存读取。但是,通过谓词测试的每个周期(每次“旋转”),我们最终都会对每个谓词执行锁定获取/释放。所以这会导致一些速度减慢。
\n选项二看似开销较小,也将 RDMA 区域声明为具有宽松一致性的 std::atomic,但省略了锁并像我们现在一样进行测试。然后,当谓词测试为真时,我们将使用语义执行显式内存栅栏(std::memory-order)。我们得到相同的屏障,但仅在谓词评估为 true 时才支付成本,因此开销更少。
\n但现在我们遇到了一个不同类型的问题。Intel 有总存储顺序 TSO,并且由于任何线程都会执行一些先写后读操作,Intel …
我读过这个问答:与“(简单)发生在之前”相比,“强烈发生在之前”的意义是什么?
作者给出了一个有趣的评估的概述,该评估在 C++20 之前是不可能的,但显然从 C++20 开始是可能的:
.-- T3 y.store(3, seq_cst); --. (2)
| | | strongly
| | sequenced before | happens
| V | before
| T3 a = x.load(seq_cst); // a = 0 --. <-' (3)
| : coherence-
| : ordered
| : before
| T1 x.store(1, seq_cst); <-' --. --. (4)
| | |st |
| | sequenced before |h |
| V |b |
| . T1 y.store(1, release); <-' | (x)
| | …Run Code Online (Sandbox Code Playgroud) 有没有一种方法可以让我们编写一些代码来为compare_exchange的“弱”版本产生“虚假失败”?虽然相同的代码应该可以很好地用于compare_exchange_strong?
我希望了解 2 个 api 的“弱”版本和“强”版本之间的真正区别,有任何示例吗?
多谢!
我正在阅读《C++ Concurrency in Action》第二版。下面的代码来自清单 7.6。它pop()使用危险指针来实现堆栈。
std::shared_ptr<T> pop() {
std::atomic<void*>& hp = get_hazard_pointer_for_current_thread();
node* old_head = head.load(); // #1
do {
node* temp;
do {
temp = old_head;
hp.store(old_head); // #2
old_head = head.load(); // #3
} while (old_head != temp); // #4
} while (old_head &&
!head.compare_exchange_strong(old_head, old_head->next));
hp.store(nullptr);
// ...
}
Run Code Online (Sandbox Code Playgroud)
书中解释了内循环的作用:
您必须在
while循环中执行此操作,以确保node在读取旧head指针#1和设置危险指针#2之间没有删除。在此窗口期间,没有其他线程知道您正在访问该特定节点。幸运的是,如果旧head节点要被删除,head那么它本身一定已经发生了变化,因此您可以检查这一点并继续循环,直到您知道该head指针仍然具有与您设置危险指针相同的值#4。
根据 的实现,如果另一个线程在和之间pop删除了头节点,则将被修改为新节点。pop …
我想知道是否可以编写std::atomic<>供 AVR \xc2\xb5C 使用的内容。__atomic_xxx()不幸的是,avr-gcc 中没有实现内置函数。
据我了解,uint8_tAVR 上的基本加载/存储是原子的,但例如operator++()不是因为它意味着 rmw 循环。因此,对于这些操作,必须禁用中断,因为这是该硬件上唯一的并发形式。对于更大的类型,uint8_t甚至operator=(T)需要防止中断。
另一方面,必须对模板的数据成员应用内存屏障:例如,该数据成员具有必须使用的std::atomic<>名称来完成机器上的实际加载/存储。valueasm volatile("" : "+m" (value));
这足以实施吗std::atomic<>?
由于此实现是无锁的,因此它应该可用于该硬件上的 ISR。
\n如果要实现这一点,std::atomic<>这将导致ISR内的机器代码效率低下,因为不必要的中断禁用和/或内存屏障会阻止优化。
std::atomic<>好吧,这可以通过扩展不安全操作的接口来规避。
另一方面:std::atomic_ref<>在 ISR 之外实施和使用它是否更可行?
对原子对象M执行释放操作A后,M的修改顺序的最长连续子序列包括:
- 由执行 A 的同一线程执行写入。(C++20 之前)
- 任何线程对 M 进行原子读-修改-写操作。被称为以 A 为首的释放序列。
Q1:为什么需要释放顺序的概念?
A1:参见“释放顺序”是什么意思?
Q2:C++20中第一项被删除了吗?
问题 3:为什么读-修改-写操作符合发布顺序,而纯写操作则不然?
宽松的 RMW 有什么特别之处,可以让它们形成一个链,而不需要成为获取加载和释放存储?是用计算机体系结构术语,还是用 C++ 语言形式主义?或者换句话说,硬件如何支持原子 RMW 的释放序列语义,但具有中断连接的纯存储?
stdatomic ×10
c++ ×9
atomic ×3
memory-model ×2
avr ×1
avr-gcc ×1
c ×1
c++11 ×1
c++20 ×1
concurrency ×1
interrupt ×1
optimization ×1
performance ×1
rdma ×1
x86 ×1