简体   繁体   English

如何有效地将std :: atomic <>用于非原始类型?

[英]How to use std::atomic<> effectively for non-primitive types?

The definitions for std::atomic<> seem to show its obvious usefulness for primitive or perhaps POD-types. std::atomic<>的定义似乎表明它对原始或POD类型有明显的用处。

When would you actually use it for classes? 你什么时候才能将它用于课堂?

When should you avoid using it for classes? 什么时候应该避免将它用于课堂?

The operations std::atomic makes available on any trivially copyable type are pretty basic. std::atomic操作在任何简单的可复制类型上都是非常基本的。 You can construct and destroy atomic<T> , you can ask if the type is_lock_free() , you can load and store copies of T , and you can exchange values of T in various ways. 你可以构造和销毁atomic<T> ,你可以询问类型是否为is_lock_free() ,你可以加载和存储T副本,你可以用各种方式交换T值。 If that's sufficient for your purpose then you might be better off doing that than holding an explicit lock. 如果这对你的目的来说足够了,那么你可能比持有一个明确的锁更好。

If those operations aren't sufficient, if for example you need to atomically perform a sequence operations directly on the value, or if the object is large enough that copying is expensive, then instead you would probably want to hold an explicit lock which you manage to achieve your more complex goals or avoid doing all the copies that using atomic<T> would involve. 如果这些操作不够,例如,如果您需要直接对值执行序列操作,或者如果对象足够大以至于复制成本很高,那么您可能希望保留一个您管理的显式锁定实现更复杂的目标或避免使用atomic<T>所涉及的所有副本。

// non-POD type that maintains an invariant a==b without any care for
// thread safety.
struct T { int b; }
struct S : private T {
    S(int n) : a{n}, b{n} {}
    void increment() { a++; b++; }
private:
    int a;
};

std::atomic<S> a{{5}}; // global variable

// how a thread might update the global variable without losing any
// other thread's updates.
S s = a.load();
S new_s;
do {
    new_s = s;
    new_s.increment(); // whatever modifications you want
} while (!a.compare_exchange_strong(s, new_s));

As you can see, this basically gets a copy of the value, modifies the copy, then tries to copy the modified value back, repeating as necessary. 如您所见,这基本上获取值的副本,修改副本,然后尝试复制修改后的值,并根据需要重复。 The modifications you make to the copy can be as complex as you like, not simply limited to single member functions. 您对副本所做的修改可能非常复杂,而不仅限于单个成员函数。

It works for primitive and POD types. 它适用于原始和POD类型。 The type must be memcpy -able, so more general classes are out. 类型必须是memcpy -able,因此更普遍的类已经出来了。

The standard say that 标准说

Specializations and instantiations of the atomic template shall have a deleted copy constructor, a deleted copy assignment operator, and a constexpr value constructor. 原子模板的特化和实例化应具有已删除的复制构造函数,已删除的复制赋值运算符和constexpr值构造函数。

If that is strictly the same as the answer by Pete Becker, I'm not sure. 如果这与Pete Becker的答案完全相同,我不确定。 I interpret this such that you are free to specialize on your own class (not only memcpy-able classes). 我解释这一点,你可以自由地专注于你自己的课程(不仅仅是memcpy-able课程)。

I'd prefer std::mutex for this kind of scenarios. 对于这种情况,我更喜欢std :: mutex。 Nevertheless I've tried a poor mans benchmark to profile a version with std::atomics and std::mutex in a single threaded (and thus perfectly sync) environment. 尽管如此,我还是尝试了一个糟糕的mans基准来在单线程(因此完全同步)环境中使用std :: atomics和std :: mutex来分析版本。

#include <chrono>
#include <atomic>
#include <mutex>

std::mutex _mux;
int i = 0;
int j = 0;
void a() {
    std::lock_guard<std::mutex> lock(_mux);
    i++;
    j++;
}

struct S {
    int k = 0;
    int l = 0;

    void doSomething() {
        k++;
        l++;
    }
};

std::atomic<S> s;
void b() {
    S tmp = s.load();
    S new_s;
    do {
        new_s = tmp;
        //new_s.doSomething(); // whatever modifications you want
        new_s.k++;
        new_s.l++;
    } while (!s.compare_exchange_strong(tmp, new_s));
}

void main(void) {

    std::chrono::high_resolution_clock clock;

    auto t1 = clock.now();
    for (int cnt = 0; cnt < 1000000; cnt++)
        a();
    auto diff1 = clock.now() - t1;

    auto t2 = clock.now();
    for (int cnt = 0; cnt < 1000000; cnt++)
        b();
    auto diff2 = clock.now() - t2;

    auto total = diff1.count() + diff2.count();
    auto frac1 = (double)diff1.count() / total;
    auto frac2 = (double)diff2.count() / total;
}

on my system the version using std::mutex was faster than the std::atomic approach. 在我的系统上,使用std :: mutex的版本比std :: atomic方法更快。 I think this is caused by the additional copying of the values. 我认为这是由额外复制值引起的。 Further, if used in a multithreaded environment, the the busy looping can affect performance too. 此外,如果在多线程环境中使用,繁忙的循环也会影响性能。

Summing up, yes it is possible to use std::atomic with various pod types, but in most cases std::mutex is the weapon of choice, as it is intentionally easier to understand what is going on, and therefore is not as prone to bugs as the version presented with the std::atomic. 总结一下,是的,可以使用std :: atomic和各种pod类型,但在大多数情况下,std :: mutex是首选武器,因为它有意更容易理解发生了什么,因此不容易与std :: atomic一起提供的版本的bug。

With Visual Studio 2017 I have experienced compiler error C2338 due to "alignment" issues when attempting to use std::atomic with a class; 使用Visual Studio 2017时,由于尝试将std :: atomic与类一起使用时出现“对齐”问题,我遇到了编译器错误C2338; you're much better off using a std::mutex which I ended up doing anyway. 你使用std :: mutex要好得多,我最后还是做了。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM