关注公众号获取更多信息:
之前讲的都是理论相关的,下面详细讲一下我们现实中会使用到的内存模型。今天是最简单也是最宽松的内存模型----memory_order_relaxed
带标签 memory_order_relaxed 的原子操作无同步操作;它们不会在同时的内存访问间强加顺序。它们只保证原子性和修改顺序一致性。
例如,
对于最初为零的 x 和 y ,
// 线程 1 :
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// 线程 2 :
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D
允许产生结果 r1 == 42 && r2 == 42 ,因为即使线程 1 中 A 先序于 B 且线程 2 中 C 先序于 D ,却没有制约避免 y 的修改顺序中 D 先出现于 A ,而 x 的修改顺序中 B 先出现于 C 。D 在 y 上的副效应,可能可见于线程 1 中的加载 A ,同时 B 在 x 上的副效应,可能可见于线程 2 中的加载 C 。
即使使用宽松内存模型,也不允许“无中生有”的值循环地依赖于其各自的计算,例如,对于最初为零的 x 和 y , // 线程1: r1 = x.load(std::memory_order_relaxed); if (r1 == 42) y.store(r1, std::memory_order_relaxed); // 线程2: r2 = y.load(memory_order_relaxed); if (r2 == 42) x.store(42, std::memory_order_relaxed); 不允许产生结果 r1 == 42 && r2 == 42 ,因为存储 42 于 y 只在存储 42 于 x 后有可能,这又循环依赖于存储 42 于 y 。注意 C++14 前规范曾在技术上允许,但不推荐实现者如此实现。 | (C++14 起) |
宽松内存顺序的典型使用是计数器自增,例如 std::shared_ptr 的引用计数器,因为这只要求原子性,但不要求顺序或同步(注意 std::shared_ptr 计数器自减要求与析构函数进行获得释放同步)
下面举个例子:
#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> cnt = {0};
void f()
{
for (int n = 0; n < 1000; ++n) {
cnt.fetch_add(1, std::memory_order_relaxed);
}
}
int main()
{
std::vector<std::thread> v;
for (int n = 0; n < 10; ++n) {
v.emplace_back(f);
}
for (auto& t : v) {
t.join();
}
std::cout << "Final counter value is " << cnt << '\n';
}
输出:
Final counter value is 10000
那么在什么情景下使用memory_order_relaxed呢?
由于Relaxed ordering 仅仅保证load()
和store()
是原子操作,除此之外,不提供任何跨线程的同步。
如果某个操作只要求是原子操作,除此之外,不需要其它同步的保障,就可以使用 Relaxed ordering。程序计数器是一种典型的应用场景:
#include <cassert>
#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> cnt = {0};
void f()
{
for (int n = 0; n < 1000; ++n) {
cnt.fetch_add(1, std::memory_order_relaxed);
}
}
int main()
{
std::vector<std::thread> v;
for (int n = 0; n < 10; ++n) {
v.emplace_back(f);
}
for (auto& t : v) {
t.join();
}
assert(cnt == 10000); // never failed
return 0;
}