【linux线程(二)】线程互斥与线程同步

💓博主CSDN主页:杭电码农-NEO💓

⏩专栏分类:Linux从入门到精通⏪

🚚代码仓库:NEO的学习日记🚚

🌹关注我🫵带你学更多操作系统知识
  🔝🔝


在这里插入图片描述

Linux线程

  • 1. 前言
  • 2. 多线程互斥相关背景概念
  • 3. 多线程互斥详解
  • 4. 互斥锁的接口使用
  • 5. 死锁相关概念
  • 6. 线程安全和可重入的关系
  • 7. 线程同步基本概念
  • 8. 线程同步的接口使用
  • 9. 总结以及拓展

1. 前言

如果你不了解线程的基本概念,请你先
移步上一篇文章: 线程基本概念

本章重点:

本篇文章着重讲解线程互斥以及线程
同步的相关概念,以及如何实现它们.
周边概念包括临界资源,原子性,互斥量
等也会在本文当中提及


2. 多线程互斥相关背景概念

在学习互斥前,需要先补充一些相关概念:

  • 临界资源:多线程执行流共享的资源就叫做临界资源
  • 临界区:每个线程内部,访问有临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互,多个线程并发的操作共享变量,会带来一些问题

比如说我们最常见的高铁售票系统,可以把买票操作分为三步: 第一步: 判断现在还有无车票.第二步: 乘客付款后,钱包金额减少. 第三步: 乘客获得一张车票,高铁的总票数减一.多个执行流执行这三步时可能会出现问题,如下图:

在这里插入图片描述

可以写一段代码来验证上面的情况:

// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{char *id = (char*)arg;while ( 1 ) {if ( ticket > 0 ) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;} else {break;}}
}
int main( void )
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, "thread 1");pthread_create(&t2, NULL, route, "thread 2");pthread_create(&t3, NULL, route, "thread 3");pthread_create(&t4, NULL, route, "thread 4");//等待线程结束pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

发现多次执行这段代码得到的结果可能不同
为什么会出现不同的结果?


3. 多线程互斥详解

为啥上面可能会出现多种结果?
是有多种原因在里面的:

  1. if 语句判断条件为真以后,代码可以并发的切换到其他线程
  2. usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
  3. - -ticket操作本身就不是一个原子操作

这里有一个问题,为什么- -ticket操作不是原子的?其实我们鉴定一个操作是不是原子性的可以查看这个操作的汇编代码,若汇编代码只有一条,则我们认为这个操作是原子的,反之则这个操作不是原子性的,可以来看看减减的汇编代码是有三条:

在这里插入图片描述
在这里插入图片描述

要解决上面的问题,需要满足以下条件:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

本质上就是需要一把锁, 互斥锁

在这里插入图片描述

任何一个时间,只允许一个线程获得这把锁并且继续向后执行,没拿到锁的线程默认只能在加锁处阻塞等待其他线程释放掉锁才能继续往后走,多个线程来竞争一把锁,它们的关系就是互斥


4. 互斥锁的接口使用

互斥锁的使用一般分为四个步骤:

  1. 初始化互斥锁
  2. 在到达临界区前加锁
  3. 在跑完临界区代码后解锁
  4. 用完互斥锁后进行销毁

第一步: 初始化互斥锁

方法一, 静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法二, 动态分配

在这里插入图片描述

第二步和第三步: 加解锁

在这里插入图片描述

调用pthread_ lock 时,可能会遇到以下情况:

  1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  2. 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,
    那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

第四步: 销毁互斥锁

在这里插入图片描述

所以现在可以更改一下售票系统:

int ticket = 100;
pthread_mutex_t mutex;//全局
void *route(void *arg)
{char *id = (char*)arg;while ( 1 ) {pthread_mutex_lock(&mutex);if ( ticket > 0 ) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;pthread_mutex_unlock(&mutex);// sched_yield(); 放弃CPU} else {pthread_mutex_unlock(&mutex);break;}}
}
int main( void )
{pthread_t t1, t2, t3, t4;pthread_mutex_init(&mutex, NULL);pthread_create(&t1, NULL, route, "thread 1");pthread_create(&t2, NULL, route, "thread 2");pthread_create(&t3, NULL, route, "thread 3");pthread_create(&t4, NULL, route, "thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_mutex_destroy(&mutex);
}

5. 死锁相关概念

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。这样干说可能有点抽象,举个例子:

在这里插入图片描述

形成死锁的必要条件:

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

所以大家在写代码时,要避免写出死锁


6. 线程安全和可重入的关系

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

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

所以说,重入实际上比线程安全更加严格
下面是常见的不可重入的情况:

在这里插入图片描述

可重入和线程安全的联系和区别:

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

7. 线程同步基本概念

在多线程下并发的跑一段有加锁的代码,确实不会出现由于原子性而导致的不可预估的问题,但是也不代表这种方案就没有缺点了,想象一下以下的场景: 一共有6个线程在并发的执行一段代码,假如不做人为的干涉,当一个线程释放锁之后,下一个拿到锁的线程完全是不可预测的,也就是随机的,并且在加解锁这里,有一个可以称为就近原则的东西,就是说1号线程释放锁后,它此时距离锁最近,下一次获取锁可能还是1号线程,这就会导致其他线程虽然被创建出来了,但是并没有被调用,浪费的资源

不仅如此,当一个线程持有锁来到临界区后,可能临界区资源并没有就绪,比如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中.有没有一种方法可以让线程在资源不就绪时不要频繁的检测,一旦资源就绪就通知线程来访问资源?

要解决上面的问题,本质就是要线程同步
而线程同步坚持使用条件变量来实现

在这里插入图片描述


8. 线程同步的接口使用

使用条件变量通常分为四步:

  1. 初始化条件变量
  2. 利用条件变量等待资源就绪
  3. 资源就绪后唤醒线程来访问
  4. 使用完后销毁条件变量

第一步:

pthread_cond_t cond;//定义变量后再初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

在这里插入图片描述
第二步:

在这里插入图片描述

进行资源等待的前提是要拿到锁进入到临界区,所以wait函数既要你的条件变量,也要你的互斥锁,因为在后期资源就绪时,你这个线程是持有锁醒来的,继续该你执行

第三步:

在这里插入图片描述

broadcast函数是唤醒所有的线程,而signal是唤醒一个线程

第四步:
在这里插入图片描述
写一个简单的案例:

pthread_cond_t cond;
pthread_mutex_t mutex;
void *r1( void *arg )
{while ( 1 ){pthread_cond_wait(&cond, &mutex);printf("线程被成功唤醒\n");}
}
void *r2(void *arg )
{while ( 1 ) {sleep(5);//等待一会儿再唤醒线程pthread_cond_signal(&cond);sleep(1);}
}
int main( void )
{pthread_t t1, t2;pthread_cond_init(&cond, NULL);pthread_mutex_init(&mutex, NULL);pthread_create(&t1, NULL, r1, NULL);pthread_create(&t2, NULL, r2, NULL);pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);
}

9. 总结以及拓展

总的来说,多线程场景下还是比较容易出现各种错误的,所以在编写多线程的代码时,一定要对底层足够熟悉,对互斥锁以及条件变量要有一定的理解才能解决问题

一定要注意条件变量是在加解锁之间使用的


🔎 下期预告:生产者消费者模型 🔍

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/540270.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Java、Android面试]_03_java内存管理:虚拟内存、堆、垃圾回收

本人今年参加了很多面试&#xff0c;也有幸拿到了一些大厂的offer&#xff0c;整理了众多面试资料&#xff0c;后续还会分享众多面试资料&#xff0c;感兴趣的朋友可收藏关注&#xff0c; 现分享如下&#xff1a; 文章目录 1. Java虚拟机运行时数据区2. Java堆3. 垃圾回收3.1 如…

白酒:酿造环境的优化与生态平衡的维护

在豪迈白酒的酿造过程中&#xff0c;酿造环境的优化与生态平衡的维护对于产品的品质和可持续发展具有重要意义。云仓酒庄深知这一点&#xff0c;采取了一系列措施来保护和改善酿造环境。 首先&#xff0c;酒庄重视酿造环境的绿化和美化工作。通过种植树木、花卉等植物&#xff…

windows达梦安装

1.首先准备好windows安装系统&#xff0c;准备工作的做好&#xff0c;然后把素材包dm8_20230418_x86_win_64放进去&#xff0c;进行解压 解压完成之后&#xff0c;把dm8_20230418_x86_win_64再次进行解压&#xff0c;然后点击安装setup进行安装 然后点击接受进行下一步&#xf…

证书透明度|微软发布CT政策,亚数TrustAsia成为其可信任的CT运营商

近日&#xff0c;Microsoft&#xff08;微软&#xff09;发布证书透明度&#xff08;Certificate Transparency&#xff0c;CT&#xff09;政策&#xff0c;允许微软Web应用程序与浏览器保持一致的信任&#xff0c;或需要满足监管要求&#xff0c;可以通过CT来实现。 &#xff…

如何注册使用Snapchat?

Snapchat是一款图片分享软件应用。利用该应用程序&#xff0c;用户可以拍照、录制影片、撰写文字和图画,并传送到自己在该应用上的好友列表。现如今&#xff0c;Snapchat也成为独立战引流然而&#xff0c;即使如此受欢迎&#xff0c;Snapchat的注册使用仍然是新手的难题&#x…

C++语言现在还有人学吗?

在当今信息爆炸的时代&#xff0c;计算机编程语言繁多&#xff0c;涌现了许多新兴的编程语言&#xff0c;如Python、JavaScript等。针对C编程语言是否还有人学的问题&#xff0c;我个人认为可以从以下几个方面进行讨论。 首先&#xff0c;C诞生于1979年&#xff0c;起初是为了开…

计算机考研|南京大学AI宣布改考408的启示

大家都是考计算机的&#xff0c;直接去官网上验证一点也不难 我去了南京大学的官网&#xff0c;并没有找到相关的通知&#xff0c;目前只能看到南京大学2024硕士研究生招生目录中相关的描述&#xff0c;可以看到还没有改&#xff0c;至于要不要改&#xff0c;真正的官方消息出…

Python爬虫实战入门:豆瓣电影Top250(保你会,不会来打我)

文章目录 需求所需第三方库requests模块lxml模块了解 lxml模块和xpath语法xpath语法-基础节点选择语法 实战教程完整代码 需求 目标网站: https://movie.douban.com/top250 需求: 爬取电影中文名、英文名、电影详情页链接、导演、主演、上映年份、国籍、类型、评分、评分人数, …

优思学院|质量管理方面的证书有哪些?

在这个迅速变化的时代&#xff0c;每个行业都在寻找方法来提高效率、质量和生产力。 对于那些刚踏入或有兴趣深入探索质量管理领域的朋友们来说&#xff0c;一个常见的疑问浮现&#xff1a;为了提升自己的专业技能和职业前景&#xff0c;应该考取哪些证书&#xff1f; 在这方…

3D产品配置器:提升客户体验与满意度的创新工具

在竞争激烈的市场中&#xff0c;企业不断寻求新的方法来提升客户体验和满意度。3D产品配置器作为一种创新工具&#xff0c;为客户提供了一种全新的购物体验&#xff0c;同时也为企业带来了一系列的好处。 1、互动体验&#xff1a;完整的产品理解 3D产品配置器通过互动的方式&…

15.2 Scrapy 入门

目录 一. 目标 二. 准备工作 三. 开始入门 1. 创建项目 2. 创建 Spider 3. 创建 Item 4. 解析 Response 5. 使用 Item 6. 后续 Request 7. 运行 8. 保存文件 9. 使用 Item Pipeline 一. 目标 以一个目标例子来实战理解。目标如下&#xff1a;&#xff08;1&…

Oladance、南卡、漫步者开放式耳机值不值得买?三大顶流机型全面测评

​当前市场充斥着不少质量欠佳且音质平庸的开放式耳机&#xff0c;这类产品不仅会损害音频体验&#xff0c;长期使用还可能对用户的听力健康构成威胁。作为一名资深的音频设备评测专家&#xff0c;我强调在挑选耳机时应优先考虑那些拥有强大专业背景的品牌。基于我个人的深入测…