Linux——线程3|线程互斥和同步

这篇具有很好参考价值的文章主要介绍了Linux——线程3|线程互斥和同步。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

加锁保护

我们上一篇提到过,多个线程执行下面代码可能会出错,具体原因可查看上一篇Linux博客。

Linux——线程3|线程互斥和同步
Linux——线程3|线程互斥和同步

为避免这种错误的出现,我们可采用加锁保护。

互斥锁

PTHREAD_MUTEX_INITIALIZER

Linux——线程3|线程互斥和同步

用pthread_mutex_t定义一把锁。ptherad_mutex_init是对锁进行初始化的函数。如果这把锁是全局的并且是静态定义的,我们可直接使用PTHREAD_MUTEX_INITIALIZER这样的话宏进行初始化,用这种方式进行初始化不需要init和destroy。

在这里,这把锁用来保护全局的tickets这一过程我们称为访问临界资源时对临界资源进行保护。

访问临界资源的代码被称作临界区,这里的临界区是函数里面if语句大括号里包含的内容

Linux——线程3|线程互斥和同步

pthread_mutex_lock

Linux——线程3|线程互斥和同步

返回值成功返回0,失败返回错误码。

Linux——线程3|线程互斥和同步

pthread_mutex_lock是进行加锁,出入的是刚才定义的锁

加了锁之后,每个线程都会进行加锁,加锁的特点是:任何一个时刻,只允许一个线程成功获得这把锁,其它没有拿到锁的线程进行阻塞等待,直到拿到锁的线程把锁给释放掉,其它线程才能进来。

Linux——线程3|线程互斥和同步

加锁之后,任何一个时刻,只允许一个线程执行这部分代码以及后面的解锁。即加锁后的代码只允许一个线程去执行,其它线程进行阻塞等待。

Linux——线程3|线程互斥和同步

pthread_mutex_unlock

pthread_mutex_unlock是用来进行解锁的,返回值成功返回0,失败返回错误码。

Linux——线程3|线程互斥和同步
Linux——线程3|线程互斥和同步

若把解锁写在这里,当tickets为0时候,执行break,此时直接退出while循环,未经过解锁,其它线程无法会一直阻塞

Linux——线程3|线程互斥和同步

因此,我们需要修改加锁位置,其中加锁和解锁之间的代码称为临界区,这里被共享访问的tickets就是临界资源。

Linux——线程3|线程互斥和同步

运行程序后,结果正确,但我们可以看到截图里是一个进程在抢,这是因为当某个线程抢完票后,释放了所,但该线程的优先级可能很高,进而又被调度会又被执行,

Linux——线程3|线程互斥和同步

我们让程序执行完后usleep一会

Linux——线程3|线程互斥和同步

此时可以获得我们想要的结果

Linux——线程3|线程互斥和同步

加锁的时候,一定要保证加锁的力度,越小越好,越细越好。如这里,尽量不要像下图这样加解锁,这段程序我们主要的核心还是tickets,这里我们可以把函数里面的打印语句,放到解锁后面,

Linux——线程3|线程互斥和同步

ptherad_mutex_init

若我们不想定义全局锁(全局变量的锁),若我们在main函数定义了一把锁,若要使用,就必须用pthread_mutex_init进行初始化。

第一个参数,这把锁的地址,第二个参数,这把锁的属性,我们一般设为空。

返回值成功返回0,失败返回错误码。

Linux——线程3|线程互斥和同步

pthread_mutex_destory

当我们不要在main函数定义的锁时,我们用pthread_mutex_destory

我们用局部的锁,对多个线程进行加锁

Linux——线程3|线程互斥和同步
Linux——线程3|线程互斥和同步
Linux——线程3|线程互斥和同步

进行一下小优化

Linux——线程3|线程互斥和同步
Linux——线程3|线程互斥和同步

思考题

  1. 加了锁之后,线程在临界区中,是否会切换,会有问题吗?

会切换,OS说了算,OS调度器决定是否切换。不会有问题,因为虽然被切换了,但是我们是持有锁被切换的,所以其它抢票线程要执行临界区代码,也必须先申请锁,锁它是无法申请成功的,因为锁在被切走的进程身上,所以,也不会让其它线程进入临界区,就保证了临界区中数据一致性。当一个线程,不申请锁,直接去访问临界资源,这种方式是错误的编码方式。

  1. 原子性在哪里体现?

目前来说,在没有持有锁的线程看来,对该线程最有意义的情况只有俩种:1.前面的线程未持有锁(什么都没做)2.前面的线程释放锁(做完)。因为此时该线程可以申请锁。

  1. 加锁就是串行执行了吗?

是的,执行临界区代码一定是串行的。

要访问临界资源,每一个线程都必须先申请锁,前提是每一个线程都必须看到同一把锁并且并且去访问它,锁本身就是一种共享资源。在上面代码中,锁保护了tickets,而谁又来保证锁的安全呢?所以,为了保证锁的安全,申请和释放锁必须是原子的。那么有如何保证原子性呢?我们继续往下看,锁是如何实现的。

互斥锁的实现

swap或exchange指令是以一条汇编的方式,将内存和CPU内寄存器数据进行交换。

如果我们在汇编的角度,如果只有一条汇编语句,我们就人为该汇编语句的执行是原子的。

加锁和解锁的代码

lock相当于pthread_mutex_lock(),unlock也一样

Linux——线程3|线程互斥和同步

lock过程:

在进程或线程角度,是如何看待CPU上寄存器的,CPU内部的寄存器,本质叫做当前执行流的上下文。寄存器的空间是被所有的执行流共享的,但是寄存器的内容是被每一个执行流私有的,因为这些是当前执行流的上下文。

%al是CPU中的一个寄存器,一开始把0放到该寄存器中,0虽然被放到了寄存器中,但0是某个线程的上下文,接下来进行交换xchgb,该语句只有一句,所以该语句是原子性的,这里的含义是把寄存器的值和锁的值做交换。交换玩之后去判断寄存器内容,如果>0,就返回,即锁申请成功,反之阻塞挂起。

该图右边是内存,mtx是锁。

Linux——线程3|线程互斥和同步

当执行完第一条语句之后,线程有可能被切换,线程被切走的时候是带着数据走的,走的时候带走了寄存器里放进去的0,当新线程来了之后,新线程照样从第一句语句开始执行(这是加锁操作必须的),新线程把0放了进来,也就是说此时俩个线程都有0,进而说明,寄存器是共享的,但寄存器里面的内容是私有的。

当第二个线程执行完第二条语句后,此时线程被切走,寄存器里现在是1,mtx里面是0,第二个线程被切走的时候要带走自己的上下文,即把1带走,之后第一个线程被切了回来,第一个线程走的时候带的是0,第一个线程回来之后在寄存器里面放了0,之后第一个线程继续执行语句,由于走的时候第一条语句已经执行完了,现在要执行第二条交换语句(此时内存里mtx是0),交换之后,寄存器里和mtx都是0(0和0交换),第一个线程做判断,发现寄存器里的值是0,这时候就把第一个线程挂起,挂起的时候第一个线程又把上下文数据0带走了,这时候恢复第二个线程,把1恢复到寄存器,由于走的时候完成了交换,所以现在进行判断操作,寄存器里内容>0,直接返回,此时申请锁成功,lock调用被返回pthread_mutex_lock也被返回,之后继续执行后续的代码。

交换这一行是真正的申请锁。这里1被所有的线程轮流式的拿到,而且这个数字1永远只有1个,而且

不会有任何新增的数据存在,这个1就是锁。

谁来保证锁的安全?

自己保证,通过一行汇编的方式来保证原子性。

锁的实现就是上面所述内容:简单来说通过内存数据和CPU寄存器数据进行交换。

可重入VS线程安全

可重入是针对函数来说的,一个函数被多个执行流重复进入的现象叫可重入,在重入期间如果该函数没问题,该函数就叫可重入函数。

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

常见的线程不安全的情况

  1. 不保护共享变量的函数

  1. 函数状态随着被调用,状态发生变化的函数

  1. 返回指向静态变量指针的函数

  1. 调用线程不安全函数的函数

常见的线程安全的情况

  1. 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的

  1. 类或者接口对于线程来说都是原子操作

  1. 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

  1. 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的

  1. 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构

  1. 可重入函数体内使用了静态的数据结构

常见可重入的情况

  1. 不使用全局变量或静态变量

  1. 不使用用malloc或者new开辟出的空间

  1. 不调用不可重入函数

  1. 不返回静态或全局数据,所有数据都有函数的调用者提供

  1. 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

  1. 函数是可重入的,那就是线程安全的

  1. 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

  1. 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的(例如抢票成程序,每个线程都在对全局变量做修改)。

可重入与线程安全区别

  1. 可重入函数是线程安全函数的一种

  1. 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

  1. 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

注意:如果一个函数是可重入的,它一定是线程安全的,如果是线程安全的,但不一定可重入。

死锁

我们在实际中不可能只用一把锁,我们可能会用多把锁,这里以俩把锁为例。

线程A要完成某些工作,申请锁1和锁2。线程B也需要俩把锁,先申请锁2,再申请锁1。我们单个申请一把锁是原子的,当一把锁申请完再去申请另一把锁,可能会出问题,如这里的线程A,B,俩个同时运行线程A拿锁1,线程B拿锁2,接下来线程A申请锁2,线程B申请锁1,此时就会出现问题,双方想要的锁都被对方拿到。此时出现了互相申请对方锁的情况,这种情况就叫死锁。

Linux——线程3|线程互斥和同步

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。当只有一把锁的时候,也有可能会产生死锁。

当我们把抢票程序里面解锁的地方,改为申请锁

Linux——线程3|线程互斥和同步
Linux——线程3|线程互斥和同步

此时程序卡在了这里

Linux——线程3|线程互斥和同步

死锁四个必要条件

  1. 互斥条件:一个资源每次只能被一个执行流使用

  1. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放

  1. 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺

  1. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁

  1. 破坏死锁的四个必要条件

  1. 加锁顺序一致

  1. 避免锁未释放的场景

  1. 资源一次性分配

避免死锁算法

  1. 死锁检测算法(了解)

  1. 银行家算法(了解)

pthrad_mutex_trylock

pthread_mutex_trylock() 是 pthread_mutex_lock() 的非阻塞版本。如果 mutex 所引用的互斥对象当前被任何线程(包括当前线程)锁定,则将立即返回该调用。否则,该互斥锁将处于锁定状态,调用线程是其属主。

Linux——线程3|线程互斥和同步

pthread_mutex_trylock() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。文章来源地址https://www.toymoban.com/news/detail-449771.html

到了这里,关于Linux——线程3|线程互斥和同步的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux——多线程,互斥与同步

    目录 一.linux互斥 1.进程线程间的互斥相关背景概念 2.互斥量mutex 3.加锁互斥锁mutex 4.锁的底层原理  二.可重入VS线程安全 1.概念 2.常见的线程不安全的情况 3.常见的线程安全的情况  4.常见不可重入的情况  5..常见可重入的情况 6.可重入与线程安全联系  三.死锁 1.死锁四个必

    2024年02月05日
    浏览(33)
  • Linux-线程的同步与互斥

    🚀 临界资源:多线程指行流共享的资源叫做临界资源。 🚀 临界区:每个线程内部访问临界资源的代码片段叫做临界区。 🚀 互斥:任何时刻,互斥保证只有一个指行流进入临界区,访问临界资源,通常是对临界区起保护作用。 🚀 原子性:不被任何调度所打断的操作,该

    2024年02月09日
    浏览(46)
  • 【Linux】多线程2——线程互斥与同步/多线程应用

    💭上文主要介绍了多线程之间的独立资源,本文将详细介绍多线程之间的 共享资源 存在的问题和解决方法。 intro 多线程共享进程地址空间,包括创建的全局变量、堆、动态库等。下面是基于全局变量实现的一个多线程抢票的demo。 发现错误:线程抢到负数编号的票,为什么

    2024年02月10日
    浏览(42)
  • 【关于Linux中----线程互斥与同步】

    先来用代码模拟一个抢票的场景,四个线程不停地抢票,一共有1000张票,抢完为止,代码如下: 执行结果如下: 可以看到,最后出现了票数为负数的情况,很显然这是错误的,是不应该出现的。 为什么会出现这种情况? 首先要明确,上述的几个线程是不能同时执行抢票的

    2023年04月08日
    浏览(42)
  • 【Linux】多线程 --- 线程同步与互斥+生产消费模型

    人生总是那么痛苦吗?还是只有小时候是这样? —总是如此 1. 假设现在有一份共享资源tickets,如果我们想让多个线程都对这个资源进行操作,也就是tickets- -的操作,但下面两份代码分别出现了不同的结果,上面代码并没有出现问题,而下面代码却出现了票为负数的情况,这

    2024年02月06日
    浏览(45)
  • 『Linux』第九讲:Linux多线程详解(三)_ 线程互斥 | 线程同步

    「前言」文章是关于Linux多线程方面的知识,上一篇是 Linux多线程详解(二),今天这篇是 Linux多线程详解(三),内容大致是线程互斥与线程同步,讲解下面开始! 「归属专栏」Linux系统编程 「主页链接」个人主页 「笔者」枫叶先生(fy) 「枫叶先生有点文青病」「每篇一句

    2024年02月02日
    浏览(72)
  • Linux pthread线程操作 和 线程同步与互斥操作

    在Linux系统中玩线程,使用pthread,这篇博客记录如何 创建线程 和 使用线程 和线程的 同步 与 互斥 。 还有一份nginx线程池的代码供大家阅读学习! 目录 一、简介 什么是线程 线程的优点、缺点 线程的应用场合 二、线程的使用 1.  创建线程 - pthread_create 2.  线程的终止 - pt

    2024年02月02日
    浏览(31)
  • 【Linux】多线程02 --- 线程的同步互斥问题及生产消费模型

    🍎 作者: 阿润菜菜 📖 专栏: Linux系统编程 线程同步互斥问题是指多线程程序中,如何保证共享资源的正确访问和线程间的协作。 因为线程互斥是实现线程同步的基础和前提,我们先讲解线程互斥问题。 在多线程中,假设我们有一个黄牛抢票的代码,其中有一份共享资源

    2024年02月08日
    浏览(42)
  • 一文搞定Linux线程间通讯 / 线程同步方式-互斥锁、读写锁、自旋锁、信号量、条件变量、信号等等

    目录 线程间通讯 / 线程同步方式 锁机制 互斥锁(Mutex) 读写锁(rwlock) 自旋锁(spin) 信号量机制(Semaphore) 条件变量机制 信号(Signal) 线程间通讯 / 线程同步方式 p.s 以下有很多段落是直接引用,没有使用 markdown 的 “引用” 格式,出处均已放出。 参考 / 引用: 100as

    2024年02月10日
    浏览(42)
  • 【Linux系统编程:线程】 线程控制 -- 创建、终止、等待、分离 | 线程互斥与同步 | 互斥量与条件变量 | 生产者消费者模型 | 线程池 | STL/智能指针与线程安全 | 读者写者模型

    写在前面 本文重点: 了解线程概念,理解线程与进程区别与联系。 学会线程控制,线程创建,线程终止,线程等待。 了解线程分离与线程安全。 学会线程同步。 学会使用互斥量,条件变量,posix 信号量,以及读写锁。 理解基于读写锁的读者写者问题。 一、线程概念 💦

    2024年02月04日
    浏览(68)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包