在多线程编程中,咱们经常使用互斥锁来保证全局变量的线程安全,例如 pthread 中的 pthread_mutex,mach 中的 semaphore。他们经过 lock & unlock 或是 up & down 的方式来维护资源的状态,保证只有特定个数的线程能得到特定个数的资源。html
那么单单从软件层面可否真正的实现互斥锁呢?答案是否认的,由于不管如何,程序的互斥状态都须要存储在内存中,在多线程操做互斥状态时,是没法保证互斥状态的线程安全的。也就是说,必须经过硬件支持,同时在处理器指令集层面提供原语,才能实现真正的互斥,也就是题目中提到的 Exclusive。编程
本文将介绍 ARM 指令集中与 Exclusive 相关的指令,经过学习这些指令,你不只可以理解锁的本质和实现原理,还能掌握在汇编层面保证读写一致性的方法。安全
在 上一篇文章 中,咱们介绍了用于限制 CPU 乱序执行的内存屏障指令。相似的,Acquire 和 Release 也属于内存屏障,也是为了防止乱序执行带来逻辑错误。bash
Acquire 用于修饰内存读取指令,一条 read-acquire 的读指令会禁止它后面的内存操做指令被提早执行,即后续内存操做指令重排时没法向上越过屏障,下图[1]直观的描述了这一功能:多线程
Release 用于修饰内存写指令,一条 write-release 的写指令会禁止它上面的内存操做指令被滞后到写指令完成后才执行,即写指令以前的内存操做指令重排时午安向下越过屏障,下图[2]直观描述了这一功能: 并发
为了在硬件层面支持读写互斥,就须要判断一个地址是否已被其余处理器或核心修改,在 ARM 处理器中包含了被称为 Exclusive Monitor 的状态机来维护内存的互斥状态,从而保证读写一致性[2]。jsp
状态机的起始状态为 Open,对于某地址的 Load-Exclusive 读操做会将读取的地址标记为 Exclusive 状态;在对同一地址进行 Store-Exclusive 写操做会先检查 Monitor 是否处于 Exclusive 状态,若处于该状态则将内容写入,并将状态置为 Open,若是在写入前发现 Monitor 已经处于 Open 状态,说明有其余处理器或核心已经写入内容,本次写入失败。函数
简言之,Load-Exclusive 读指令将读取的地址标记为 Exclusive,Store-Exclusive 执行只能写入状态为 Exclusive 的地址,并在成功后将地址从新标记为 Open,经过这种方式便可保证多读单写,即保证了读取效率,又防止了多写带来的一致性问题。布局
对于多核的体系结构,ARM 将 Exclusive Monitor 分为 Local 和 Global 两种。post
若是内存地址被标记为 Nonshareable,则它的可见性被局限在处理器内,对于这类内存的互斥状态只须要维护在处理其内部的 Local Monitor 中。
Local Monitor 只在处理器内维护了状态,因为不涉及多多处理器的状态共享,不须要对真正的内存进行标记,所以它的硬件既能够经过对内存地址进行标记实现,也能够经过追踪指令的执行实现。这也要求不进行内存共享的代码在使用 Local Monitor 编程时不能以 Local Monitor 会对地址进行检查为前提[2]。
对于多处理器并发编程,能够经过在被标记为 Shareable 的内存单元中定义一个 mutex 信号量实现,为了保证 mutex 的多读单写,须要借助于全部处理器共享的硬件结构 Global Monitor,他会记录特定处理器对共享内存的 Exclusive 状态,从而保证多处理器并发时的多读单写。
Compare and Swap 简称为 CAS,是无锁编程中最经常使用的方式,它在修改某个共享的值 a
时,首先读取 a
的值,拷贝两份,分别存储为 pre_a
和 new_a
,将 new_a
的值进行修改,在将 new_a
写回到内存以前,先检查内存中的 a
是否等于 pre_a
,若等于则说明 a
的值未被他人修改,此时能够将 new_a
写入内存,不然说明当前读到的 a
已经不是最新的,写入失败。
显然,经过 CAS 和自旋锁搭配便可实现无锁的互斥写,可是 CAS 中的关键步骤 Compare & Swap 必须具备原子性,不然可能 Compare 时发现值未变化,但在 Compare 和 Swap 的间隙中有他人修改了值,从而致使多写。
上述基本概念中咱们介绍了三种概念,分别是 Acquire and Release
, Exclusive Monitor
和 Compare and Swap
,在汇编层面,他们都有特定的指令支持。
LDXR 即 LDR 的 Exclusive 版本,它的用法与 LDR 彻底一致,区别在于它含有 Load-Exclusive 语义,即将读取的内存单元状态置为 Exclusive。
STXR 即 STR 的 Exclusive 版本,因为须要是否 Store 成功,他相比于 STR 多了一个 32 位寄存器的参数用于接收执行结果,用法为:
STXR Ws, Xt, [Xn|SP{,#0}]
复制代码
即尝试将 Xt
写入 [Xn|SP{,#0}]
,若是写入成功则将 0 写入 Ws,不然将非 0 写入,它经常和 CBZ 指令搭配,若是写入失败则跳回到 LDXR,从新执行一遍 LDXR & STXR 操做,直至成功。
下面的例子给出了使用 LDXR & STXR 实现原子加一的过程:
; extern int atom_add(int *val);
_atom_add:
mov x9, x0 ; 备份 x0,为了失败时恢复
ldxr w0, [x9] ; 从val所在的内存中读取一个 int,并标记 Exclusive
add w0, w0, #1
stxr w8, w0, [x9] ; 尝试写回 val 位置,写入结果保存在 w8
cbz w8, atom_add_done ; 若是 w8 为 0 说明成功,跳到程序结束
mov x0, x9 ; 恢复备份的 x0,从新执行 atom_add
b _atom_add
atom_add_done:
ret
复制代码
一样的例子存在于 libkern 提供的 OSAtomicAdd32 函数:
;int32_t OSAtomicAdd32(int32_t __theAmount, volatile int32_t *__theValue);
ldxr w8, [x1]
add w8, w8, w0
stxr w9, w8, [x1]
cbnz w9, _OSAtomicAdd32
mov x0, x8
ret lr
复制代码
除了 Exclusive 语义外,LDXR & STXR 还有其 Acquire-Release 语义的 LDAXR & STLXR 版本,用于保证执行顺序。对于单纯的 Atomic Add 操做,前者已经足够;若是涉及到相似于 上一篇文章 提到的读写等待操做,则须要经过后者强保证不被乱序执行干扰。
ARM 提供了多条指令直接完成 Compare and Swap 操做,其中 CAS 是最基础的版本,它的使用方法以下[4]:
CAS Xs, Xt, [Xn|SP{,#0}] ; 64-bit, no memory ordering
复制代码
尝试将 Xt
与内存中的值进行交换,首先比较 Xs
是否等于内存中的 [Xn|SP{,#0}]
,若是相等则将 Xt
写入内存,同时将内存中的值写回到 Xs
,所以只要在 CAS 以后判断 Xs
是否等于 Xt
便可知道是否写入成功,若是写入失败则 Xs
的值应为原始值,即 Xs
≠ Xt
,若是写入成功则内存中的值已被更新,即 Xs
= Xt
。
下面的例子采用 CAS 方式一样实现了原子加一操做:
; extern int cas_add(int *val);
_cas_add:
mov x9, x0
ldr w10, [x9]
mov w11, w10 ; w11 is used to check cas status
add w10, w10, #1
cas w11, w10, [x9]
cmp w10, w11 ; if cas succeed, w11 = <new value in memory> = w10
b.ne _cas_add
mov w0, w10
ret
复制代码
注意:为了在 iOS 系统上编译包含 CAS 指令的内容,须要给 .s 文件添加一个 Compile Flag: -march=armv8.1-a
[5]。
一样的,CAS 也有其含有 Acquire-Release 语义的版本,分别是含有 Acquire 语义的 CASA, 含有 Release 语义的 CASL,和同时包含 Acquire-Release 两种语义的 CASAL。
你们若是想亲自实践和验证这些指令,能够复用下面给出的实验代码,本文上述代码大部分出自这些代码。
main.m 中的代码,可新建一个 iOS Project 并在 main.m 中添加这些代码:
// main.m
#include <pthread.h>
#define N 100
extern int atom_add(int *val);
extern int cas_add(int *val);
int as[10000] = {0};
int flags[10000] = {0};
int counter = 0;
void* pthread_add(void *arg1) {
int idx = *(int *)arg1;
// in this way will break the assert
// as[idx] += 1;
cas_add(as + idx);
__asm__ __volatile__("dmb sy");
atom_add(flags + idx);
return NULL;
}
void* pthread_end(void *arg1) {
int idx = *(int *)arg1;
while (flags[idx] != N);
assert(as[idx] == N);
printf("a = %d\n", as[idx]);
return NULL;
}
void test(int idx) {
printf("begin test %d\n", idx);
int n = N;
pthread_t threads[n + 1];
for (NSInteger i = 0; i < n; i++) {
int *copyIdx = calloc(1, 4);
*copyIdx = idx;
pthread_create(threads + i, NULL, &pthread_add, (void *)copyIdx);
}
for (NSInteger i = 0; i < n; i++) {
pthread_detach(threads[i]);
}
pthread_create(threads + n, NULL, (void *)pthread_end, (void *)(&idx));
pthread_detach(threads[n]);
}
int main(int argc, char * argv[]) {
printf("atom_add at %p\n", atom_add);
int round = 0;
while (true) {
test(round++);
}
// omit codes...
}
复制代码
两种方式实现原子加一的汇编代码,须要添加Compile Flag: -march=armv8.1-a
。
; exclusive.s
.section __TEXT,__text, regular, pure_instructions
.p2align 2
.global _atom_add, _cas_add
_atom_add:
mov x9, x0
ldxr w0, [x9]
add w0, w0, #1
stxr w8, w0, [x9]
cbz w8, atom_add_done
mov x0, x9
b _atom_add
atom_add_done:
ret
_cas_add:
mov x9, x0
ldr w10, [x9]
mov w11, w10 ; w11 is used to check cas status
add w10, w10, #1
cas w11, w10, [x9]
cmp w10, w11 ; if cas succeed, w11 = new value in memory = w10
b.ne _cas_add
mov w0, w10
ret
复制代码