使用 x86_64 汇编写一个自旋锁
一、理论分析
自旋锁,顾名思义,即自己不断旋转重复进行的锁,当多个线程访问同一资源时,为实现互斥访问,必须给目标资源加锁,此时只允许一个线程访问,此时其他线程无法访问,并且一直重复请求访问,直到该锁被释放。访问完资源的线程及时释放锁以供其他资源访问。
自旋锁可以通过比较替换算法实现:设锁为1时被占用,为0时空闲。当一个线程请求锁时,即进入请求锁循环“spinlock”,设预期值为 0,修改值为 1,让锁值与预期值比较,若锁值等于预期值,则锁空闲,将锁值置为修改值,退出 spinlock 循环;若锁值不等于预期值,则证明锁被占用,继续 spinlock 循环。
为验证是否成功实现自旋,开启一个释放锁线程,请求锁线程自旋一段时间后,释放锁线程进行锁的释放,即把锁值置为预期值0。此时,请求锁线程成功获得锁并退出 spinlock 循环。
二、设计与实现
使用 x86_64 汇编实现自旋锁:
Intel 语法
1// 尝试获取锁
2void lock(long *p) {
3 long a = 0, c = 1;
4 printf("try to get lock...\n");
5 __asm__(
6 "push rax \n\t"
7 "push rcx \n\t"
8 "spin_lock: \n\t"
9 "mov rcx, %[c] \n\t"
10 "mov rax, %[a] \n\t"
11 // 比较并替换算法,若p==rax==0则获得锁并使p=rcx(==1),若p(==1)!=rax则进入自旋。
12 "lock cmpxchg %[p], rcx \n\t"
13 "jne spin_lock \n\t"
14 "pop rcx \n\t"
15 "pop rax \n\t"
16 : [p]"+m"(*p)
17 : [a]"r"(a), [c]"r"(c)
18 : "rcx", "rax"
19 );
20}
21// 释放锁
22void unlock(long *p) {
23 __asm__(
24 "mov %[p], 0; \n\t"
25 : [p]"+m"(*p)
26 );
27}
AT&T 语法
1void lock(long *p) {
2 long a = 0, c = 1;
3 printf("try to get lock...\n");
4 __asm__(
5 "pushq %%rax \n\t"
6 "pushq %%rcx \n\t"
7 "spin_lock: \n\t"
8 "movq %1, %%rcx \n\t"
9 "movq %2, %%rax \n\t"
10 "lock cmpxchg %%rcx, %0 \n\t"
11 "jne spin_lock \n\t"
12 "popq %%rcx \n\t"
13 "popq %%rax \n\t"
14 : "+m"(*p)
15 : "r"(c), "r"(a)
16 : "%rcx", "%rax"
17 );
18}
19void unlock(long *p) {
20 __asm__(
21 "movq $0, %0; \n\t"
22 : "+r"(*p)
23 );
24}
测试自旋锁
初始化锁值为 1,主线程尝试获取锁,进入自旋,子线程在一段时间后释放锁,锁值置为 0,接着,主线程获得锁并把锁置为 1。
1#include <stdio.h>
2#include <pthread.h>
3#include <unistd.h>
4
5// intel语法实现自旋锁 > gcc -pthread -masm=intel -o s spinlock.c
6// 尝试获取锁
7void lock(long *p) {
8 long a = 0, c = 1;
9 printf("try to get lock...\n");
10 __asm__(
11 "push rax \n\t"
12 "push rcx \n\t"
13 "spin_lock: \n\t"
14 "mov rcx, %[c] \n\t"
15 "mov rax, %[a] \n\t"
16 // 比较并替换算法,若p==rax==0则获得锁并使p=rcx(==1),若p(==1)!=rax则进入自旋。
17 "lock cmpxchg %[p], rcx \n\t"
18 "jne spin_lock \n\t"
19 "pop rcx \n\t"
20 "pop rax \n\t"
21 : [p]"+m"(*p)
22 : [a]"r"(a), [c]"r"(c)
23 : "rcx", "rax"
24 );
25}
26// 释放锁
27void unlock(long *p) {
28 __asm__(
29 "mov %[p], 0; \n\t"
30 : [p]"+m"(*p)
31 );
32}
33
34// 释放锁线程
35void *mythread(void* args) {
36 long* p = (long*) args;
37 // 推迟释放锁,此时自旋在进行中
38 sleep(2);
39 // 释放锁
40 unlock(p);
41 printf("after unlock: %ld\n", *p);
42}
43
44int main() {
45 long a = 1; // 设刚开始锁已被获取
46 long *p = &a;
47 // 开启一个用于释放锁的线程
48 pthread_t t1;
49 pthread_create(&t1, NULL, mythread, (void*)p);
50 printf("before lock: %ld\n", *p);
51 // 主线程尝试获取
52 lock(p);
53 pthread_join(t1, NULL);
54 printf("after lock: %ld\n", *p);
55 return 0;
56}
这里采用 intel 语法编写的自旋锁进行测试,执行命令 gcc -pthread -masm=intel -o s spinlock.c
进行编译。
若采用 AT&T,执行命令 gcc -pthread -o s spinlock.c
进行编译,无需 -masm=intel
,因为 gcc 底层默认采用 AT&T。
三、运行结果
运行结果如下:
1before lock: 1
2try to get lock...
3after unlock: 0
4after lock: 1
一开始锁值为 1,请求锁线程(即主线程)请求获得锁,进入自旋。2s 后释放锁线程进行锁的释放,接着请求锁线程成功获得锁,锁值又被置为 1,成功实现自旋与锁的释放。