在多线程开发中,为了确保数据安全性,经常需要对数据进行加锁、解锁处理。C++11中引入了原子的概念,简而言之就是访问它时它自动加锁解锁,从而使软件开发更为简便。
原子可谓一个既简单又复杂的概念。简单到访问它时就跟单线程访问一块内存一样简单,复杂的地方在于它的实现涉及到各种内存模型,在优化中经常会遇到。
下面给出一个简单的原子示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #include <iostream> #include <thread> #include <atomic> using namespace std; atomic_int val = { 0 };//这个类型也可以写作 atomic<int> 用于表示整型数据的原子 void icrement () { for (int i = 0; i < 100000000; i++) { val++; } } int main (int argc, char* argv []) { //创建两个线程 thread t1 (icrement); thread t2 (icrement); //等待两个线程执行完 t1.join (); t2.join (); cout << val << endl; return 0; } |
经过十几秒左右的等待后,代码执行完毕,结果不出所料,200000000。简单的原子操作差不多就是这样,atomic模板可以包括任何类型,另外原子的操作也与它本身的操作方式基本相同,因为原子模板重载了所有的运算符。
简单的说完了,说说复杂的原子概念。
假如一个原子,它长这样
1 | atomic_int val = { 0 }; |
嗯,跟上面的相同。它实际上可以提供三种类型的操作:读、写、RMW(同时包括读写),通过三种类型的函数实现。
首先是读,比如 int i=val; 这样的代码,实际上是通过load函数实现。
1 | int i = val.load (memory_order_seq_cst); |
后面的参数代表内存顺序。这个的含义是顺序执行当前的原子操作。什么含义?含义就是,如果一个函数中对这个原子进行了多项操作,那么首先执行之前的原子操作,然后执行本条操作,最后执行之后的原子操作。说白了就是单线程的执行顺序。原子的操作过程并不是必须固定的,一个函数中如果有两条原子操作,那么首先执行后面操作,然后执行前面操作是完全可能的。这个在优化中经常会遇到。
然后是写操作,比如 val = i; 这样的操作
1 | val.store (i, memory_order_seq_cst); |
嗯,这儿也顺序执行,以免颠覆各位三观。
然后就是同时读写这样的操作了。 比如原子+=一个数之后同时可访问,通过compare_exchange这类函数实现。
然后,接下来说说内存访问模型了。一共有六种
1、memory_order_seq_cst 顺序执行,可用于读、写、RMW操作
2、memory_order_relaxed 乱序执行,可用于读、写、RMW操作
3、memory_order_acq_rel 首先执行之前的写操作,然后执行本条操作,然后执行之后的读操作,可用于RMW操作
4、memory_order_release 首先执行之前的写操作,然后执行本条操作,可用于写、RMW操作
5、memory_order_acquire 首先执行本条操作,然后执行后面的读操作,可用于读、RMW操作
6、memory_order_consume 首先执行本条操作,然后执行后面的读写操作,可用于读、RMW操作
基本的概念就是上面这些了,接下来动手实践吧