操作系统--初探如何实现锁机制

目录

前言

关中断

硬件提供的原语

test-and-set

compare-and-swap

fetch-and-add

后言


前言

  锁对于我来说是一个很难理解的机制,因为底层知识了解不够。只是在Java层面用过其提供的synchronized关键字还有reentrantlocl。弱点专攻,所以我专门找了书籍去看了这方面的知识,由于本人能力有限,所以以下言论有错还请各位指出。

关中断

  用过锁的同学都知道,锁可以同步一段临界区(访问共享资源的代码段)。而同步如果不要管它的效果的话,最简单的理解就是:A在执行的时候,不允许任何线程抢过cpu,不管你是时钟中断,还是其他什么中断。那好,现在我们知道中断可以打断线程的执行。嗯?是不是想到什么了,我在A执行阶段过中断不久可以实现“锁”了么?答案是正确的(最早的互斥量的解决方案之一就是临界区关中断),但是下面的缺点可能让你打消这个念头!

临界区关中断实现互斥量优缺点

优点:

  1. 这个方法的好处就是简单。你当然不用想破了脑子去弄明白为什么这方法是可行的。没有了中断,线程就可以保证它执行的代码确实会执行,并且不会有其他线程会干扰它。

缺点:

  1. 这个方法要求我们允许任何调用线程取执行特权操作(即中断的开和关),而且还要信任这个功能不会被滥用。
  2. 这种方法在多处理器上行不通。如果多个线程运行在多个不同的CPU上,每个线程都试图进入相同的临界区,无论中断是否已经关了,线程还可以运行在其他的处理器上,并因此而进入了临界区。由于多处理器现在很普遍了,我们的通用方案必须要比这个方案好。

由于关中断在多处理器下行不通,那么系统设计这就开始研究为锁提供硬件支持。

硬件提供的原语

test-and-set

伪代码:

int TestAndSet(int *ptr, int new) {
      int old = *ptr; // fetch old value at ptr
      *ptr = new; // store ’new’ into ptr
      return old; // return the old value
}

含义:返回ptr指向的旧值,同时ptr的值更新为new。当然,关键是这一些列的操作是以原子形式执行的。

自旋锁实现伪代码

typedef struct __lock_t {
    int flag;
} lock_t;

void init(lock_t *lock) {
    // 0 indicates that lock is available, 1 that it is held
    lock->flag = 0;
}

void lock(lock_t *lock) {
    while (TestAndSet(&lock->flag, 1) == 1)
        ; // spin-wait (do nothing)
}

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

compare-and-swap

伪代码:

int CompareAndSwap(int *ptr, int expected, int new) {
    int actual = *ptr;
    if (actual == expected)
        *ptr = new;
    return actual;
}

含义:检测ptr指定地址的值是否与expected相等;如果相等,就将ptr指向的内存地址更新为new值。如果不等的话,什么都不做。最后都返回actual值,从而可以让调用compare-and-swap指令的代码知道成功与否。

 自旋锁实现伪代码

仅仅将之前test-and-set的lock()函数替换:

typedef struct __lock_t {
    int flag;
} lock_t;

void init(lock_t *lock) {
    // 0 indicates that lock is available, 1 that it is held
    lock->flag = 0;
}

void lock(lock_t *lock) {
     while (CompareAndSwap(&lock->flag, 0, 1) == 1)
        ; // spin-wait (do nothing)
}

fetch-and-add

伪代码:

int FetchAndAdd(int *ptr) {
     int old = *ptr;
     *ptr = old + 1;
     return old;
}

含义:原子地将某一地址的值加1。

 排队锁实现伪代码:

typedef struct __lock_t {
    int ticket;
    int turn;
} lock_t;

void lock_init(lock_t *lock) {
    lock->ticket = 0;
    lock->turn = 0;
}

void lock(lock_t *lock) {
    int myturn = FetchAndAdd(&lock->ticket);
    while (lock->turn != myturn)
        ; // spin
}

void unlock(lock_t *lock) {
    FetchAndAdd(&lock->turn);
}

优点:

 不像一个单独的值,这个方案里用了ticket和turn变量作为组合来构造锁。

其基本操作很简单:当一个线程希望获得锁时,它先对ticket值做一次原子地fetch-and-add操作;此时这个值就作为这个线程的”turn”(myturn)。

然后,全局共享变量lock->turn用来决定轮到了哪个线程;当对于某个线程myturn等于turn时,那就轮到了这个线程进入临界区。unlock简单地将turn值加1,由此下一个等待线程(如果存在的话)就可以进入临界区了。

注意这个方法相对于前面的几种方式的一个重要不同:它保证了所有线程的执行。

一旦某个线程得到了他自己的ticket值,在将来的某一时刻肯定会被调度执行(一旦前面的那些线程执行完临界区并释放锁)。

在先前的方案中,并没有这一保证;比如,某个自旋在test-and-set的线程可能会一直自旋下去,即使其他的线程获得、释放锁。

后言

显然操作系统实现的锁机制不可能仅仅用中断或者硬件提供的原语实现,肯定会有很多优化。

优化的两个方面:

  1. 公平。一旦锁空闲了,是否每个竞争该锁的线程都被公平对待?从另一个方式看的话,就是测试更极端的情况:是否有竞争该锁的线程处于饥饿状态而一直得不到锁?

  2. 性能。尤其是用锁后的增加的时间开销。这里有几个不同的情况值得考虑一下。

    一个是没有竞争的情况;当只有一个线程在运行、获取、释放锁,锁开销有多少?

    另一个是多线程在单CPU上竞争同一个锁的情况,是否需要担心其性能?

    最后一点,当引入多CPU,各个CPU上的多个线程竞争同一个锁的性能如何? 

参考:https://github.com/EmbedXj/OperatingSystems.ThreeEasyPieces

猜你喜欢

转载自blog.csdn.net/shuzishij/article/details/86486612