本文为《C++ Concurrency in Action》 读书笔记,对其中的一些知识点进行总结。阅读这本书前建议先阅读《Effective C++》
原子类型或者说是原子操作,在操作系统的学习中我们已经很熟悉了,原子操作可以保证一个操作的完整性,即要做完,要么不做,对于原子操作,其相应的操作顺序或者访问内存顺序一般是确定了的,因此一般原子操作不会引起数据竞争,C++中的原子操作有很多,还可以用模板将自定义的操作设置为原子操作。有的原子操作其实现方式用锁,而有的实现方式不用锁,用锁的操作一般效率较低。我们可以使用is_lock_free()函数判断其是否有所锁,返回true则直接使用原子指令,返回false则使用锁完成。
在C++中,标准 原子类型 定义在头文件 < atomic > 中,通常,标准原子类型是不能拷贝和赋值,他们没有拷贝构造函数和拷贝赋值操作。
常用的原子类型:
对一个变量,可以采用多线程对其进行操作,但是我们必须为这个变量进行互斥的访问,我们要么枷锁,要么用原子操作,当然用原子将会具有更高的效率。
int a = 0;
mutex m;
void fun1() {
lock_guard<mutex> gm(m);
a++;
}
以上的函数中,a是共享变量,因此必须互斥的访问,但是,我们可以采用原子变量,采用原子变量或就不需要进行枷锁了,而且往往具有更高的效率。
atomic_int a = 0;
void fun2() {
a++;
}
std::atomic的相关操作以及内存顺序
最基本的原子整型类型就是 std::atomic< bool > 。虽然它依旧不能拷贝构造和拷贝赋值,但是你可以使用一个非原子的 bool类型构造它,所以它可以被初始化为true或false,并且你也可以从一个非原子bool变量赋 值给 std::atomic< bool > 的实例
std::atomic<bool> b(true);
b=false;
对于常用的操作符,c++内部操作函数大多数都已经已经实现,但是对于读写操作atomic有一组特殊读写操作的函数。而这两种操作将会涉及到内存序,实际cpu在执行指令时吧并不是按照我们程序中设想的的顺序进行执行,为了获得更好的性能,一般会对相应的指令进行重排序,所以我们的原子操作虽然对于单线程而言是有序执行,但是当涉及到多线程时,执行的效果可能不尽如意。关于指令优化的顺序可以阅读书籍《linux内核设计与实现》中的相关章节。
- store操作,对原子变量进行写如操作,其参数将会指定写的方式,具体将会在后面阐述。
- load操作,对原子变量进行读操作,其参数将会指定写的方式,具体将会在后面阐述。
c++中指定了6中内存顺序,使得对于c++代码的优化按照其他的顺序执行,也就是说,对于单个的线程而言,只要能够保证最后的运行结果能正确,c++可以对指令的执行进行重排。(关于内存排序的操作,主要参考书籍《深入理解C++ 11:C++11新特性解析和应用》)
memory_order_seq_cst
这个是默认的内存顺序,这表示所有的指令执行顺序按照代码的顺序执行,如果对于内存顺序不了解,则可以直接使用默认方式,这样的方式能够同步关系。
这个例子中我们直接使用赋值符号,相当于我们使用默认的内存排序,因此这段代码的输出值将永远是 1 ,同样的,我们也可以采用显示的调用store和load来进行操作
x.load(std::memory_order_seq_cst);
x.load();
x.store(std::memory_order_seq_cst);
x.store();
上述的几种方式效果一样,因为 memory_order_seq_cst 是原子操作在内存的默认顺序,即按照代码的顺序执行。
memory_order_relaxed
这种内存顺序使得单个线程在左后不影响及如果的情况下能过按照任意的顺序进行执行,
线程 t2 可能有多种输出结果。
但是对于最后的结果,我们无论如何都可以或者结果Got(1, 2), 因此,对于中间的执行顺序对最后的结果不会产生影响的情况下,我们可以指定这种方式,这将会提高程序的性能。
memory_order_acquire 和memory_order_release
std::memory_order_release
操作和store进行搭配,表示所有在该语句之前的原子写操作都不能被重排到该语句之后,也就是当这一条语句执行完毕后,该语句之前的所有原子写操作都已经完成。
`` memory_order_acquire ```操作和load进行搭配,所有在该语句之后的读操作都将不能重排到该语句的前面。
这样使得不同的线程之间的操作有了一定的顺序,因此,这样便在强顺序和无顺序之间有了一种新的方案。
memory_order_consume
memory_order_consume
内存栅栏(内存屏障)
内存屏障就像是一条线,使得在屏障前的操作不能被重排到屏障后的操作。内存屏障也需要和上面的6种内存序列搭配使用。
std::atomic_thread_fence(memory_order_relaxed),不能起到屏障作用
std::atomic_thread_fence(memory_order_release),防止屏障前的内操作重排到屏障的后面的写操作。
std::atomic_thread_fence(memory_order_acquire),防止屏障后的屏障操作重载到屏障前的任意load操作前。
#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
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
}
上述的 1 一定在 3 的前面先执行,4 一定在 6 的前面先执行,这样就能达到同步的效果。