2024年Linux最全Linux线程-互斥与同步_linux线程间同步和互斥的方法(2),2024年最新高级面试题+解析

最全的Linux教程,Linux从入门到精通

======================

  1. linux从入门到精通(第2版)

  2. Linux系统移植

  3. Linux驱动开发入门与实战

  4. LINUX 系统移植 第2版

  5. Linux开源网络全栈详解 从DPDK到OpenFlow

华为18级工程师呕心沥血撰写3000页Linux学习笔记教程

第一份《Linux从入门到精通》466页

====================

内容简介

====

本书是获得了很多读者好评的Linux经典畅销书**《Linux从入门到精通》的第2版**。本书第1版出版后曾经多次印刷,并被51CTO读书频道评为“最受读者喜爱的原创IT技术图书奖”。本书第﹖版以最新的Ubuntu 12.04为版本,循序渐进地向读者介绍了Linux 的基础应用、系统管理、网络应用、娱乐和办公、程序开发、服务器配置、系统安全等。本书附带1张光盘,内容为本书配套多媒体教学视频。另外,本书还为读者提供了大量的Linux学习资料和Ubuntu安装镜像文件,供读者免费下载。

华为18级工程师呕心沥血撰写3000页Linux学习笔记教程

本书适合广大Linux初中级用户、开源软件爱好者和大专院校的学生阅读,同时也非常适合准备从事Linux平台开发的各类人员。

需要《Linux入门到精通》、《linux系统移植》、《Linux驱动开发入门实战》、《Linux开源网络全栈》电子书籍及教程的工程师朋友们劳烦您转发+评论

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

零、前言

本章主要讲解学习Linux中对多线程的执行中的同步与互斥

一、Linux线程互斥

1、基本概念及引入

  • 互斥相关概念:
  1. 临界资源:多线程执行流共享的资源就叫做临界资源
  2. 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  3. 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  4. 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
  • 示例:模拟抢票
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
int thickets=100;//100张票
//thickets--表示抢票
void\* Routine(void\* arg)
{
    while(1)
    {
        if(thickets>0)
        {
            usleep(30000);//抢票时间
            printf("%s get a thickets, now thickets' number:%d\n",(char\*)arg,--thickets);
        }
        else 
            break;
    }
    return NULL;
}

int main()
{
    pthread_t tid1,tid2,tid3;
    pthread\_create(&tid1,NULL,Routine,(void\*)"thread 1");
    pthread\_create(&tid2,NULL,Routine,(void\*)"thread 2");
    pthread\_create(&tid3,NULL,Routine,(void\*)"thread 3");

    pthread\_join(tid1,NULL);
    pthread\_join(tid2,NULL);
    pthread\_join(tid3,NULL);
    return 0;
}

  • 效果:

image-20220401104913338

注:变量tickets被多个执行流同时访问,所以thickets就是一个临界资源,当访问临界资源时,判断tickets是否大于0、打印剩余票数以及--tickets的代码也就是临界区

  • 出现负数的原因:

if语句判断条件为真以后,代码可以并发的切换到其他线程

usleep用于模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段

–ticket操作本身就不是一个原子操作,可能在执行当中也被切换成其他线程

  • 具体可能的过程:

当thickets为1时,一个线程进行if判断为真,进入代码段,当执行到usleep进行系统调用休眠,返回时到用户态时线程发生切换,多个线程此时也进行if判断为真(thickets还是1),这些线程当进行打印的时候进行了多次的减减操作,也就造成了负数的情况

  • – 操作并不是原子操作,而是对应三条汇编指令:
  1. load :将共享变量ticket从内存加载到寄存器中
  2. update : 更新寄存器里面的值,执行-1操作
  3. store :将新值,从寄存器写回共享变量ticket的内存地址
  • –执行对应的汇编代码:
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

注:因为减减操作并不是原子的,当减减操作第一步执行完(thickets=100),可能该线程的时间片到了(寄存器中的数据被保存eax=100),其他线程切入,而切入的线程执行了多次减减并写会到内存(thickets=80),当切出的线程切回时,恢复线程上下文数据(eax=100),再进行减减(eax=99),把数据写回到内存时(thickets=99),此时的数据的值只达到了一次减减的效果,此时的资源并不安全

2、互斥量mutex介绍

  • 概念:
  1. 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况变量归属单个线程,其他线程无法获得这种变量
  2. 但有时候,很多变量都需要在线程间共享,这样的变量成为共享变量,可以通过数据的共享,完成线程之间的交互
  3. 多个线程并发的操作共享变量,就会带来一些问题
  • 要解决以上问题需要做到三点:
  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区
  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

注:要做到这三点,本质上就是需要一把锁,Linux上提供的这把锁叫互斥量

  • 示图:

image-20220401112455039

3、互斥量的使用

  • 初始化互斥量:
  1. 静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

  1. 动态分配
int pthread\_mutex\_init(pthread_mutex_t \*restrict mutex, const pthread_mutexattr_t \*restrictattr);

参数:mutex:要初始化的互斥量;attr:互斥量的属性,一般设置为NULL

  • 销毁互斥量:
int pthread\_mutex\_destroy(pthread_mutex_t \*mutex);

  • 注意:
  1. 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  2. 不要销毁一个已经加锁的互斥量
  3. 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
  • 互斥量加锁和解锁:
int pthread\_mutex\_lock(pthread_mutex_t \*mutex);
int pthread\_mutex\_unlock(pthread_mutex_t \*mutex);

返回值:成功返回0,失败返回错误号

  • 调用 pthread_ lock 时可能遇到的情况:
  1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  2. 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁
  • 示例:改进抢票
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
int thickets=100;//100张票
//thickets--表示抢票
pthread_mutex_t lock;//线程共用一个互斥锁
void\* Routine(void\* arg)
{
    while(1)
    {
        pthread\_mutex\_lock(&lock);
        if(thickets>0)
        {
            usleep(100000);//抢票时间
            printf("%s get a thickets, now thickets' number:%d\n",(char\*)arg,--thickets);
            pthread\_mutex\_unlock(&lock);
        }
        else
        {
            pthread\_mutex\_unlock(&lock);
            break;
        }
        usleep(100000);
    }
    return NULL;
}
int main()
{
    pthread\_mutex\_init(&lock,NULL);
    pthread_t tid1,tid2,tid3;
    pthread\_create(&tid1,NULL,Routine,(void\*)"thread 1");
    pthread\_create(&tid2,NULL,Routine,(void\*)"thread 2");
    pthread\_create(&tid3,NULL,Routine,(void\*)"thread 3");

    pthread\_join(tid1,NULL);
    pthread\_join(tid2,NULL);
    pthread\_join(tid3,NULL);
    pthread\_mutex\_destroy(&lock);
    return 0;
}

  • 效果:

image-20220401232114007

4、互斥量原理

  • 概念:
  1. 对于互斥锁来说被多个线程同时可见,也就是说互斥锁本身就是一个临界资源,所以互斥锁想要保护临界区的互斥性,那么互斥锁操作则一定是原子的
  2. 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性
  3. 即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期
  • 示图:伪代码

image-20220402204130295

注:在交换和赋值的过程中本质就是让竞争的多线程中保证中有一个线程的交换得到的寄存器数据为1,即保证同一时刻只有一个竞争的线程为1,由此才能往下执行,否则只能进行等待

二、可重入/线程安全

1、基本概念

  • 线程安全:
  1. 多个线程并发同一段代码时,不会出现不同的结果,没有数据错乱的情况
  2. 常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题
  • 重入:
  1. 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入
  2. 一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则是不可重入函数
  • 注意:
  1. 对于可重入来说是函数的特性,对于线程安全来说是线程的特性
  2. 如果一个函数是可重入的,那么执行还函数的多线程是线程安全的

2、线程安全

  • 常见线程不安全的情况:
  1. 不保护共享变量的函数
  2. 函数状态随着被调用,状态发生变化的函数
  3. 返回指向静态变量指针的函数
  4. 调用线程不安全函数的函数
  • 常见的线程安全的情况:
  1. 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  2. 类或者接口对于线程来说都是原子操作
  3. 多个线程之间的切换不会导致该接口的执行结果存在二义性

3、重入函数

  • 常见不可重入的情况:
  1. 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  2. 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  3. 可重入函数体内使用了静态的数据结构
  • 常见可重入的情况:
  1. 不使用全局变量或静态变量
  2. 不使用用malloc或者new开辟出的空间
  3. 不调用不可重入函数
  4. 不返回静态或全局数据,所有数据都有函数的调用者提供
  5. 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

4、联系与区别

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

三、常见锁概念

  • 死锁:

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

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

注:对于死锁,四个条件缺一不可

  • 避免死锁:
  1. 破坏死锁的四个必要条件
  2. 加锁顺序一致
  3. 避免锁未释放的场景
  4. 资源一次性分配
  • 避免死锁算法:
  1. 死锁检测算法
  2. 银行家算法

四、Linux线程同步

1、基本概念

  • 同步概念与竞态条件:
  1. 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
  2. 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件
  • 注意:
  1. 在多线程中,为了保护临界资源,我们需要用到互斥锁,但是在线程竞争的情况下,此外我们还需要考虑资源的一些特殊情况
  2. 在特殊的情况下,可能存在某个线程多次的竞争获取锁,但是却没有做出实际的事情,这种频繁的申请虽然没有什么问题,但是不是很合理
  3. 同时如果线程的竞争力非常强,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题
  4. 由此我们需要对于这种特殊的情况,保证线程能够按照某种次序进行临界资源的访问,由此就需要条件变量
  • 条件变量:

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中

2、条件变量的使用

  • 初始化条件变量:
  1. 静态分配
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

  1. 动态分配
  • 初始化函数原型:
int pthread\_cond\_init(pthread_cond_t \*restrict cond,const pthread_condattr_t \*restrictattr);

  • 解释:
  1. 参数:cond:要初始化的条件变量;attr:设置属性,一般填NULL
  2. 返回值:条件变量初始化成功返回0,失败返回错误码
  • 销毁函数原型:
int pthread\_cond\_destroy(pthread_cond_t \*cond)

  • 解释:
  1. 参数:cond:需要销毁的条件变量
  2. 返回值:条件变量销毁成功返回0,失败返回错误码
  3. 使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁
  • 等待条件满足函数原型:
int pthread\_cond\_wait(pthread_cond_t \*restrict cond,pthread_mutex_t \*restrict mutex);

  • 解释:

最后的话

最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!

资料预览

给大家整理的视频资料:

给大家整理的电子书资料:

如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

要销毁的条件变量

  1. 返回值:条件变量销毁成功返回0,失败返回错误码
  2. 使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁
  • 等待条件满足函数原型:
int pthread\_cond\_wait(pthread_cond_t \*restrict cond,pthread_mutex_t \*restrict mutex);

  • 解释:

最后的话

最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!

资料预览

给大家整理的视频资料:

[外链图片转存中…(img-xXchLdYD-1714731501743)]

给大家整理的电子书资料:

[外链图片转存中…(img-NaFzZrev-1714731501743)]

如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值