可以将 std::atomic 与 POD 结构一起使用,只是它有一个构造函数吗?

is it ok to use std::atomic with a struct that is POD except that it has a construtor?

我正在使用一些原子变量,都是无符号整数,我想将它们收集到一个结构中——实际上是一个 POD。但是我也想要一个构造函数,因为我的编译器不完全是 c++11(所以我必须定义我自己的构造函数来用初始值创建它)。

原来我有:

// Names are not the real names - this is just for example
std::atomic<int> counter1;
std::atomic<int> counter2;
std::atomic<int> counter3;

然后我很乐意根据需要 increment/decrement 他们。但后来我决定我想要更多的计数器,因此将它们放入一个结构中:

struct my_counters {
    int counter1;
    int counter2;
    int counter3;
    // Constructor so that I can init the values I want.
    my_counters(c1, c2, c3) : counter1(c1), counter2(c2), counter3(c3){;}
};

但由于我添加了自定义构造函数,从技术上讲,这不再是 POD。我正在阅读有关此的其他问题,他们说要使用 std::atomic 我需要一个 POD,但我阅读的其他问题表明该结构需要可复制或类似的......无论如何,我感到困惑,我想知道我是否可以安全地将我的结构 my_counters 用作原子类型:

std::atomic<my_counters> counters;

然后在各种线程中:

// Are these operations now still atomic (and therefore safe to use across threads):
counters.counter1++;
counters.counter2--;
counters.counter3 += 4;

其他人已经说过,但为了清楚起见,我认为您需要这个:

struct my_counters {
    std::atomic<int> counter1;
    std::atomic<int> counter2;
    std::atomic<int> counter3;
    // Constructor so that I can init the values I want.
    my_counters(c1, c2, c3) : counter1(c1), counter2(c2), counter3(c3){;}
};

然后简单地:

my_counters counters;

换句话说,原子的是计数器,而不是结构。该结构只是用于将它们组合在一起并初始化它们。

彼得编辑

如果您同时使用来自不同线程的这些计数器,您可能希望通过将每个计数器放在单独的缓存行中来避免线程之间的虚假共享争用。 (通常为 64 字节)。您可以在成员上使用 C++11 alignas 让编译器填充结构布局,或者在每个 atomic.

之间手动插入一些虚拟 char padding[60] 成员

由我编辑

很好 link 关于缓存的一般理解 here。值得阅读。这些天英特尔缓存行似乎是 64 字节,只是快速搜索了一下,但不要引用我的话。

我的另一个编辑

下面的评论中已经说了很多关于使用 std::atomic 来照顾(任意)class 或结构的来龙去脉,例如

struct MyStruct
{

    int a;
    int b;
};

std::atomic<MyStruct> foo = { };

但我的问题是:这什么时候有用? 具体来说,正如 ivaigult 指出的那样,您不能使用 std::atomic 改变单个成员MyStruct 以线程安全的方式。你只能用它来加载、存储或交换整个东西,想要这样做并不常见。

我能想到的唯一合法用例是当您希望能够以线程永远看不到的方式在线程之间共享(例如)struct tm 之类的东西时处于不一致的状态。然后,如果结构很小,您可能会在没有锁定特定平台的情况下逃脱,这很有用。如果你不能,请注意其中的含义(优先级反转是最严重的,对于实时代码)。

如果您 想要在线程之间共享 struct 并能够以线程安全的方式更新单个成员,那么 std::atomic 不会剪掉它(它也不是为了剪掉它而设计的)。然后,你必须求助于互斥体,为了做到这一点,从 std::mutex 派生你的结构很方便,如下所示:

struct AnotherStruct : public std::mutex
{

    int a;
    int b;
};

现在我可以做(例如):

AnotherStruct bar = { };

bar.lock ().
bar.a++;
bar.b++;
bar.unlock ();

这可以让您以线程安全的方式更新两个(大概以某种方式 linked)变量。

我很抱歉,如果这一切对于那些经验丰富的活动家来说是显而易见的,但我想澄清一下我自己的想法。它实际上与OP的问题无关。

在大多数情况下,std::atomic 对结构毫无意义,因为您最终会为每次更改复制整个结构:

std::atomic<my_counters> var(1,2,3);
my_counters another_var = var.load(); // atomic copying
another_var.counter1++;
var.store(another_var); // atomic copying

另外,loadstore是分开的操作,所以我们不能保证var.counter13对于执行上面代码的两个线程。

此外,如果您的目标 CPU 不支持这种大小的结构的原子操作,std::atomic 将回退到使用互斥锁:

#include <atomic>
#include <iostream>

struct counters {
    int a;
    int b;
    int c;
};

int main() {
    std::atomic<counters> c;
    std::atomic<int> a;

    std::cout << std::boolalpha << c.is_lock_free() << std::endl;
    std::cout << std::boolalpha << a.is_lock_free() << std::endl;
    return 0;
}

Demo

您可能会在演示中看到,std::atomic<counters> 在内部使用互斥锁。

因此,您最好让 std::atomic<int> 成为 class 成员,正如 Paul 所建议的那样。