互斥锁mutex的简单实现

mutex通常用于为一段代码加锁,以保证这段代码的原子性(atomic)操做,即:要么不执行这段代码,要么将这段代码所有执行完毕。算法

例如,最简单的并发冲突问题就是一个变量自增1:并发

balance = balance + 1;

表面看这是一条语句,但是在背后的汇编中咱们能够看到,指令集操做过程当中会引入中间变量来保存右边的值,进而这个操做至少会被扩充为:函数

int tmp = balance + 1;
balance = tmp;

这就须要一把互斥锁(mutual exclusive, mutex)将这段代码给锁住,使其达到任何一个线程“要么所有执行上述代码,要么不执行这段代码”的效果。这个用法能够表示为:atom

lock_t mutex;
...
lock(&mutex)
    balance = balance + 1;
unlock(&mutex);

那么,一个天然的问题即是,我如何实现上面的这个lock()函数呢?线程

乍一看这个问题是很是复杂的,特别是考虑到它可以被适用于各类代码的各类状况。但通过各类简化,这个lock()实现,能够经过几个test和set的组合得以实现。code

例如,it

typedef struct __lock_t { int flag; } lock_t;

void init(lock_t *mutex) {
    // 0: lock is available
    // 1: lock is held
    mutex->flag = 0;
}

void lock(lock_t *mutex) {
    while (mutex->flag == 1) {  // Test the flag.
        ;    // Wait the lock
    mutex->flag = 1;  // Set the lock, i.e. start to hold lock
}

void unlock(lock_t *mutex) {
    mutex->flag = 0;
}

我第一次看到这个算法的时候很是惊讶,一个原本极其复杂的问题就这么优雅地被解决了。它仅仅涉及到对条件的检验和变量的复制,而后整个问题就这么垂手可得地被攻破了。class

固然,我并没能看到上述代码的“坑”,也便是必须依靠指令集级别的支持才能真正作到atomic。这一样说明了并发程序的困难,稍微不注意便会调入一个万劫不复的坑里,而且你还不知道哪里出错了。thread

上述极端优雅的代码,有一个隐藏的坑,那即是在lock()函数的实现里,while循环那一段实际上是能够被乱入的。test

假设thread A是第一个运行到此的线程,那么它获得的mutex->flag就确定是0,因而它继续跳出循环往下运行,但愿经过下面的mutex->flag = 1来持有锁,使得其它线程在检测while循环时为真,进而进入循环的等待状态。

可若是在A执行到这个赋值为1的语句以前,又有另一个thread B运行到了这个while循环部分,因为mutex->flag还未被赋值为1,B一样能够跳出while,从而跟A同样拿到这把锁!这就出现了冲突。

那怎么办呢?仔细后能够发现,其实关键问题就在于:

  • mutex->flag的检测
  • mutex->flag的赋值

这两个操做必须是不被干扰的,也就是它必须是atomic的,要么这两段代码不被执行,要么这两段代码被不中断地完整执行。

这就须要借助CPU指令集的帮助,来保证上述两条语句的atomic操做,也便是著名的TestAndSet()操做。

int TestAndSet(int *ptr, int new) {
    int old = *ptr;
    *ptr = new;
    return old;
}

CPU的指令集,并不须要支持繁复的各类atomic操做。仅仅支持上面这个函数,各类互斥加锁的情形,便都可以被涵盖。

此时,在回到咱们最开始的那个优雅的lock()实现,就能够将其改造为:

typedef struct __lock_t { int flag; } lock_t;

void init(lock_t *lock) {
    // 0: lock is available
    // 1: lock is held
    mutex->flag = 0;
}

void lock(lock_t *mutex) {
    while (TestAndSet(&lock_t->flag, 1) == 1) {
        ;
}

void unlock(lock_t *lock) {
    lock->flag = 0;
}

上述代码极其精巧。乍一看在lock()实现里不是还缺乏一行mutex->flag = 1;么?可其实呢,它已经被整合到了TestAndSet()函数中。

这样的支持TestAndSet()的实现,即是最简单的spin lock,弹簧锁。之因此叫弹簧锁,那是由于在各种锁当中,弹簧锁就是最初的被投入工业使用的最简单的实现技术。

相关文章
相关标签/搜索