锁的底层实现原理


title: 锁的底层实现原理
date: 2021-06-08 17:00:00
categories: 技术杂文
tags:
- 技术杂文


锁的基本思想

在并发编程中的一个基本问题是,希望原子执行一系列命令,但是由于单处理器上的中断,或者多处理器上的多线程并发执行,需要锁机制来解决这样的问题。

使用锁的方式如下:

lock_t mutex;
...
lock(&mutex);
/* 临界区代码 */
unlock(&mutex);

lockunlock函数的语义比较简单,调用lock尝试获取锁,如果没有其他的线程持有锁,则该锁是可用的,该线程会获得锁,从而进入临界区,这个线程被称为锁的持有者。

如果另外一个线程对相同的锁变量调用lock,因为锁被别的线程所持有,该调用不会返回,这个线程就无法进入临界区,直到锁的持有者释放了锁为止。

评价锁的实现机制

如何评价一种锁的实现机制的好坏?可以制定以下的一些标准:

  • 互斥性,即锁是否有效,是否能够阻止多线程进入临界区;
  • 公平性,即当锁可用时,是否每一个竞争线程有公平的机会抢到锁,从另一方面看,是否有竞争锁的线程会饿死;
  • 性能,即使用锁之后增加的时间开销。

锁的实现机制

机制1:控制中断

在早期,实现互斥的解决方案,就是在临界区关闭中断,这是为单处理器系统开发的,实现的伪代码如下:

void lock()
{
    disableInterrupts();
}

void unlock()
{
    enableInterrupts();
}

关闭中断与打开中断,使用的是特权的硬件指令。通过在进入临界区之前关闭中断,可以保证临界区的代码不会被中断,从而原子地执行。临界区代码执行完成后,重新打开中断,程序继续正常运行。没有了中断,线程可以确信其代码会一直执行下去,不会被其他的线程干扰。

但是该方案的缺点很多:

  • 该方案要求允许所有的线程都可以执行打开与关闭中断的特权操作,恶意的程序会滥用该机制,关闭中断后不在打开中断,从而独占处理器;
  • 该方案不支持多处理器;
  • 关闭中断会导致中断丢失,可能会导致非常严重的系统问题,比如磁盘IO操作完成,但是中断被关闭,CPU错过了这个事件,等待该IO操作的进程就无法被唤醒;
  • 该方案效率较低,现代CPU对关闭与打开中断的代码执行较慢。

在计算机的发展历史上,出于某些原因,研究人员在早期热衷于研究不依赖硬件支持的锁机制,从后来的结果证明,这些工作没有太多的意义,只需要很少的硬件支持,就可以实现更好的锁机制。

机制2:测试并设置指令

锁的机制实现,需要硬件的支持。最简单的硬件支持是测试并设置指令(test-and-set instruction),也被称为原子交换(atomic exchange),实现的伪代码如下:

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

硬件指令,可以实现原子执行以上的一系列操作,可以根据这个硬件指令,实现一个简单的自旋锁:

typedef struct lock_t
{
    int flag;
}lock_t;

void init(lock_t *lock)
{
    //0表示锁是可用的,1表示锁被其他线程占用
    lock->flag = 0;
}

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

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

理解这个锁机制为什么能够工作,可以假设某个线程正在运行,调用lock后,没有其他的线程持有锁,flag值为0,调用TestAndSet后,指令会原子地将flag设置为1,并返回0,线程则跳出while循环。

当某个线程已经持有锁,此时flag值为1,本线程调用lock尝试获取锁,TestAndSet会返回1,则本线程会在while循环中自旋,直到另一个线程释放锁,即将flag置为0为止。

机制3:比较并交换指令

硬件支持的另一个原语,是比较并交换指令(compare-and-swap或者compare-and-exchange)。该指令的伪代码为:

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

比较并交换指令的基本思路是,检测ptr指向的值是否与expected相等,若相等,则更新ptr指向的值为新的值,否则,什么也不做,指令返回ptr原先指向的值。

使用比较并交换指令,实现的锁机制可以为:

typedef struct lock_t
{
    int flag;
}lock_t;

void init(lock_t *lock)
{
    //0表示锁是可用的,1表示锁被其他线程占用
    lock->flag = 0;
}

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

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

该硬件指令实现的自旋锁,与机制2所实现的等价。CompareAndSwap(&lock->flag, 0, 1) 可以理解为,检查flag是否与0相等(0表示锁未被占用),若相等,则置flag为1,并返回flag的旧值0。

机制4:链接的加载和条件式存储指令

一些平台提供了链接的加载(load-linked)和条件式存储(store-conditional)指令,可以配合使用,来实现锁机制,指令的实现伪代码为:

int LoadLinked(int *ptr)
{
    return *ptr;
}

int StoreConditional(int *ptr, int value)
{
    if(no one has updated *ptr since the LoadLinked to this address)
    {
        *ptr = value;
        return 1; /* success */
    }
    else
    {
        return 0; /* failed to update */
    }
}

这里关键的是条件式存储指令,只有上一次加载的地址在期间都没有更新时,才会更新成功。

使用该指令实现的锁机制的伪代码为:

typedef struct lock_t
{
    int flag;
}lock_t;

void init(lock_t *lock)
{
    //0表示锁是可用的,1表示锁被其他线程占用
    lock->flag = 0;
}

void lock(lock_t *lock)
{
    while(1)
    {
        while(LoadLinked(&lock->flag) == 1)
        {
            ; /* 自旋,等待flag为0 */
        }

        if(StoreConditional(&lock->flag, 1) == 1)
        {
            return; /* 条件存储成功则获取锁,否则重新LoadLinked */
        }
    }
}

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

这里需要关注条件式存储是如何失败的。一个线程调用了lock,执行链接的加载指令,返回0,表示当前锁可用,但是在执行条件式存储之前,被调度程序中断了,另一个线程占用了CPU,也调用了lock,也执行了链接的加载指令,同样此时锁是可用,返回0。

现在两个线程都执行了链接的加载指令,将要执行条件式存储指令,重点是只有一个线程能够成功更新flag为1。先执行条件式存储指令的线程将会成功获取锁,而后执行条件式存储指令的线程将失败。

机制5:获取并增加指令

另一个硬件原语是获取并增加指令(fetch-and-add),该指令能够原子地返回特定地址的旧值,并将该旧值增加1。指令的伪代码为:

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

根据获取并增加指令,实现的锁机制可以为:

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)
    {
        ; /* 自旋 */
    }
}

void unlock(lock_t *lock)
{
    FetchAndAdd(&lock->turn);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容