我发现了一些问题(比如这个)问什么[[carries_dependency]]是,这不是我在这里问的。
我想知道你什么时候不应该使用它,因为我读过的所有答案都让人觉得你可以把这段代码贴在任何地方,而且你会神奇地得到相等或更快的代码。一个评论说代码可以相等或更慢,但海报没有详细说明。
我想在任何函数返回或参数上使用 this 的合适位置是指针或引用,并且将在调用线程内传递或返回,并且不应在回调或线程入口点上使用它。
有人可以评论我的理解并详细说明一般的主题,何时以及何时不使用它?
c++ multithreading memory-model carries-dependency stdatomic
在尝试使用 std 原子指针时,我遇到了以下问题。说我这样做:
std::atomic<std::string*> myString;
// <do fancy stuff with the string... also on other threads>
//A can I do this?
myString.load()->size()
//B can I do this?
char myFifthChar = *(myString.load()->c_str() + 5);
//C can I do this?
char myCharArray[255];
strcpy(myCharArray, myString.load()->c_str());
Run Code Online (Sandbox Code Playgroud)
我很确定 C 是非法的,因为在此期间 myString 可能会被删除。
但是我不确定 A 和 B。我认为它们是非法的,因为在执行读取操作时可能会尊重指针。
但是,如果是这种情况,您怎么能从可能被删除的原子指针中读取数据。由于负载为1步,读取数据为1步。
最近的一次讨论让我想知道与常规整数增量相比,原子增量有多昂贵.
我写了一些代码来尝试和基准测试:
#include <iostream>
#include <atomic>
#include <chrono>
static const int NUM_TEST_RUNS = 100000;
static const int ARRAY_SIZE = 500;
void runBenchmark(std::atomic<int>& atomic_count, int* count_array, int array_size, bool do_atomic_increment){
for(int i = 0; i < array_size; ++i){
++count_array[i];
}
if(do_atomic_increment){
++atomic_count;
}
}
int main(int argc, char* argv[]){
int num_test_runs = NUM_TEST_RUNS;
int array_size = ARRAY_SIZE;
if(argc == 3){
num_test_runs = atoi(argv[1]);
array_size = atoi(argv[2]);
}
if(num_test_runs == 0 || array_size == 0){
std::cout << "Usage: atomic_operation_overhead <num_test_runs> <num_integers_in_array>" << …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 循环中使用原子读取,以我目前的理解。
写入的传播(忽略副作用)是否取决于内存顺序和使用的原子操作?
(仅当上一个问题的答案是并非所有原子读取总是读取最新值时,此问题才有意义。请仔细阅读,我在这里不询问副作用的可见性和传播。我只关心原子变量本身的值。)这意味着根据用于修改原子变量的操作,可以保证任何后续原子读取接收变量的最新值。因此,我必须在保证始终读取最新值的操作之间进行选择,或者使用宽松的原子读取,以及这种特殊的写入操作,以保证对其他原子操作的修改的即时可见性。
我不是 ARM 专家,但至少在某些 ARM 架构上不会对这些存储和加载进行重新排序吗?
atomic<int> atomic_var;
int nonAtomic_var;
int nonAtomic_var2;
void foo()
{
atomic_var.store(111, memory_order_relaxed);
atomic_var.store(222, memory_order_relaxed);
}
void bar()
{
nonAtomic_var = atomic_var.load(memory_order_relaxed);
nonAtomic_var2 = atomic_var.load(memory_order_relaxed);
}
Run Code Online (Sandbox Code Playgroud)
我没有成功地让编译器在它们之间放置内存屏障。
我试过如下(在 x64 上):
$ arm-linux-gnueabi-g++ -mcpu=cortex-a9 -std=c++11 -S -O1 test.cpp
Run Code Online (Sandbox Code Playgroud)
我有:
_Z3foov:
.fnstart
.LFB331:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
movw r3, #:lower16:.LANCHOR0
movt r3, #:upper16:.LANCHOR0
mov r2, #111
str r2, [r3]
mov r2, …Run Code Online (Sandbox Code Playgroud) 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] 时,可能会或可能不会在另一个线程中写入。
附加:如果不需要底层整数的准确值,那么读取原子变量的最快方法是什么?
TL:DR:如果互斥体实现使用获取和释放操作,那么实现是否可以像通常允许的那样进行编译时重新排序,并重叠两个应该独立于不同锁的临界区?这将导致潜在的僵局。
假设互斥锁在 上实现std::atomic_flag:
struct mutex
{
void lock()
{
while (lock.test_and_set(std::memory_order_acquire))
{
yield_execution();
}
}
void unlock()
{
lock.clear(std::memory_order_release);
}
std::atomic_flag lock; // = ATOMIC_FLAG_INIT in pre-C++20
};
Run Code Online (Sandbox Code Playgroud)
到目前为止看起来还可以,关于使用单个这样的互斥锁:std::memory_order_release与std::memory_order_acquire.
在这里使用std::memory_order_acquire/std::memory_order_release不应该一见钟情。它们类似于 cppreference 示例https://en.cppreference.com/w/cpp/atomic/atomic_flag
现在有两个互斥锁保护不同的变量,两个线程以不同的顺序访问它们:
mutex m1;
data v1;
mutex m2;
data v2;
void threadA()
{
m1.lock();
v1.use();
m1.unlock();
m2.lock();
v2.use();
m2.unlock();
}
void threadB()
{
m2.lock();
v2.use();
m2.unlock();
m1.lock();
v1.use();
m1.unlock();
}
Run Code Online (Sandbox Code Playgroud)
释放操作可以在无关的获取操作之后重新排序(无关操作 == 对不同对象的后续操作),因此执行可以转换如下:
mutex m1;
data v1;
mutex …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?
来自链接: 加载/存储宽松原子变量和普通变量有什么区别?
这个回答给我留下了深刻的印象:
使用原子变量解决了这个问题——通过使用原子,即使内存顺序放宽,所有线程都可以保证读取最新的写入值。
今天,我阅读了以下链接:https : //preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/
atomic<int*> Guard(nullptr);
int Payload = 0;
Run Code Online (Sandbox Code Playgroud)
线程1:
Payload = 42;
Guard.store(&Payload, memory_order_release);
Run Code Online (Sandbox Code Playgroud)
线程2:
g = Guard.load(memory_order_consume);
if (g != nullptr)
p = *g;
Run Code Online (Sandbox Code Playgroud)
问题: 我了解到数据依赖会阻止相关指令被重新排序。但我认为这对于确保执行结果的正确性是显而易见的。comsume-release 语义是否存在并不重要。所以我想知道 comsum-release 真的可以。哦,也许它使用数据依赖性来防止指令重新排序,同时确保 Payload 的可见性?
所以
如果我使 1.preventing 指令重新排序 2.确保 Payload 的非原子变量的可见性,是否有可能使用 memory_order_relaxed 获得相同的正确结果:
atomic<int*> Guard(nullptr);
volatile int Payload = 0; // 1.Payload is volatile now
// 2.Payload.assign and Guard.store in order for data dependency
Payload = 42;
Guard.store(&Payload, memory_order_release);
// 3.data Dependency …Run Code Online (Sandbox Code Playgroud) stdatomic ×10
c++ ×9
atomic ×4
memory-model ×4
c++11 ×2
performance ×2
arm ×1
lock-free ×1
optimization ×1
propagation ×1
spinlock ×1
x86 ×1