内存模型基础
- 基本结构,这个结构奠定了与内存相关的基础
- 并发
对象和内存位置
- 每一个变量都是一个对象,包括作为其成员变量的对象。
- 每个对象至少占有一个内存位置。
- 基本类型都有确定的内存位置(无论类型大小如何,即使他们是相邻的,或是数组的一部分)。
- 相邻位域是相同内存中的一部分。
对象、内存位置和并发
所有东西都在内存中。为了避免条件竞争,两个线程就需要一定的执行顺序。第一种方式,使用互斥量来确定访问的顺序;当同一互斥量在两个线程同时访问前被锁住,那么在同一时间内就只有一个线程能够访问到对应的内存位置,所以后一个访问必须在前一个访问之后。另一种方式是使用原子操作(atmic operations)同步机制,决定两个线程的访问顺序。当多于两个线程访问同一个内存地址时,对每个访问这都需要定义一个顺序。
当程序中的对同一内存地址中的数据访问存在竞争,你可以使用原子操作来避免未定义行为。当然,这不会影响竞争的产生——原子操作并没有指定访问顺序——但原子操作把程序拉回了定义行为的区域内。
修改顺序
每一个在C++程序中的对象,都有(由程序中的所有线程对象)确定好的修改顺序(modification order),在初始化开始阶段确定。在大多数情况下,这个顺序不同于执行中的顺序,但是在给定的执行程序中,所有线程都需要遵守这顺序。如果对象不是一个原子类型,你必要确保有足够的同步操作,来确定每个线程都遵守了变量的修改顺序。当不同线程在不同序列中访问同一个值时,你可能就会遇到数据竞争或未定义行为。如果你使用原子操作,编译器就有责任去替你做必要的同步。
这一要求意味着:投机执行是不允许的,因为当线程按修改顺序访问一个特殊的输入,之后的读操作,必须由线程返回较新的值,并且之后的写操作必须发生在修改顺序之后。同样的,在同一线程上允许读取对象的操作,要不返回一个已写入的值,要不在对象的修改顺序后(也就是在读取后)再写入另一个值。虽然,所有线程都需要遵守程序中每个独立对象的修改顺序,但它们没有必要遵守在独立对象上的相对操作顺序。
C++中的原子操作和原子类型
原子操作是一类不可分割的操作,当这样操作在任意线程中进行一半的时候,你是不能查看的;它的状态要不就是完成,要不就是未完成。如果从对象中读取一个值的操作是原子的,并且对对象的所有修改也都是原子的话,那么加载操作要不就会检索对象初始化的值,要不就将值存在某一次修改中。另一方面,非原子操作可能会被视为由一个线程完成一半的操作。如果这种是一个存储操作,那么其他线程看到的,可能既不是存储前的值,也可能不是已存储的值。如果非原子操作是一个加载操作,那么它可能会去检索对象的部分成员,或是在另一个线程修改了对象的值后,对对象进行检索;所以,检索出来的值可能既不是第一个值,也不是第二个值,可能是某种两者结合的值。这就是一个简单的条件竞争(如第3章所描述),但是这种级别的竞争会构成数据竞争,且会伴有有未定义行为。
标准原子类型
在C++中(大多数情况下)你需要一个原子类型去执行一个原子操作。
标准原子类型(atomic types):所有在这种类型上的操作都是原子的,虽然可以使用互斥量去达到原子操作的效果,但只有在这些类型上的操作是原子的(语言明确定义)。实际上,标准原子类型都很相似:它们大多数都有一个is_lock_free()成员函数,这个函数允许用户决定是否直接对一个给定类型使用原子指令(x.is_lock_free()返回true),或对编译器和运行库使用内部锁x.is_lock_free()返回false)。
只用
std
::
atomic_flag
类型不提供
is_lock_free()成员函数。
这种类型上的操作都需要是无锁的
(
lock-free
)。
在
std
::
atomic_flag
对象明确初始化后,做查询和设置
(使用
test_and_set()
成员函数),或清除(使用clear()成员函数)都很容易。这就是:无赋值,无拷贝,没有测试和清
除,没有其他任何操作。
对
于
std
::
atomic_flag
模板,使用对应的
T类型去特化模板的方式,要好于使用别名的方式。
std::atomic_flag的相关操作
最简单的标准原子类型,它表示了一个布尔标志。这个类型的对象可以
在两个状态间切换:设置和清除。它就是那么的简单,只作为一个构建块存在。我从未期待
这个类型被使用,除非在十分特别的情况下。
使用 std::atomic_flag实现自旋互斥锁:
#include <atomic>
class spinlock_mutex
{
std::atomic_flag flag;// = ATOMIC_FLAG_INIT;
public:
//初始化标志是“清除”,并且互斥量处于解锁状态。
//std::atomic_flag 类型的对象必须被ATOMIC_FLAG_INIT初始化,总是初始化为清除。
spinlock_mutex()
:flag{ ATOMIC_FLAG_INIT } //此处使用新标准统一的初始化方式,否则就是调用复制构造函数(已删除)
{}
void lock()
{
while (flag.test_and_set(std::memory_order_acquire));
//循环运行test_and_set(),直到旧值为false,当前线程设置为true
}
void unlock()
{
flag.clear(std::memory_order_release);
}
};
spinlock_mutex使用测试:
#include <atomic>
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
class spinlock_mutex;//使用上面的定义
struct test {
spinlock_mutex m;
int i;
};
void fun(test &t)
{
std::lock_guard<spinlock_mutex> lk(t.m);
++t.i;
std::cout << "thread : fun # " << t.i << std::endl;
}
int main()
{
test t;
std::lock_guard<spinlock_mutex> lk(t.m);
t.i = 0;
lk.~lock_guard();
std::vector<std::thread> vtds;
for (int i = 0;i < 10;++i)
{
vtds.push_back(std::thread{ fun,std::ref(t) });
}
for (auto & th : vtds)
{
th.join();
}
system("pause");
return 0;
}
由于
std
::
atomic_flag
局限性太强,因为它没有非修改查询操作,它甚至不能像普通的布尔
标志那样使用。所以,你最好使用
std
::
atomic
。
std::atomic的相关操作
std
::
atomic
:
最基本的原子整型类型就。
它有着比
std
::
atomic_flag
更加
齐全的布尔标志特性。虽然它依旧不能拷贝构造和拷贝赋值,但是你可以使用一个非原子的
bool类型构造它,所以它可以被初始化为
rue或
false,并且你也可以从一个非原子
bool变量赋
值给
std
::
atomic
的实例。
std::atomic<bool> f(true);
f = false;
store()是一个存储操作,而load()是一个加载操作。exchange()是一个“读-改-写”操作:
std::atomic<bool> b;
bool x = b.load(std::memory_order_acquire);
b.store(true);
x = b.exchange(false, std::memory_order_acq_rel);//x:true;b:false
/*std::memory_order_acq_rel:Reads as an acquire operation
*and writes as a release operation (as described above).
*/
std
::
atomic
<bool>
提供的exchange(),不仅仅是一个“读-改-写”的操作;它还介绍了一种新的
存储方式:当当前值与预期值一致时,存储新值的操作。
存储一个新值或旧值取决于当前值:
这是一种新型操作,叫做“比较交换”,它的形式表现为compare_exchange_weak()(会发生
“
伪失败”(
spurious failure
))
和
compare_exchange_strong()
成员函数。
“比较-交换”
操作是原子类型编程的基石;它比较原子
变量的当前值和提供的预期值,当两值相等时,存储预期值。当两值不等,预期值就会被更
新为原子变量中的值。
“
比较
交换
”
函数值是一个
bool
变量,当返回
true
时执行存储操作,当
false则更新期望值。(此处不是很懂)
std
::
atomic
和
std
::
atomic_flag
的不同之处在于,
std
::
atomic
不是无锁的;
为了保证操作的原子性,其实现中需要一个内置的互斥量。当处于特殊情况时,你可以使用
is_lock_free()成员函数,去检查
std
::
atomic
上的操作是否无锁。这是另一个,除
了
std
::
atomic_flag
之外,所有原子类型都拥有的特征。
std::atomic指针运算
fetch_add()
和fetch_sub()
的返回值与算数符号运算的返回值略有不同(
如果
x是
std
::
atomic
<
Foo
*>
类型的数组的首地址,
x.ftech_add(3)
让
指向第四个元素,并且函
数返回指向第一个元素的地址)
。这种操作也被称为“
交换-相加”
,并且这是一个原子的“
读-
改-
写”操作,如同exchange()和compare_exchange_weak()/compare_exchange_strong()一样。
正像其他操作那样,返回值是一个普通的
T* 值,而非是std::atomic<Foo*>
对象的引用,所以
调用代码可以基于之前的值进行操作:
#include <atomic>
#include <cassert>
class Foo {};
int main()
{
Foo some_array[5];
std::atomic<Foo*> p(some_array);
Foo* x = p.fetch_add(2); // p加2,并返回原始值
assert(x == some_array);
assert(p.load() == &some_array[2]);
x = (p -= 1); // p减1,并返回p值
assert(x == &some_array[1]);
assert(p.load() == &some_array[1]);
system("pause");
return 0;
}
p.fetch_add(3,std::memory_order_release);
标准的原子整型的相关操作
std
::
atomic
<int>
和
std
::
atomic
<unsigned
long
long
>
:
fetch_add(), fetch_sub()(返回旧值,非引用), fetch_and(), fetch_or(),
fetch_xor()
,还有复合赋值方式
((+=, -=(返回新值
,非引用
), &=, |=
和
^=)
,以及
++
和
--(++x, x++, --x
和
x--)
。
只有除法、乘法
和移位操作不在其中。因为,整型原子值通常用来作计数器,或者是掩码,所以以上操作的
缺失显得不是那么重要;如果需要,额外的操作可以将
compare_exchange_weak()
放入循环
中完成。
std::atomic<>主要类的模板
主模板的存在,在除了标准原子类型之外,允许用户使用自定义类型创建一个原子变量。
std
::
atomic
<
UDT
>
使用要求
(UDT
是用户定义类型,
保证“比较交换”操作能正常的工作
)
:
- 必须有拷贝赋值运算符 。即不能有任何虚函数或虚基类,以及必须使用编译器创建的拷贝赋值操作 。可以使用memcpy()进行拷贝。
- 必须是“位可比的”(bitwise equality comparable) 。可以使用memcmp()对位进行比较。
“
双字节比较和交
换”(
double-word-compare-and-swap
,
DWCAS
)
指令:
如果你的
UDT类型的大小如同
(或小于
)一个
int或
void*
类型时,大多数平台将会
对
std
::
atomic
<
UDT
>
使用原子指令,有些平台可能会对用户自定义类型
(两倍于int或 void* 的大小)特化的
std
::
atomic
<
>
使用原子指令。
限制:
- 浮点数的特化,存在相等却表达式不同的情况。
- UDT定义了与memcmp()不同的比较操作。
- 不能使用包含有计数器,标志指针和简单数组的类型,作为特化类型。
原子操作的释放函数
在不同的原子类型中
也有等价的非成员函数存在。大多数非成员函数的命名与对应成员函数有关,但是需
要
“atomic_”作为前缀(
比如,
std::atomic_load() )
。这些函数都会被不同的原子类型所重载。
在指定一个内存序列标签时,他们会分成两种:一种没有标签,另一种将“_explicit”作为后
缀,并且需要一个额外的参数,或将内存顺序作为标签,亦或只有标签
例
如,
std::atomic_store(&atomic_var,new_value) 与
std::atomic_store_explicit(&atomic_var,ne
w_value,std::memory_order_release
)。不过,原子对象被成员函数隐式引用,所有释放函数都
持有一个指向原子对象的指针
作为第一个参数
。
std
::
atomic_flag的操作:
std
::
atomic_flag_test_and_set
()
和
std
::
atomic_flag_test_and_set_explicit
()
。
C++标准库也对在一个原子类型中的 std::shared_ptr<> 智能指针类型提供释放函数。这打破
了“
只有原子类型,才能提供原子操作”
的原则
这里
std::shared_ptr<>
肯定不是原子类型。
但是,C++标准委员会感觉对此提供额外的函数是很重要的。可使用的原子操作有:load,
store, exchange
和compare/exchange
,这些操作重载了标准原子类型的操作,并且获取一
个
std::shared_ptr<>*
作为第一个参数:
std::shared_ptr<my_data> p;
void process_global_data()
{
std::shared_ptr<my_data> local = std::atomic_load(&p);
process_data(local);
}
void update_global_data()
{
std::shared_ptr<my_data> local(new my_data);
std::atomic_store(&p, local);
}
同步操作和强制排序
不同线程对数据的读写:
#include <atomic>
#include <vector>
#include <iostream>
#include <chrono>
#include <thread>
std::vector<int> data;
std::atomic<bool> data_ready(false);
void reader_thread()
{
while (!data_ready.load()) // 1
{
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
std::cout << "The answer=" << data[0] << std::endl; // 2
}
void writer_thread()
{
data.push_back(42); // 3
data_ready = true; // 4
}
int main()
{
std::thread t_reader{ reader_thread }, t_writer{ writer_thread };
t_reader.detach();
t_writer.detach();
system("pause");
return 0;
}
强制访问顺序是由对
std
::
atomic
<bool>
类型的data_ready变量进行操作完成的;
这些操作通
过“
先行发生
”(
happens-before
)和“
同步发生
”(
synchronizes-with
)确定必要的顺序。3先于4,1先于2,读写同步,于是强制顺序:写先于读。
同步发生
“
同步发生”
关系是指:只能在原子类型之间进行的操作。
:在变量
x进行适当标记的原子写操作
W,同步与对
进行适当标记的
原子读操作,读取的是
W操作写入的内容;或是在
W之后,同一线程上的原子写操作对
写入
的值;亦或是任意线程对
的一系列原子读
改
-写操作
(例如,
fetch_add()或
compare_exchange_weak())。
先行发生
“
先行发生”
关系是一个程序中,基本构建块的操作顺序;它指定了某个操作去影响另一个操
作。
对于参数中的函数调用顺序是未指定顺序的(结果一般为2,1,与环境相关):
void foo(int a, int b)
{
std::cout << a << " , " << b << std::endl;
}
int get_num()
{
static int i = 0;
return ++i;
}
int main()
{
foo(get_num(), get_num()); // 无序调用get_num()
system("pause");
return 0;
}
原子操作的内存顺序
三种内存模型(内存序列):
排序一致序列
(
sequentially consistent
),获取
-释放序列
(
memory_order_consume, memory_order_acquire, memory_order_release
和
memory_order_acq_rel
),和自由序列(
memory_order_relaxed
)。
排序一致队列:
程序中的行为从任意角度
去看,序列顺序都保持一致。
全序——序列一致:
#include <atomic>
#include <thread>
#include <cassert>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x()
{
x.store(true, std::memory_order_seq_cst); // 1
}
void write_y()
{
y.store(true, std::memory_order_seq_cst); // 2
}
void read_x_then_y()
{
while (!x.load(std::memory_order_seq_cst));
if (y.load(std::memory_order_seq_cst)) // 3
++z;
}
void read_y_then_x()
{
while (!y.load(std::memory_order_seq_cst));
if (x.load(std::memory_order_seq_cst)) // 4
++z;
}
int main()
{
x = false;
y = false;
z = 0;
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y);
std::thread d(read_y_then_x);
a.join();
b.join();
c.join();
d.join();
assert(z.load() != 0); // 5:可能是1、2,所以永远都不会触发
system("pause");
return 0;
}
序列一致是最简单、直观的序列,但是他也是最昂贵的内存序列,因为它需要对所有线程进
行全局同步。在一个多处理系统上,这就需要处理期间进行大量并且费时的信息交换。
非排序一致内存模型:
线程没必要去保证一致性。
在没
有明确的顺序限制下,唯一的要求就是,所有线程都要统一对每一个独立变量的修改顺序。
对不同变量的操作可以体现在不同线程的不同序列上,提供的值要与任意附加顺序限制保持
一致。
踏出排序一致世界后,最好的示范就是使用
memory_order_relaxed对所有操作进行约束。
自由序列:
在原子类型上的操作以自由序列执行,没有任何同步关系。在同一线程中对于同一变量的操
作还是服从先发执行的关系,但是这里不同线程几乎不需要相对的顺序。唯一的要求是,在
访问同一线程中的单个原子变量不能重排序;当一个给定线程已经看到一个原子变量的特定
值,线程随后的读操作就不会去检索变量较早的那个值。当使用
memory_order_relaxed,就
不需要任何额外的同步,对于每个变量的修改顺序只是线程间共享的事情。
非限制操作只有非常少的顺序要求(relaxed——任意):
#include <atomic>
#include <thread>
#include <cassert>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y()
{
x.store(true, std::memory_order_relaxed); // 1
y.store(true, std::memory_order_relaxed); // 2
}
void read_y_then_x()
{
while (!y.load(std::memory_order_relaxed)); // 3
if (x.load(std::memory_order_relaxed)) // 4
++z;
}
int main()
{
x = false;
y = false;
z = 0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load() != 0); // 5,可能会触发
system("pause");
return 0;
}
书上说可能会触发(虽然我循环跑了一次,没有触发,待研究,要与之后的计数员的笔记一块阅读),这里的关键点:
x和
y是两个不同的变量,所以这里没有顺序
去保证每个操作产生相关值的可见性。
非限制操作
——多线程版:
#include <thread>
#include <atomic>
#include <iostream>
std::atomic<int> x(0), y(0), z(0); // 1
std::atomic<bool> go(false); // 2
unsigned const loop_count = 10;
struct read_values
{
int x, y, z;
};
read_values values1[loop_count];
read_values values2[loop_count];
read_values values3[loop_count];
read_values values4[loop_count];
read_values values5[loop_count];
void increment(std::atomic<int>* var_to_inc, read_values* values)
{
while (!go)
std::this_thread::yield(); // 3 自旋,等待信号
for (unsigned i = 0;i<loop_count;++i)
{
values[i].x = x.load(std::memory_order_relaxed);
values[i].y = y.load(std::memory_order_relaxed);
values[i].z = z.load(std::memory_order_relaxed);
var_to_inc->store(i + 1, std::memory_order_relaxed); // 4
std::this_thread::yield();
/*Provides a hint to the implementation to
reschedule the execution of threads,
allowing other threads to run*/
}
}
void read_vals(read_values* values)
{
while (!go)
std::this_thread::yield(); // 5 自旋,等待信号
for (unsigned i = 0;i<loop_count;++i)
{
values[i].x = x.load(std::memory_order_relaxed);
values[i].y = y.load(std::memory_order_relaxed);
values[i].z = z.load(std::memory_order_relaxed);
std::this_thread::yield();
}
}
void print(read_values* v)
{
for (unsigned i = 0;i<loop_count;++i)
{
if (i)
std::cout << ",";
std::cout << "(" << v[i].x << "," << v[i].y << "," << v[i].z << ")";
}
std::cout << std::endl;
}
int main()
{
std::thread t1(increment, &x, values1);
std::thread t2(increment, &y, values2);
std::thread t3(increment, &z, values3);
std::thread t4(read_vals, values4);
std::thread t5(read_vals, values5);
go = true; // 6 开始执行主循环的信号
t5.join();
t4.join();
t3.join();
t2.join();
t1.join();
print(values1); // 7 打印最终结果
print(values2);
print(values3);
print(values4);
print(values5);
system("pause");
return 0;
}
强烈建议避免自由的原子操作,除非
它们是硬性要求的,并且在使用它们的时候需要十二分的谨慎。上面程序结果多种。由于给出的不直观的结果,复杂性增强。
获取
-
释放序列:
原子加载就是“获取”(acquire)操作
(memory_order_acquire),原子存储就是
“释放”操作
(memory_order_release),原子读-
改
-写
操作
(例如
fetch_add()或
exchange())在这里,不是
“获取
”,就是
“释放
”,或者两者兼有的操作
(memory_order_acq_rel)。这里,同步在线程释放和获取间,是成对的
(pairwise
)。释放操作
与获取操作同步,这样就能读取已写入的值。这意味着不同线程看到的序列虽还是不同,但
这些序列都是受限的。
获取(acquire——加载)-释放(release——存储)不意味着统一操作顺序:
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x()
{
x.store(true, std::memory_order_release);
}
void write_y()
{
y.store(true, std::memory_order_release);
}
void read_x_then_y()
{
while (!x.load(std::memory_order_acquire));
if (y.load(std::memory_order_acquire)) // 1
++z;
}
void read_y_then_x()
{
while (!y.load(std::memory_order_acquire));
if (x.load(std::memory_order_acquire))
++z;
}
int main()
{
x = false;
y = false;
z = 0;
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y);
std::thread d(read_y_then_x);
a.join();
b.join();
c.join();
d.join();
assert(z.load() != 0); // 3,可能触发
}
因为x和y是由不同线程写入,所以序列中的每一次释放到获取都不会影响到
其他线程的操作(书中所述:结果类似自由序列)。至于分析,理解能力有限,感觉是一次对应后再次获取就可能不是对应的值了(
https://www.zhihu.com/question/24301047),仍然待研究。
获取-释放操作会影响序列中的释放操作:
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y()
{
x.store(true, std::memory_order_relaxed); // 1
y.store(true, std::memory_order_release); // 2
}
void read_y_then_x()
{
while (!y.load(std::memory_order_acquire)); // 3,自旋,等待y被设置为true
if (x.load(std::memory_order_relaxed)) // 4
++z;
}
int main()
{
x = false;
y = false;
z = 0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load() != 0); // 5,不会触发
}
读到的y和写入是一样的,
因为存储y使用的是
memory_order_release,读取y使用的是
memory_order_acquire,存储就与读取就同步了。1先于2,则1先于3,故1先于4,所以不会触发。
与同步传递相关的获取
-
释放序列:通过获取和释放来控制同步。
使用获取和释放顺序进行同步传递:
#include <atomic>
#include <cassert>
#include <thread>
std::atomic<int> data[5];
std::atomic<bool> sync1(false), sync2(false);
void thread_1()
{
data[0].store(42, std::memory_order_relaxed);
data[1].store(97, std::memory_order_relaxed);
data[2].store(17, std::memory_order_relaxed);
data[3].store(-141, std::memory_order_relaxed);
data[4].store(2003, std::memory_order_relaxed);
sync1.store(true, std::memory_order_release); // 1.设置sync1
}
void thread_2()
{
while (!sync1.load(std::memory_order_acquire)); // 2.直到sync1设置后,循环结束
sync2.store(true, std::memory_order_release); // 3.设置sync2
}
void thread_3()
{
while (!sync2.load(std::memory_order_acquire)); // 4.直到sync1设置后,循环结束
assert(data[0].load(std::memory_order_relaxed) == 42);
assert(data[1].load(std::memory_order_relaxed) == 97);
assert(data[2].load(std::memory_order_relaxed) == 17);
assert(data[3].load(std::memory_order_relaxed) == -141);
assert(data[4].load(std::memory_order_relaxed) == 2003);
//以上实现了同步,不会触发
}
int main()
{
std::thread t1{thread_1}, t2{thread_2}, t3{thread_3};
t1.join();
t2.join();
t3.join();
return 0;
}
更改:
#include <atomic>
#include <cassert>
#include <thread>
std::atomic<int> data[5];
std::atomic<int> sync(0);
void thread_1()
{
data[0].store(42, std::memory_order_relaxed);
data[1].store(97, std::memory_order_relaxed);
data[2].store(17, std::memory_order_relaxed);
data[3].store(-141, std::memory_order_relaxed);
data[4].store(2003, std::memory_order_relaxed);
sync.store(1, std::memory_order_release);
}
void thread_2()
{
int expected = 1;
while (!sync.compare_exchange_strong(expected, 2,//保证thread_1对变量只进行一次更新
std::memory_order_acq_rel))//同时进行获取和释放的语义
expected = 1;
/*当expected与sync相等时候,sync=2,返回true,结束程序;
*当expected与sync不等的时候,expected=sync,返回false,
进入循环,expected=1;
*/
}
void thread_3()
{
while (sync.load(std::memory_order_acquire)<2);
assert(data[0].load(std::memory_order_relaxed) == 42);
assert(data[1].load(std::memory_order_relaxed) == 97);
assert(data[2].load(std::memory_order_relaxed) == 17);
assert(data[3].load(std::memory_order_relaxed) == -141);
assert(data[4].load(std::memory_order_relaxed) == 2003);
}
int main()
{
std::thread t1{thread_1}, t2{thread_2}, t3{thread_3};
t1.join();
t2.join();
t3.join();
return 0;
}
关于
compare_exchange_weak、
compare_exchange_strong:
http://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange。
获取
-
释放序列和
memory_order_consume
的数据相关性:
memory_order_consume
是“
获取-
释放”
序列模型的一部分,
它完
全依赖于数据,并且其展示了与线程间先行关系
的不同之处。数据依赖:
前序依赖
dependency-ordered-before
)和携带依赖
carries-a-dependency-to
)。
使用
std
::
memory_order_consume
同步数据:
#include <string>
#include <atomic>
#include <thread>
#include <cassert>
struct X
{
int i;
std::string s;
};
std::atomic<X*> p;
std::atomic<int> a;
void create_x()
{
X *x = new X;
x->i = 42;
x->s = "hello";
a.store(99, std::memory_order_relaxed); // 1
p.store(x, std::memory_order_release); // 2
}
void use_x()
{
X* x;
while (!(x = p.load(std::memory_order_consume))) // 3
std::this_thread::sleep_for(std::chrono::microseconds(1));
assert(x->i == 42); // 4,不会触发
assert(x->s == "hello"); // 5,不会触发
assert(a.load(std::memory_order_relaxed) == 99); // 6,可能触发
}
int main()
{
std::thread t1(create_x);
std::thread t2(use_x);
t1.join();
t2.join();
return 0;
}
存储p
的操作标记为
emory_order_release,加载p
的操作标记为
emory_order_consume,这就意味着p存储
仅先行那些需要加载
的操作。
对
x变量操作
的表达式对加载p
的操作携带有依赖。
显式打破依赖链
:
int global_data[] = {...};
std::atomic<int> index;
void f()
{
int i = index.load(std::memory_order_consume);
do_something_with(global_data[std::kill_dependency(i)]);
//让编译器知道这里不需要重新读取该数组的内容
}
std
::
kill_dependency
(
)
是一个简单的函数模
板,其会复制提供的参数给返回值,但是依旧会打破依赖链。
你必须记住,这是为了优化,所以这
种方式必须谨慎使用,并且需要性能数据证明其存在的意义(待研究)。
释放队列与同步
使用原子操作从队列中读取数据:
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
std::vector<int> queue_data;
std::atomic<int> count;
void populate_queue()
{
unsigned const number_of_items = 20;
queue_data.clear();
for (unsigned i = 0;i<number_of_items;++i)
{
queue_data.push_back(i);
}
count.store(number_of_items, std::memory_order_release); // 1 初始化存储
}
void consume_queue_items()
{
while (true)
{
int item_index;
if ((item_index = count.fetch_sub(1, std::memory_order_acquire)) <= 0)
// 2 一个“读-改-写”操作
{
wait_for_more_items(); // 3 等待更多元素
break;
}
process(queue_data[item_index - 1]); // 4 安全读取queue_data
}
}
int main()
{
std::thread a(populate_queue);
std::thread b(consume_queue_items);
std::thread c(consume_queue_items);
a.join();
b.join();
c.join();
system("pause");
return 0;
}
栅栏
栅栏操作会对内存序列进行约束,
使其无法对任何数据进行修改,典型的做法是与使用
memory_order_relaxed约束序的原子操
作一起使用。栅栏属于全局操作,执行栅栏操作可以影响到在线程中的其他原子操作。因为
这类操作就像画了一条任何代码都无法跨越的线一样,所以栅栏操作通常也被称为“内存栅栏”(memory barriers)。
栅栏可以让自由操作变的有序:
#include <atomic>
#include <thread>
#include <cassert>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y()
{
x.store(true, std::memory_order_relaxed); // 1
std::atomic_thread_fence(std::memory_order_release); // 2,释放栅栏
y.store(true, std::memory_order_relaxed); // 3
}
void read_y_then_x()
{
while (!y.load(std::memory_order_relaxed)); // 4
std::atomic_thread_fence(std::memory_order_acquire); // 5,获取栅栏
if (x.load(std::memory_order_relaxed)) // 6
++z;
}
int main()
{
x = false;
y = false;
z = 0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load() != 0); // 7,不会触发
}
这两个栅栏都是必要
的:你需要在一个线程中进行释放,然后在另一个线程中进行获取,这样才能构建出同步关
系。
栅栏同步依赖于读取
/写入的操作发生于栅栏之前
/后,但是这里有一点很重要:同步
点,就是栅栏本身。
如果采用如下方式,则也许会触发,失去了栅栏将两个释放操作隔开。
void write_x_then_y()
{
std::atomic_thread_fence(std::memory_order_release);
x.store(true, std::memory_order_relaxed);
y.store(true, std::memory_order_relaxed);
}
原子操作对非原子的操作排序
使用非原子操作执行序列:
#include <atomic>
#include <thread>
#include <assert.h>
bool x = false; // x现在是一个非原子变量
std::atomic<bool> y;
std::atomic<int> z;
void write_x_then_y()
{
x = true; // 1 在栅栏前存储x
std::atomic_thread_fence(std::memory_order_release);
y.store(true, std::memory_order_relaxed); // 2 在栅栏后存储y
}
void read_y_then_x()
{
while (!y.load(std::memory_order_relaxed)); // 3 在#2写入前,持续等待
std::atomic_thread_fence(std::memory_order_acquire);
if (x) // 4 这里读取到的值,是#1中写入
++z;
}
int main()
{
x = false;
y = false;
z = 0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load() != 0); // 5 断言将不会触发
}
其中y必须是原子操作,否则会数据竞争,而对x不用(栅栏作用)。
不仅是栅栏可对非原子操作排序。你在上面
看到
memory_order_release/memory_order_consume对,也可以用来排序非原子访问,为的是可
以动态分配对象,并且本章中的许多例子都可以使用普通的非原子操作,去替代标记为m
emory_order_relaxed的操作。
后记
在本章的学习中,可以说是举步维艰,我觉得要先看《深度探索C++对象模型》等书,对内存模型有一个初步的认识,看来还要努力学习。同时,书中部分代码没有完全明白,今后需要重温。文中文字部分摘抄至原翻译作品以及自己的部分感想总结。