1.原子操作
多线程下为了实现对临界区资源的互斥访问,最普遍的方式是使用互斥锁保护临界区。
然而,如果临界区资源仅仅是数值类型时,对这些类型c++提供了原子类型,通过使用原子类型可以更简洁的获得互斥保护的支持。
(1). 一个实例
#include
#include
#include
using namespace std;
atomic_llong total{0};
void func(int){
for(long long i = 0; i < 100000; ++i){
total += i;
}
}
int main(){
thread t1(func, 0);
thread t2(func, 0);
t1.join();
t2.join();
cout << total << endl;
return 0;
}
上述由于使用了atomic_llong
类型的原子变量,所以 total += i;
操作是具备互斥保护的。
(2). cstdatomic中的原子类型和内置类型对应表
原子类型名称 | 对应的内置类型名称 |
---|---|
atomic_bool | bool |
atomic_char | char |
atomic_schar | signed char |
atomic_uchar | unsigned char |
atomic_int | int |
atomic_uint | unsigned int |
atomic_short | short |
atomic_ushort | unsigned short |
atomic_long | long |
atomic_ulong | unsigned long |
atomic_llong | long long |
atomic_ullong | unsigned long long |
atomic_char16_t | char16_t |
atomic_char32_t | char32_t |
atomic_wchar_t | wchar_t |
(3).另一种使用原子类型的方式
使用std::atomic
模板类。
注意点:
a.该模板类不支持拷贝构造,移动构造,赋值运算符。
b.std::atomic
定义了到T
的类型转换函数。
(4).atomic类型及其相关的操作
操作 |
atomic_flag |
atomic_bool |
atomic_integral_type |
atomic |
atomic |
atomic |
Atomic |
---|---|---|---|---|---|---|---|
test_and_set |
Y |
||||||
clear |
Y |
||||||
is_lock_free |
y |
y |
y |
y |
y |
y |
|
load |
y |
y |
y |
y |
y |
y |
|
store |
y |
y |
y |
y |
y |
y |
|
exchange |
y |
y |
y |
y |
y |
y |
|
compare_exchange_weak+strong |
y |
y |
y |
y |
y |
y |
|
fetch_add,+= |
y |
y |
y |
||||
fetch_sub,-= |
y |
y |
y |
||||
fetch_or,|= |
y |
y |
|||||
fetch_and,&= |
y |
y |
|||||
fetch_xor,^= |
y |
y |
|||||
++,-- |
y |
y |
y |
y |
(5).使用atomic_flag
可自行实现自旋锁
#include
#include
#include
#include
using namespace std;
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void f(int n){
while(lock.test_and_set())
cout << "waiting from thread " << n << endl;
cout << "thread " << n << " starts working" << endl;
}
void g(int n){
cout << "thread " << n << " is going to start." << endl;
lock.clear();
cout << "thread " << n << " starts working" << endl;
}
int main(){
lock.test_and_set();
thread t1(f, 1);
thread t2(g, 2);
t1.join();
usleep(100);
t2.join();
return 0;
}
2.顺序一致性,内存模型
默认下,使用原子类型时,自然就是顺序一致的。即,指令实际被cpu执行的顺序,和高级语言中书写顺序是一致的。
有时,对某些并发场景,我们可能并不需要如此严格的限制,也能保证指令执行的正确性,我们可以借助顺序一致性,内存模型的显式控制来达到此目的。
一个实例
#include
#include
#include
using namespace std;
atomic a{0};
atomic b{0};
int ValueSet(int){
int t = 1;
a = t;
b = 2;
}
int Observer(int){
cout << "(" << a << ", " << b << ")" << endl;
}
int main(){
thread t1(ValueSet, 0);
thread t2(Observer, 0);
t1.join();
t2.join();
cout << "Got (" << a << ", " << b << ")" << endl;
return 0;
}
上述实例中线程t1依次对a,b执行赋值。线程t2依次读取a,b的值。
但从高级语言到处理器执行二进制指令的实际效果并不一定严格按上述预期的顺序来。
从高级语言到处理器执行二进制指令有两个阶段会影响指令实际执行的顺序:
(1). 编译阶段
编译器处于性能优化考虑,针对没有执行依赖的语句可能生成汇编代码时调整指令顺序。
上述实例在执行汇编时,线程t1中 int t = 1;a = t;
和b = 2;
没有依赖关系,所以,允许安排汇编语句时,b = 2;
对应的汇编语句在int t = 1;a = t;
对应的汇编语句之前或中间。线程t2中访问a
,访问b
类似。
顺序一致性指的是编译后的汇编指令顺序和高级语言中顺序是否一致。
(2).二进制指令执行阶段
假设编译器按高级语言一致顺序产生了如下汇编代码
1 Loadi reg3, 1; #将立即数1放入寄存器reg3
2 Move reg4, reg3; #将reg3的数据放入reg4
3 Store reg4, a; #将寄存器reg4中的数据存入内存地址a
4 Loadi reg5, 2; #将立即数2放入寄存器reg5
5 Store reg5, b; #将寄存器reg5中的数据存入内存地址b
处理器实际执行二进制指令时由于上述1,2,3
和4,5
没有依赖关系,所以某些cpu体系结构下,4,5
可能在1,2,3
之前或1,2,3
中间被执行。
这里,我们称严格按二进制指令顺序执行指令的cpu体系结构为强顺序
的,反之,则为弱顺序
的。
所以,内存模型是一个针对cpu体系结构的概念。
弱顺序体系结构下,保证指令执行顺序符合预期的手段是添加额外的汇编指令。
1 Loadi reg3, 1; #将立即数1放入寄存器reg3
2 Move reg4, reg3; #将reg3的数据放入reg4
3 Store reg4, a; #将寄存器reg4中的数据存入内存地址a
Sync
4 Loadi reg5, 2; #将立即数2放入寄存器reg5
5 Store reg5, b; #将寄存器reg5中的数据存入内存地址b
由于添加了额外的Sync
汇编指令,即使在弱内存cpu体系结构下执行上述汇编指令,也能保证先执行1,2,3
再执行4,5
。
像Sync
这样的汇编指令称为:内存栅栏。
3.高级语言如何保证指令执行顺序和预期(代码中出现顺序)一致
(1).编译阶段保证得到的汇编指令顺序和高级语言中一致。
(2).针对强顺序cpu
体系结构,无需额外处理。针对弱顺序cpu
体系结构,在汇编指令中额外插入内存栅栏。
默认情况下,使用原子操作时,上述(1),(2)均是满足的。
4.通过放松一致性要求来提高执行效率
c++的原子操作大多都可以使用memory_order作为一个参数。
c++11中memory_order所有可能取值:
枚举值 | 定义规则 |
---|---|
memory_order_relaxed | 不对执行顺序做任何保证 |
memory_order_acquire | 本线程中,所有后续读操作必须在本条原子操作完成后执行 |
memory_order_release | 本线程中,所有之前的写操作完成后才能执行本条原子操作 |
memory_order_acq_rel | memory_order_acquire + memory_order_release |
memory_order_consume | 本线程中,所有后续的有关本原子类型的操作,必须在本条原子操作完成后执行 |
memory_order_seq_cst | 全部存取操作都按顺序执行 |
memory_order_seq_cst
是c++11所有原子操作默认值。具备最强一致性要求。
通常,可把atomic的成员函数可使用的memory_order
分为三组:
(1). 原子存储操作(store)
memory_order_relaxed 、memory_order_release 、memory_order_seq_cst
(2).原子读取操作(load)
memory_order_relaxed、memory_order_consume、memory_order_acquire 、memory_order_seq_cst
(3).同时读写操作
全部六种
5.利用显式设置memory_order保证原子操作既快又对
的实例
5.1.默认版本
#include
#include
#include
using namespace std;
atomic a;
atomic b;
int Thread1(int){
int t = 1;
a = t;
b = 2;
}
void Thread2(int){
while(b != 2);
cout << a << endl;
}
int main(){
thread t1(Thread1, 0);
thread t2(Thread2, 0);
t1.join();
t2.join();
return 0;
}
上述t2
种预期打印出来的a
应该是1
。
原子操作默认下会保证严格的顺序一致性(编译层面,cpu体系执行层面),若我们希望维持预期下,放松一致性要求就需要通过显式设置memory_order
来达到目的。
5.2.一个一致性要求略低但保证符合预期的版本
#include
#include
#include
using namespace std;
atomic a;
atomic b;
int Thread1(int){
int t = 1;
a.store(t, memory_order_relaxed);
b.store(2, memory_order_release);
}
int Thread2(int){
while(b.load(memory_order_acquire) != 2);
cout << a.load(memory_order_relaxed) << endl;
}
int main(){
thread t1(Thread1, 0);
thread t2(Thread2, 0);
t1.join();
t2.join();
return 0;
}
t1
中memory_order_release
会保证a
的写入先执行,再执行b
的写入。
t2
中memory_order_acquire
会保证先读取b
,再读取a
。
上述两个限制下,我们知道t2
中a
将会符合预期。