0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

深入了解互斥锁、条件变量、读写锁以及自旋锁

Linux爱好者 来源:果冻虾仁 作者:果冻虾仁 2021-11-01 10:02 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

锁是一个常见的同步概念,我们都听说过加锁(lock)或者解锁(unlock),当然学术一点的说法是获取(acquire)和释放(release)。

恰好pthread包含这几种锁的API,而C++11只包含其中的部分。接下来我主要通过pthread的API来展开本文。

mutex(互斥量)

mutex(mutual exclusive)即互斥量(互斥体)。也便是常说的互斥锁。

尽管名称不含lock,但是称之为锁,也是没有太大问题的。mutex无疑是最常见的多线程同步方式。其思想简单粗暴,多线程共享一个互斥量,然后线程之间去竞争。得到锁的线程可以进入临界区执行代码。

//声明一个互斥量
pthread_mutex_tmtx;
//初始化
pthread_mutex_init(&mtx,NULL);
//加锁
pthread_mutex_lock(&mtx);
//解锁
pthread_mutex_unlock(&mtx);
//销毁
pthread_mutex_destroy(&mtx);

mutex是睡眠等待(sleep waiting)类型的锁,当线程抢互斥锁失败的时候,线程会陷入休眠。优点就是节省CPU资源,缺点就是休眠唤醒会消耗一点时间。另外自从Linux 2.6版以后,mutex完全用futex的API实现了,内部系统调用的开销大大减小。

值得一提的是,pthread的锁一般都有一个trylock的函数,比如对于互斥量:

ret=pthread_mutex_trylock(&mtx);
if(0==ret){//加锁成功
...
pthread_mutex_unlock(&mtx);
}elseif(EBUSY==ret){//锁正在被使用;
...
}

pthread_mutex_trylock用于以非阻塞的模式来请求互斥量。就好比各种IO函数都有一个noblock的模式一样,对于加锁这件事也有类似的非阻塞模式。

当线程尝试加锁时,如果锁已经被其他线程锁定,该线程就会阻塞住,直到能成功acquire。但有时候我们不希望这样。

pthread_mutex_trylock在被其他线程锁定时,会返回特殊错误码。加锁成返回0,仅当成功但时候,我们才能解锁在后面进行解锁操作!

C++11开始引入了多线程库,其中也包含了互斥锁的API:std::muxtex

此外,依据同一线程是否能多次加锁,把互斥量又分为如下两类:

  • 是:称为『递归互斥量』recursive mutex ,也称『可重入锁』reentrant lock
  • 否:即『非递归互斥量』non-recursive mute),也称『不可重入锁』non-reentrant mutex

若同一线程对非递归的互斥量多次加锁,可能会造成死锁。递归互斥量则无此风险。C++11中有递归互斥量的API:std::recursive_mutex。对于pthread则可以通过给mutex添加PTHREAD_MUTEX_RECURSIVE 属性的方式来使用递归互斥量:

//声明一个互斥量
pthread_mutex_tmtx;
//声明一个互斥量的属性变量
pthread_mutexattr_tmtx_attr;

//初始化互斥量的属性变量
pthread_mutexattr_init(&mtx_attr);
//设置递归互斥量的属性
pthread_mutexattr_settype(&mtx_attr,PTHREAD_MUTEX_RECURSIVE);

//把属性赋值给互斥量
pthread_mutext_init(&mtx,&mutext_attr);

然而对于递归互斥量或者说可重入锁的使用则需要克制。Stevens大神生前在《APUE》中说『使用好它是十分tricky的,仅当没有其他解决方案时才使用』。

可重入锁这个概念和称呼的走俏多半是Java语言的功劳。

condition variable(条件变量)

请注意条件变量不是锁,它是一种线程间的通讯机制,并且几乎总是和互斥量一起使用的。所以互斥量和条件变量二者一般是成套出现的。比如C++11中也有条件变量的API:std::condition_variable

对于pthread:

//声明一个互斥量
pthread_mutex_tmtx;
//声明一个条件变量
pthread_cond_tcond;
...

//初始化
pthread_mutex_init(&mtx,NULL);
pthread_cond_init(&cond,NULL);

//加锁
pthread_mutex_lock(&mtx);
//加锁成功,等待条件变量触发
pthread_cond_wait(&cond,&mtx);

...
//加锁
pthread_mutex_lock(&mtx);
pthread_cond_signal(&cond);
...
//解锁
pthread_mutex_unlock(&mtx);
//销毁
pthread_mutex_destroy(&mtx)

pthread_cond_wait函数会把条件变量和互斥量都传入。并且多线程调用的时候条件变量和互斥量一定要一一对应,不能一个条件变量在不同线程中wait的时候传入不同的互斥量。否则是未定义结果。

关于是先解锁互斥量还是先进行条件变量的通知,是另外一个比较大的议题。有种论断说:先解锁互斥量再通知条件变量可以减少多余的上下文切换,进而提高效率。这种说法是基于一种实现假设:先通知条件变量,再解锁。

可能让其他等待条件变量的线程被唤醒了,但是此时互斥量还没解锁,从而再次陷入休眠。然而对于另外一些实现,比如Linux系统,则通过等待变形(wait morphing)解决了这一问题。所以先通知再解锁也没用问题。

另外在使用条件变量的过程中有个稍微违反直觉的写法:那就是使用while而不是if来做判断状态是否满足。这样做的原因有二:

  1. 避免惊群;
  2. 避免某些情况下线程被虚假唤醒(即没有pthread_cond_signal就解除了阻塞)。

比如半同步/半reactor的网络模型中,在工作线程消费fd队列的时候:

while(1){
if(pthread_mutex_lock(&mtx)!=0){//加锁
...//异常逻辑
}
while(!queue.empty()){
if(pthread_cond_wait(&cond,&mtx)!=0){
...//异常逻辑
}
}
autodata=queue.pop();
if(pthread_mutex_unlock(&mtx)!=0){//解锁
...//异常逻辑
}
process(data);//处理流程,业务逻辑
}

read-write lock(读写锁)

顾名思义『读写锁』就是对于临界区区分读和写。在读多写少的场景下,不加区分的使用互斥量显然是有点浪费的。此时便该上演读写锁的拿手好戏。

读写锁有一个别称叫『共享-独占锁』。不过单看『共享-独占锁』或者『读写锁』这两个名称,其实并未区分对于读和写,到底谁共享,谁独占。可能会让人误以为读写锁是一种更为泛化的称呼,其实不是。读写锁的含义是准确的:是一种 读共享,写独占的锁。

读写锁的特性:

  • 当读写锁被加了写锁时,其他线程对该锁加读锁或者写锁都会阻塞(不是失败)。
  • 当读写锁被加了读锁时,其他线程对该锁加写锁会阻塞,加读锁会成功。

因而适用于多读少写的场景。

//声明一个读写锁
pthread_rwlock_trwlock;
...
//在读之前加读锁
pthread_rwlock_rdlock(&rwlock);

...共享资源的读操作

//读完释放锁
pthread_rwlock_unlock(&rwlock);

//在写之前加写锁
pthread_rwlock_wrlock(&rwlock);

...共享资源的写操作

//写完释放锁
pthread_rwlock_unlock(&rwlock);

//销毁读写锁
pthread_rwlock_destroy(&rwlock);

其实加读锁和加写锁这两个说法可能会造成误导,让人误以为是有两把锁,其实读写锁是一个锁。所谓加读锁和加写锁,准确的说法可能是『给读写锁加读模式的锁定和加写模式的锁定』。

读写锁和互斥量一样也有trylock函数,也是以非阻塞地形式来请求锁,不会导致阻塞。

pthread_rwlock_tryrdlock(&rwlock)
pthread_rwlock_trywrlock(&rwlock)

C++11中有互斥量、条件变量但是并没有引入读写锁。而在C++17中出现了一种新锁:std::shared_mutex。用它可以模拟实现出读写锁。demo代码可以直接参考cppreference:

https://en.cppreference.com/w/cpp/thread/shared_mutex

另外多读少写的场景有些特殊场景,可以用特殊的数据结构减少锁使用:

  • 多读单写的线性数据。用数组实现环形队列,避免vector等动态扩张的数据结构,写在结尾,由于单写因而可以不加锁;读在开头,由于多读(避免重复消费)所以需要加一下锁(互斥量就行)。
  • 多读单写的KV。可以使用双缓冲(double buffer)的数据结构来实现。double buffer同名的概念比较多,这里指的是foreground 和 backgroud 两个buffer进行切换的『0 - 1切换』技术。比如实现动态加载(热加载)配置文件的时候。可能会在切换间隙加一个短暂的互斥量,但是基本可以认为是lock free的。

我一张口,你就会发现:无非是空间换时间的老套路了。

spinlock(自旋锁)

自旋之名颇为玄妙,第一次听闻常让人略觉高大。但和无数个好似『故意把简单概念复杂化』的计算机术语一样,自旋锁的本质简单的难以置信。

要了解自旋锁,首先了解自旋。什么是自旋(spin)呢?更为通俗的一个词是『忙等待』(busy waiting)。最最通俗的一个理解,其实就是死循环……。

单看使用方法和使用互斥量的代码是差不多的。只不过自旋锁不会引起线程休眠。当共享资源的状态不满足的时候,自旋锁会不停地循环检测状态。因为不会陷入休眠,而是忙等待的方式也就不需要条件变量。

这是优点也是缺点。不休眠就不会引起上下文切换,但是会比较浪费CPU。

//声明一个自旋锁变量
pthread_spinlock_tspinlock;

//初始化
pthread_spin_init(&spinlock,0);

//加锁
pthread_spin_lock(&spinlock);

//解锁
pthread_spin_unlock(&spinlock);

//销毁
pthread_spin_destroy(&spinlock);

pthread_spin_init函数的第二个参数名为pshared(int类型)。表示的是是否能进程间共享自旋锁。这被称之为Thread Process-Shared Synchronization。互斥量的通过属性也可以把互斥量设置成进程间共享的。pshared有两个枚举值:

  • PTHREAD_PROCESS_PRIVATE:仅同进程下读线程可以使用该自旋锁
  • PTHREAD_PROCESS_SHARED:不同进程下的线程可以使用该自旋锁

Linux上的glibc中这两个枚举值分别是01Mac上不是)。所以通常也会看到直接传0的代码。你可能觉得不使用宏,直接用数字硬编码不是一个好习惯。的确,妥妥的Magic Number,但还有一个有趣的事实你需要了解:并不是所有实现都支持自旋锁设置pshared。比如:

intpthread_spin_init(pthread_spinlock_t*lock,intpshared){
/*RelaxedMOisfinebecausethisisaninitializingstore.*/
atomic_store_relaxed(lock,0);
return0;
}

所以直接传0可能也无伤大雅。

自旋锁 VS 互斥量+条件变量 孰优孰劣?肯定要看具体的使用场景,(我好像在说片汤话)。当你不知道在你的使用场景下这两种锁该用哪个的时候,那就是用互斥量吧!

或者通过压测的判断,不过大多数时候我们好像并不需要这么一个pthread的自旋锁,知友们可以提供一些自旋锁的使用参考。

编辑:jq
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • cpu
    cpu
    +关注

    关注

    68

    文章

    11216

    浏览量

    222945
  • 数据
    +关注

    关注

    8

    文章

    7314

    浏览量

    93982
  • Mac
    Mac
    +关注

    关注

    0

    文章

    1125

    浏览量

    54870
  • 函数
    +关注

    关注

    3

    文章

    4406

    浏览量

    66838

原文标题:如何理解互斥锁、条件变量、读写锁以及自旋锁?

文章出处:【微信号:LinuxHub,微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    五大认证加冕!德施曼以“首创AI智能”重构智能竞争格局

    。这是德施曼继去年获得高端智能“连续三年全国销量第一、全球销量第一”以及“核心科技引领全球”权威认证之后,在权威机构背书认证中的另一个里程碑。中研世纪在《中国A
    的头像 发表于 09-20 10:43 653次阅读
    五大认证加冕!德施曼以“首创AI智能<b class='flag-5'>锁</b>”重构智能<b class='flag-5'>锁</b>竞争格局

    五大认证加冕!德施曼以“首创AI智能”重构智能竞争格局

    近日,智能领军品牌德施曼获得尚普咨询及中研世纪两大权威机构颁发的中国“首创AI智能”、“AI智能领导者”、“AI智能全国销量第一”、“AI智能锁好评率第一”、“AI智能
    的头像 发表于 09-20 10:41 936次阅读

    官网nrf24l01的例程demo会出现互斥报错是为什么?

    我在用nrf24L01官网下载的例程包,用stm32进行开发,串口助手显示互斥错误,有朋友遇到这个问题吗
    发表于 09-10 06:05

    什么是电磁控制板?24路控板的使用步骤概述

    电磁控制板是一种专用于控制电磁工作状态的电子装置,它可以接收指令并对电磁进行通断电操作,从而实现对相关设备的锁定和解锁控制。电磁本身依靠电流产生磁场作用力来吸附或释放
    的头像 发表于 07-14 16:26 569次阅读
    什么是电磁<b class='flag-5'>锁</b>控制板?24路<b class='flag-5'>锁</b>控板的使用步骤概述

    联世界,智启万家,华普微蓝牙智能解决方案

    密码、指纹、刷卡与多模组合等主流门锁形态,支持与手机APP、小程序、智能中控网关等多终端互联,助力客户快速实现智能门锁产品的量产落地。功能亮点手机蓝牙解锁搭载BLE5.0及以上
    的头像 发表于 06-26 15:46 481次阅读
    <b class='flag-5'>锁</b>联世界,智启万家,华普微蓝牙智能<b class='flag-5'>锁</b>解决方案

    WT3000A,电子 AI 智慧语音方案强势来袭!

    模组(WIFI+WT2606A语音芯片)或WT2606A单语音芯片电子 - AI 智慧语音解决方案,可满足您多样化产品需求!今天就带大家深入了解一下这款宝藏方案。
    的头像 发表于 05-28 15:52 543次阅读
    WT3000A,电子<b class='flag-5'>锁</b> AI 智慧语音方案强势来袭!

    存器的工作原理与作用

    存器(Latch)作为数字电路中的基本元件,扮演着存储单个比特信息的关键角色。其独特的功能和广泛的应用领域使其成为数字电路设计和实现的核心组成部分。本文将深入探讨存器的基本概念、工作原理、类型、主要作用
    的头像 发表于 02-03 16:53 4052次阅读

    电路工作原理 自电路与常开电路的区别

    。 基本组成与结构 自电路通常由电源开关、启动按钮、停止按钮、接触器以及相关的辅助常开触点组成。其中,接触器是自电路中的关键元件,它包含一个电磁线圈和若干个常开、常闭触点。 工作原理详解 当电机启动时,首先合上
    的头像 发表于 01-31 10:07 5422次阅读

    电路在智能家居中的运用

    一、自电路的基本原理 自电路,又称为自保持电路,是一种能够在电路中形成正反馈的电路结构。当电路中的某个条件满足时,电路能够自动保持当前状态,无需外部持续的控制信号。这种电路通常由一个触发器(如
    的头像 发表于 01-18 10:17 1331次阅读

    电路与非自电路的比较

    ,使得电路在一定条件下能够保持其状态的电路。这种电路在接收到初始信号后,即使初始信号消失,电路也能维持其状态,直到接收到新的信号。 工作原理 自电路通常包含一个或多个反馈回路,这些回路能够将输出信号的一部分反馈到
    的头像 发表于 01-18 10:11 1510次阅读

    电路模组选择指南

    1. 自电路模组的基本原理 自电路模组的核心是一个继电器,它包含一个控制线圈和一个或多个触点。当控制线圈通电时,继电器的触点会闭合,即使控制线圈断电,触点也会保持闭合状态,这就是所谓的自功能
    的头像 发表于 01-18 10:08 1184次阅读

    电路在工业自动化中的应用

    电路因其能够自动维持状态的特性,在工业自动化领域中扮演着重要角色。 1. 自电路的基本原理 自电路的核心在于一个反馈回路,该回路能够使电路在触发后保持其状态,直到外部条件改变。
    的头像 发表于 01-18 10:07 1325次阅读

    电路常见故障及解决方法

    电路因其便捷性和实用性,在工业和日常生活中得到了广泛应用。然而,由于各种因素,自电路可能会出现故障。 自电路原理 在讨论故障之前,首先需要了解
    的头像 发表于 01-18 10:05 3465次阅读

    电路的类型和特点

    在电子工程领域,自电路是一种常见的设计,它能够使电路在没有持续的触发信号的情况下保持其状态。这种电路的设计对于实现自动化控制和减少人为干预至关重要。 一、自电路的类型 自电路可以根据其工作原理
    的头像 发表于 01-18 10:03 1635次阅读

    电路如何设计

    电路的设计旨在实现电路在按下开关后能自动保持持续通电,直到按下其他开关使之断路为止的功能。以下是自电路设计的基本步骤和要点: 一、基本设计步骤 接入电源 : 将零线接入电路的指定端子(如十一号
    的头像 发表于 01-18 09:56 2820次阅读