前面已经讲过两种锁了,现在我们先停一停,思考一下如果我们自己要实现一把锁该怎么做?
1. 思路
通常的一种思路就是设置一个共享标记,假设通过指针 lock 传递。然后去检查该 lock 指向的共享内存的值是否为 1. 代码像下面的样子:
void mylock(int *lock) {
int tmp = 1;
while (tmp == 1) {
tmp = *lock; // 如果 *lock 为 1,说明锁被占用,这时候会死循环。
*lock = 1; // 无论锁有没有被占用,这里都将共享内存的值置 1.
上面这段代码存在的问题就是竞争未加锁的 lock 的时候,如果两个线程同时竞争一把未上锁的 lock,此时 *lock == 0,如果其中一个线程执行完 tmp = *lock 的时候,切换到了另一个线程,另一个线程也执行了 tmp = *lock,这样,两边的线程都认为自己拿到了锁 (tmp == 0) 从而结束循环,于是产生错误。
所有,除非有一种办法,让 tmp = *lock; *lock = 1
一次性执行完毕而不会打断,那么上面的代码就完美了。
事实上,这种手段是存在的,只不过在 C 语言中没有办法,但是有汇编指令可以帮助我们做到,这条指令非常简单——xchg.
2. xchg 指令
它的格式如下:
主要用于交换 a 和 b 的值。在单核 cpu 中,汇编指令都是原子的,所以要么指令一次执行成功,要么不执行。在多核 cpu 中,为了防止竞争,只要在汇编指令前加一条前缀 lock 即可:
这样就可以防止这一条指令同时被多个 cpu 同时执行。
3. 解决问题
3.1 mylock 实现
tmp = *lock; *lock = 1
如果能写成
tmp = 1;
xchg(tmp, *lock);
那就相当美好了,可是 C 语言是禁止这种写法的。为了能完成同样的功能,不得不把第 1 节中的代码改成汇编的样子:
void mylock(int *lock) {
__asm__ __volatile__ ("1:\n\t"
"movl $1, %%eax\n\t"
"lock xchg %%eax, %0\n\t" /* 将 lock 中的值和 eax 进行交换, 相当于 eax = lock, lock = 1 */
"test %%eax, %%eax\n\t"/* 判断 eax 是否为 1 */
"jnz 1b" /* 如果为 1 则跳到标记 1 的地方继续执行 */
::"m"(*lock)
:"%eax"
上面代码中的 eax,就相当于前面代码中的 tmp 变量,只不过换成了寄存器。
这些代码看起来可能会有晕,一点一点解释:
- asm表示在 C 语言中内联汇编。
- volatile表示内存中的值可能产生变化,在编译的时候重新从该内存获取值而不是从寄存器中取该内存的备份。
- “1:\n\t” 表示位置标记,类似 C 语言中的 goto 语句的标记。这是方便指令”jnz 1b” 能够在满足条件的时候跳转到这里。
- $1 表示立即数 1,所以 “mov $1, %%eax\n\t” 表示置 eax 的内容为 1.
- %0 表示把后面的 “m”(*lock) 中的 *lock 的值放到这个位置。 “lock xchg %%eax, %0\n\t” 的功能前面已经讲过了,就是交换 eax 和 *lock 的值。
- test %%eax, %%eax 用于测试 eax 的值是否为 0,如果为 0,为将结果保存到 eflags 寄存器的 ZF 位。
- “jnz 1b” 表示如果 test 指令的结果为 1,就跳到 “1:\n\t” 这个地方。
3.2 myunlock 解锁实现
解锁的代码相当简单,一行就可以搞定:
void myunlock(int *lock) {
*lock = 0;
}
这个没什么好解释的了……
4. 功能测试
我们将之前的 luffy 和 allen 抢火车票的例子拿来修改修改,将互斥锁换成我们实现的锁。
程序 myspinlock 通过命令行传参数,传 0 表示不使用锁,传 1 表示使用我们自己写的锁。
4.1 程序清单
请原谅我很不要脸的把锁的名字取为 myspinlock,事实上,这就是后面我们要讲的自旋锁,虽然远远没有系统提供的自旋锁有那么多机制,那么复杂。相信自己,你已经知道自旋锁大概是什么东西了,下一篇文章我会正式的讲。
// myspinlock.c
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>
int uselock = 0;
int lock;
int tickets = 3;
void myspin_lock(int *lock) {
__asm__ __volatile__ ("1:\n\t"
"movl $1, %%eax\n\t"
"lock xchg %%eax, %0\n\t" /* 将 lock 中的值和 eax 进行交换, 相当于 eax = lock, lock = 1 */
"test %%eax, %%eax\n\t"/* 判断 eax 是否为 1 */
"jnz 1b" /* 如果为 1 则跳到标记 1 的地方继续执行 */
::"m"(*lock)
:"%eax" /* 表示在内联汇编中我用过了 eax 寄存器,通知一下编译器做相关处理以免冲突 */
);
}
void myspin_unlock(int *lock) {
*lock = 0;
}
void* allen(void* arg) {
int flag = 1;
while(flag) {
if (uselock)
myspin_lock(&lock);
int t = tickets;
usleep(1000*20);// 20ms
if (t > 0) {
printf("allen buy a ticket\n");
--t;
usleep(1000*20);// 20ms
tickets = t;
}
else flag = 0;
if (uselock)
myspin_unlock(&lock);
usleep(1000*20);// 20ms
}
return NULL;
}
void* luffy(void* arg) {
int flag = 1;
while(flag) {
if (uselock)
myspin_lock(&lock);
int t = tickets;
usleep(1000*20);
if (t > 0) {
printf("luffy buy a ticket\n");
--t;
usleep(1000*20);// 20ms
tickets = t;
}
else flag = 0;
if (uselock)
myspin_unlock(&lock);
usleep(1000*20);// 20ms
}
return NULL;
}
int main(int argc, char* argv[]) {
if (argc >= 2) uselock = atoi(argv[1]);
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, allen, NULL);
pthread_create(&tid2, NULL, luffy, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
4.2 编译和运行
$ gcc myspinlock.c -o myspinlock -lpthread
图1 不使用锁和使用锁的结果
5.总结
- 理解实现锁的关键步骤
- 理解 xchg 指令以及 lock 前缀的作用
练习:完成本文实验。
思考:我们实现的锁和互斥量的区别在哪里?(这很重要!!!)