接上篇《??关于多线程同步的一切:伪共享??》原子是指不可分割的最小单位。程序中的原子操作意味着任务不能被分割成更小的步骤。原子性是一个可见性的概念:当我们称一个操作为原子时,它实际上暗示了一个什么是原子的上下文。注意:我们说的是线程视角观察不到的半完成状态,不是说没有物理进度状态,要看你的观察视角。例如,一个线程中互斥量保护的区域对于另一个线程来说是原子的,因为从另一个线程的角度来说,它不能进入??临界区读取数据的中间状态,但是对于内核来说就不是原子的了。从线程的角度来看,只能观察到未完成和完成两种状态,无法观察到半完成状态。任务执行不会被打断,也不会穿插其他操作。原子性对于多线程操作来说是一个非常重要的属性,因为它是不可分割的,所以一个线程不能穿插在另一个线程执行原子操作的时候。例如,如果一个线程原子地写入共享数据,其他线程无法读取“修改一半的数据”;类似地,如果一个线程原子地读取共享数据,它读取的是那一刻共享变量的值,所以原子读写不存在数据竞争。原子操作通常用于与顺序无关的场景。原子指令原子指令是指由硬件直接执行的单个不可分割、不可中断的机器指令。原子指令是无锁编程的基石。原子指令通常分为两类:store/loadread-modify-write(RMW)Store/Load指令store:将数据存储到内存,对应变量写入(赋值)load:从内存加载数据,对应变量读取通常,一个简单存储/加载机器指令是原子的。例如数据复制指令(mov)可以将内存位置的数据读到CPU寄存器中,相当于Loaddata。x86架构读/写“按照数据类型对齐且长度不大于机器字长的数据”是原子的。那么什么是数据类型对齐要求呢?比如在x86_64架构的LLP64系统上(LLP64是指long、longlong和指针类型都是64位的),只要int32类型的数据满足首地址除以4为0,int64/long类型的数据就满足首地址除以8为0,数据满足类型对齐要求,读写是原子的。一个字节数据的读写必须是原子的。事实上,Intel新的CPU架构保证了存储在一个CacheLine中的数据被读取和写入(不大于机器字长),跨CacheLine的数据访问不能保证原子性。在C/C++编程中,变量和结构会自动满足对齐要求,例如:inti;voidf(){longy;}structFoo{intx;短裤;void*ptr;}foo;全局变量i会被放置在起始地址能被4整除的内存位置,局部变量y会被放置在起始地址能被8整除的内存位置,结构体中的x成员会被放置在其起始地址可被4整除的内存Location。为了将ptr放在一个起始地址能被8整除的内存位置,编译器会在s后面加上padding,这样ptr也满足对齐要求。通过Cmalloc()接口动态分配的内存的返回值一般是8/16字节对齐。如果有更高的内存对齐要求,可以使用aligned_alloc(alignment,size)接口。C++中的alignas关键字用于设置结构或变量的对齐要求。对满足对齐要求且不大于机器字长的类型变量的赋值是原子的,不会有半完成(即只完成半字节赋值),对于阅读。注意:读写长度大于机器字长的数据不符合原子操作的特点。例如,在x86_64系统上,以下结构变量的读写是非原子的:structFoo{inta;诠释乙;intc;}foo1;voidset_foo1(constFoo&f){foo1=f;}foo1包含3个int成员,共12个字节,比机器字长大8个字节,所以它对`foo1不是原子的=f`。基于以上知识,我们知道有些getter/setter接口即使在多线程环境下也不需要加锁,如:structFoo{size_tget_x()const{//OKreturnx;}voidset_y(floaty){//OKthis->y=y;}size_tx;floaty;};intmain(){charbuf[8];Foo*f=(Foo*)buf;f->设置(3.14);//dang}但是,如果你将一块buf投射到Foo中,然后调用它的getter/setter,这是很危险的,可能会破坏上述对齐要求。如果将一个int变量编码成一个buf,最好使用memcpy而不是cast+assignment。Read-Modify-Write指令但有时,我们需要更复杂的操作指令,不仅仅是单一的读或写,它需要组合几个动作来完成某个任务。比如语句`++count`就对应了“读+修改+写”三个操作,但这三个操作并不是一个原子操作。因此,在多线程程序中使用++count,会导致多个执行流交错,导致计数错误(通常是结果小于预期值)。考虑另一种情况:读取+判断,我们来看看经典的单例实现:classSingleton{staticSingleton*instance;public:staticSingleton*get_instance(){if(instance==nullptr){instance=newSingleton;}返回实例;}};因为instance的判断和`instance=newSingleton`不是原子的,所以需要加锁:classSingleton{staticSingleton*instance;静态std::mutex互斥量;public:staticSingleton*get_instance(){mutex.lock();if(instance==nullptr)instance=newSingleton;mutex.unlock();返回实例;}};但是为了性能,更好的解决方案是添加双重检查,代码变成如下:staticSingleton*get_instance(){if(instance==nullptr){mutex.lock();if(instance==nullptr){//仔细检查instance=newSingleton;}mutex.unlock();返回实例;}returninstance;}首先检查,如果instance不为空,则直接返回instance,大多数时候会碰到这种情况,因为instance一旦创建,就不再为空了。如果实例为空,则加锁,然后第二次检查实例是否为空。为什么需要仔细检查?因为前面的检查通过后,有可能其他线程创建了实例,导致实例不再为空。一切似乎都进展顺利,高效周到。但是双重检查真的安全吗?这其实是一个很经典的问题。它有两个风险:首先,编写者没有告诉编译器它必须假设该实例可能被其他线程修改,因此编译器可能认为保留两个if之一就足够了。当然也可能不做这个优化,取决于编译器的策略,必须把实例改成volatile,告诉编译器两次读取都必须从内存中加载,避免double-check优化。就是上面说的原子性。实例指针不能保证存放在8字节对齐的地方,所以需要用std::atomic
