代码之家  ›  专栏  ›  技术社区  ›  code_fodder

使用STD::原子是一个结构,它是POD,除了它有一个构造?

  •  3
  • code_fodder  · 技术社区  · 7 年前

    我正在使用一些原子变量,都是无符号的int,我想把它们收集到一个结构中——有效地说是一个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;
    

    然后我很乐意根据需要增加/减少它们。但后来我决定再要几个计数器,然后把它们放到一个结构中:

    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::原子我需要一个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;
    
    2 回复  |  直到 7 年前
        1
  •  7
  •   catnip    7 年前

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

    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 在成员上让编译器填充结构布局,或手动插入一些虚拟对象 char padding[60] 各成员之间 atomic .

    由我编辑

    关于一般理解缓存的良好链接 here . 值得一读。英特尔缓存线似乎是64字节这些天,从只是一个快速的一点谷歌,但不要引用我。

    我的另一个编辑

    在下面的评论中,有很多关于使用STD:和原子来检查一个(任意)类或结构的前后关系,例如。

    struct MyStruct
    {
    
        int a;
        int b;
    };
    
    std::atomic<MyStruct> foo = { };
    

    但我的问题是: 什么时候有用? 具体地说,正如ivaigult指出的,不能使用STD::原子来改变个体成员。 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 ();
    

    这允许您以线程安全的方式更新两个(可能以某种方式链接)变量。

    对于那些经验丰富的竞选者来说,这些都是显而易见的,我很抱歉,但我想澄清我自己的想法。实际上这和OP的问题无关。

        2
  •  1
  •   ivaigult    7 年前

    在大多数情况下 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
    

    而且, load store 是单独行动,所以我们不能保证 var.counter1 3 对于执行上述代码的两个线程。

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

    #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> 作为班级成员, 保罗 建议。