如下内容针对互斥锁。数据结构
锁表明着对临界区的访问权限。只有得到锁的操做对象,才能进入临界区。分布式
锁的本质是一个数据结构(或者说是一个对象),这个对象内保留着描述锁所须要的必要信息。如当前锁是否已被占用,被哪一个线程占用。而锁的一些工具,函数库,实际上就是对一个锁对象的信息进行变动。函数
上锁操做 => 尝试对锁对象的信息进行修改,若是修改为功,则程序继续向下执行,不然将暂时停留在此。(停留的方式有两种,一种是自旋反复尝试,另外一种是挂起等待唤醒)工具
解锁操做 => 重置锁对象的信息。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来讲就是一个指令,因此就算须要进行上下文切换,它也会先执行完当前的指令,而后再执行上下文切换的指令。