为什么要引入原子引用

在C++ 11中,std::atomic提供了一种线程安全的内存访问方式。但它要求将变量声明为原子类型,这意味着,必须为原子对象分配额外的内存。对于大型的数据结构,或者频繁访问的变量,这种开销可能是不可接受的。C++ 20中新引入的原子引用std::atomic_ref允许直接对现有非原子变量进行原子操作,无需创建副本,从而避免了这部分内存开销。

C++ 20新特性之原子引用_原子引用


什么是原子引用

原子引用std::atomic_ref是C++ 20中新引入的一个模板类,它允许我们以原子方式访问和修改非原子类型的对象。与传统的原子类型std::atomic不同,原子引用并不直接存储数据,而是引用了一个已存在的非原子类型对象。这使得可以在不改变对象类型的情况下,为其提供原子访问能力。

在很多项目中,程序的数据结构已经定义完成。出于性能或兼容性考虑,直接修改数据结构以增加原子性可能有点不切实际,甚至代价特别高昂。std::atomic_ref提供了一种灵活的解决方案,能够在不改变原有数据结构的基础上,增加原子操作的能力,从而极大增强了代码的可维护性和向前兼容性。

另外,某些特定的并发算法或数据结构可能只需要在特定时刻对特定变量进行原子操作。std::atomic_ref使得我们能够按需使用原子性,在不需要原子操作时,依旧可以使用常规的非原子访问方式。这有利于细粒度优化并发控制,减少不必要的同步开销。


std::atomic_ref的使用

要使用原子引用,需要包含头文件<atomic>。我们先来看一个最简单的使用场景:原子地对一个普通整数进行递增操作。在下面的示例代码中,s_nCounter是一个静态的全局整数变量。通过std::atomic_ref<int>,我们创建了一个指向s_nCounter的原子引用。两个线程t1和t2各自执行了1000次递增操作,由于使用了atomic_ref,这些操作是线程安全的。最后的输出结果为2000,证明了原子递增的正确性。

#include <iostream>
#include <thread>
#include <atomic>
using namespace std;

static int s_nCounter = 0;

void IncrementCounter()
{
    atomic_ref<int> refCounter(s_nCounter);
    for(int i = 0; i < 1000; ++i)
    {
        ++refCounter;
    }
}

int main()
{
    thread t1(IncrementCounter);
    thread t2(IncrementCounter);

    t1.join();
    t2.join();

    // 输出:2000
    cout << s_nCounter << endl;
    return 0;
}

下面我们来看一个更复杂的使用场景:多个线程同时对vector中的元素进行递增操作。在下面的示例代码中,我们声明了两个函数。一个是直接递增函数pIncDirectly,它接受一个引用到Data类型的参数data。使用范围for循环遍历data中的每个元素,并直接对其进行递增操作。这种方式在多线程环境下可能导致数据竞争和未定义行为,因为多个线程可能同时修改同一个元素。另一个是原子递增函数pIncAtomically,同样接受一个引用到Data类型的参数data。对于data中的每一个元素,首先通过std::atomic_ref<DataItemType>创建一个原子引用xx,然后对这个原子引用执行递增操作。这样即使在多线程环境中,对每个元素的递增也是原子的,保证了操作的线程安全性。

直接递增在多线程环境下很可能出现数据竞争,导致最终累加的结果小于预期,输出可能为39999968,这是因为多个线程同时修改数据时发生了冲突。通过atomic_ref,我们保证了递增操作的原子性。即使在多线程环境下也能正确无误地递增每个元素,确保了结果的准确性,输出为预期的40000000。

#include <atomic>
#include <iostream>
#include <numeric>
#include <thread>
#include <vector>
using namespace std;

int main()
{
    using Data = vector<char>;
    using DataItemType = Data::value_type;

    auto pIncDirectly = [](Data& data)
    {
        for (DataItemType& x : data)
        {
            ++x;
        }
    };

    auto pIncAtomically = [](Data& data)
    {
        for (DataItemType& x : data)
        {
            auto xx = atomic_ref<DataItemType>(x);
            ++xx;
        }
    };

    auto Increment = [](const auto Fun)
    {
        Data data(10'000'000);
        {
            jthread j1{Fun, ref(data)};
            jthread j2{Fun, ref(data)};
            jthread j3{Fun, ref(data)};
            jthread j4{Fun, ref(data)};
        }

        cout << accumulate(cbegin(data), cend(data), 0) << endl;
    };

    // 输出可能为:39999968
    Increment(pIncDirectly);

    // 输出为:40000000
    Increment(pIncAtomically);
    return 0;
}


💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注微信公众号“希望睿智”。