【杂谈】从底层看锁的实现

如下内容针对互斥锁。数据结构

为何须要锁?

锁表明着对临界区的访问权限。只有得到锁的操做对象,才能进入临界区。分布式

锁的本质是什么?

锁的本质是一个数据结构(或者说是一个对象),这个对象内保留着描述锁所须要的必要信息。如当前锁是否已被占用,被哪一个线程占用。而锁的一些工具,函数库,实际上就是对一个锁对象的信息进行变动。函数

上锁操做    =>  尝试对锁对象的信息进行修改,若是修改为功,则程序继续向下执行,不然将暂时停留在此。(停留的方式有两种,一种是自旋反复尝试,另外一种是挂起等待唤醒)工具

解锁操做    =>  重置锁对象的信息。fetch

相似下面这样(注:这个例子不许确,后面会讲spa

typedef struct __lock_t {
    int flag;              //锁的状态 0-空闲, 1-被占用
} lock_t; 

void init(lock_t *mutex) { //初始化锁对象
    mutex->flag = 0;
}

void lock(lock_t *mutex) {
    while(mutex->flag == 1)
        ;// 自旋等待
    mutex->flag = 1;
}

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

锁信息的存储位置

一种是保留在进程内,因为操做系统提供的内存虚拟化,因此这个锁对象的内存空间,只能被当前进程访问。而且同一进程的线程能够共享内存资源。因此,这个锁对象只能被当前进程的线程所访问。操作系统

另外一种是将锁的信息保存在本机的其余应用中。例如本机没有开启外部访问的Redis。这样本机的多个应用就能够经过Redis中的这个锁的信息进行调度管理。线程

还有一种就是将锁的信息保存在其余机器中(或者本机开启外部访问的Redis中),这样其余电脑的应用也能够对这个锁进行访问,这就是分布式锁。code

对锁信息进行修改

存在的问题对象

前面有提到,前面的lock函数对锁信息的修改操做存在问题,咱们来看看问题到底出在哪里。假设,咱们的电脑只有一个CPU,这个时候有两个线程开始尝试获取锁。

 

这个程序的结果是,在线程B已经占用锁的时候,线程A还能获取到锁。这就不能知足"互斥锁"的定义,这段代码就不知足正确性。那么问题出在哪里呢?问题就在于判断和修改这两个操做没有原子性。

正如上面的例子那样,线程A刚执行完判断,还没来得及作修改操做,就发生了上下文切换,转而执行线程B的代码。切换回线程A的时候,实际上条件已经发生了变动。

硬件的支持

这个问题显然不是应用的代码可以解决的,由于上下文切换是OS决定的,普通应用无权干涉。可是硬件提供了一些指令原语,能够帮助咱们解决这个问题。这些原语有test-and-set、compare-and-swap、fetch-and-add等等,咱们能够基于这些原语来实现锁信息修改的原子操做。例如,咱们能够基于test-and-set进行实现:

//test-and-set的C代码表示
int TestAndSet(int *ptr, int new) {
    int old = *ptr; //抓取旧值
    *ptr = new; //设置新值
    return old; //返回旧值
}

typedef struct __lock_t {
    int flag;
} lock_t;

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

void lock(lock_t *lock) {
    //若是为1,说明原来就有人在用
    //若是不为1,说明原来没人在用,同时设置1,表面锁如今归我使用了
    while (TestAndSet(&lock->flag, 1) == 1) 
        ; //spin-wait (do noting)
}

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

为何这些指令不会被上下文切换所打断?

上下文切换实际上也是执行切换的指令。CPU执行指令是一条一条执行的,test-and-set对于CPU来讲就是一个指令,因此就算须要进行上下文切换,它也会先执行完当前的指令,而后再执行上下文切换的指令。

相关文章
相关标签/搜索