C++ 20 原子引用 (一)
std::atomic_ref
{}
std::atomic_ref
类型对其引用的对象进行原子操作。
使用std::atomic_ref
进行多线程读写时不会造成数据争用。被引用对象的生命周期必须超过std::atomic_ref
。操作std::atomic_ref
的子对象是未定义行为。
错误示例
你可能认为在一个原子内使用引用可以实现这种操作,实际上不可以:
#include <atomic>
#include <iostream>
#include <random>
#include <thread>
#include <vector>
struct ExpensiveToCopy
{
int counter{};
};
int getRandom(int begin, int end)
{
std::random_device seed;
std::mt19937 engine(seed());
std::uniform_int_distribution<> uniformDist(begin, end);
return uniformDist(engine);
}
void count(ExpensiveToCopy& exp)
{
std::vector<std::thread> v;
std::atomic<int> counter{exp.counter};
for (int n = 0; n < 10; ++n)
{
v.emplace_back([&counter]
{
auto randomNumber = getRandom(100, 200);
for (int i = 0; i < randomNumber; ++i) { ++counter; }
});
}
for (auto& t : v) t.join();
}
int main()
{
std::cout << std::endl;
ExpensiveToCopy exp;
count(exp);
std::cout << "exp.counter: " << exp.counter << '\n';
std::cout << std::endl;
}
最后的结果应该接近1500,所以出现了错误,原因是 std::atomic<int> counter{exp.counter}
实际上创建了一个副本。
接下来使用一个简单的示例演示一下:
int main(int argc, char* argv[])
{
int val{ 10086 };
int& ref = val;
std::atomic<int> atomicRef{ ref };
++atomicRef;
std::cout << std::format("val = {}, ref = {}, atomicRef = {}", val, ref, atomicRef.load()) << std::endl;
}
使用std::atomic_ref<int> counter{exp.counter}
代替std::atomic<int> counter{exp.counter}
解决这个问题
#include <atomic>
#include <iostream>
#include <random>
#include <thread>
#include <vector>
struct ExpensiveToCopy
{
int counter{};
};
int getRandom(int begin, int end)
{
std::random_device seed;
std::mt19937 engine(seed());
std::uniform_int_distribution<> uniformDist(begin, end);
return uniformDist(engine);
}
void count(ExpensiveToCopy& exp)
{
std::vector<std::thread> v;
std::atomic_ref<int> counter{exp.counter};
for (int n = 0; n < 10; ++n)
{
v.emplace_back([&counter]
{
auto randomNumber = getRandom(100, 200);
for (int i = 0; i < randomNumber; ++i) { ++counter; }
});
}
for (auto& t : v) t.join();
}
int main()
{
std::cout << std::endl;
ExpensiveToCopy exp;
count(exp);
std::cout << "exp.counter: " << exp.counter << '\n';
std::cout << std::endl;
}
看到这里你可能会想为什么不把计数器最开始就定义成原子变量呢像这样:
struct ExpensiveToCopy {
std::atomic<int> counter{};
};
这确实是一个有效的方法,但是对原子变量的操作时同步的,使用std::atomic_ref<int> counter
可以让你显式的控制何时需要对原子变量进行原子访问,因为大多数时间里你可能只需要读取这个变量。