原子,意味着不可切分的最小单元,程序中的原子操作指任务不可切分到更小的步骤。
原子性(atomic)是一个可见性的概念:
当我们称一个操作是atomic的,实际上隐含了一个对什么atomic的上下文。
注意:我们说的是从线程视角观察不到完成一半的状态,而并非不存在物理上的进度状态,它取决于你的观察视角。
比如说一个线程中被互斥锁保护的区域,对另一个线程是atomic的,因为从另一个线程视角来看,它没法进入临界区读到数据中间状态,但是对kernel而言却不是atomic的。
从线程视角只能观察到未做和已做两种状态,观察不到完成一半的状态,任务执行不会被中断,也不会穿插进其他操作。
原子性对多线程操作是一个非常重要的属性,因为它不可切分,所以,一个线程没法在另一个线程执行原子操作的时候穿插进去。
比如一个线程原子的写入共享数据,那么其他线程没有办法读到“半修改的数据”;同样,如果一个线程原子读取共享数据,那么它读取的是共享变量在那个瞬间的值,因此原子的读和写没有数据竞争(Data Race)。
原子操作常用于与顺序无关的场景。
原子指令指单一的不可再分的不可中断的被硬件直接执行的机器指令,原子指令是无锁编程的基石。
原子指令常被分成两类:
Store/Load指令
通常,一条简单的store/load机器指令是原子的,比如数据复制指令(mov)可以把内存位置的数据读取到CPU寄存器,相当于Load数据。
x86架构读/写“按数据类型对齐要求对齐的长度不大于机器字长的数据”是原子的。
那什么是数据类型对齐要求呢?
比如在x86_64架构LLP64系统上(LLP64指long、long long和pointer类型是64位的),只要int32类型数据满足放置在起始地址除4为0,int64/long类型数据满足起始地址除8为0,则该数据就是满足类型对齐要求,那么对它的读和写,都是原子的。
一字节的数据读写一定是原子的。
其实,Intel新CPU架构确保读写放置在一个Cache Line的数据(不大于机器字长),跨Cache Line的数据访问无法保证原子性。
C/C++编程中,变量和结构体会自动满足对齐要求,比如:
int i; void f() { long y; } struct Foo { int x; short s; void* ptr; } foo;
全局变量i会被放置在起始地址可以被4整除的内存位置,局部变量y会被放置在起始地址可以被8