使用 x86_64 汇编写一个自旋锁

5 minute

一、理论分析

自旋锁,顾名思义,即自己不断旋转重复进行的锁,当多个线程访问同一资源时,为实现互斥访问,必须给目标资源加锁,此时只允许一个线程访问,此时其他线程无法访问,并且一直重复请求访问,直到该锁被释放。访问完资源的线程及时释放锁以供其他资源访问。

自旋锁可以通过比较替换算法实现:设锁为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,成功实现自旋与锁的释放。