Linux之线程安全(下)

news/2024/11/18 0:36:38/

文章目录

  • 前言
  • 一、Linux线程互斥
    • 1.mutex的理解
      • 原子性
      • 互斥锁实现原子性的原理
    • 2.mutex的封装——Mutex.hpp
    • 3.可重入和线程安全
      • 可重入
      • 线程安全
      • 线程安全不一定是可重入的,而可重入函数一定是线程安全的。
    • 4.死锁
      • 概念
      • 造成死锁的四个必要条件
      • 如何避免死锁
  • 二、Linux线程同步
    • 1.引入
    • 2.条件变量
    • 3.条件变量接口
    • 4.理解条件变量
    • 条件变量的使用
      • 一次唤醒一个线程
      • 一次唤醒一批线程
  • 总结


前言

本文承接上一篇文章的内容,继续介绍Linux中的线程安全问题及解决方法。


一、Linux线程互斥

1.mutex的理解

  1. 锁本身也是一个共享资源。
    共享资源需要被锁保护,但是锁本身也是共享资源,谁来保护锁呢?
    pthread_mutex_lock、pthread_mutex_unlock:加锁和解锁的过程必须是线程安全的,因此加锁的过程是原子的(未来解锁的一定是一个执行流)。
  2. 谁持有锁谁就能进入临界区。如果申请锁成功,就继续向后续代码执行。如果申请锁失败,执行流会怎么办?如果已经加了一次锁,然后再加一次锁,结果又会怎样?

这时,程序不再执行,执行流会阻塞。

原子性

如果线程1,申请锁成功,进入临界区,正在访问临界资源。此时其它进程真正阻塞等待。那么问题来了,这时该线程是否可以被切换?答案是肯定的,可以被切换。
当持有锁的线程被切换走时,它是抱着锁一起被切走的。即使该线程被切换掉,其它线程此时也无法申请锁,只能等待该线程将锁释放掉。
因此,对于其它线程而言,有意义的锁的状态只有两种:1.锁被申请前、2.锁被释放后。
在其它线程眼中,当前线程持有锁的过程就是原子的(要么持有,要么不持有)。

注意:

  1. 我们在使用锁的时候一定要尽量保证临界区的粒度尽可能小(粒度是加锁和解锁之间的代码的多少,即锁保护的代码的多少)。
  2. 加锁是程序员行为,必须做到要加就都加(公共资源,要么加锁,要么不加锁,这是程序员决定的,尽量避免因为锁而写bug)。

互斥锁实现原子性的原理

从汇编指令谈加锁:为了实现互斥锁操作,大多数体系结构提供了swap和exchange指令,它们的作用是把寄存器和内存单元的数据直接进行交换。由于该操作只用了一条指令,因此可以保证原子性。
汇编指令:

lock:movb $0, %alxchgb %al, mutexif(al寄存器的内容 > 0){return 0;}else挂起等待;goto lock;
unlock:movb $1, mutex唤醒等待Mutex的线程return 0;

加锁:将内存里的数据与寄存器中的数据进行交换,就加锁了。
在这里插入图片描述
xchgb指令将CPU中寄存器里的数据与内存中对应的数据进行直接交换(原子操作)
在这里插入图片描述
在这里插入图片描述
解锁:申请锁成功的线程将寄存器内的内容(上下文信息)与内存里的数据交换,然后就成功解锁了。

2.mutex的封装——Mutex.hpp

文件Mutex.hpp

  1 #pragma once2 #include<iostream>3 #include<pthread.h>4 using namespace std;5 class Mutex6 {7 public:8         Mutex(pthread_mutex_t* lock_p = nullptr)9         :lock_p_(lock_p)10         {}11         void lock()12         {13                 if(lock_p_)14                 {15                         pthread_mutex_lock(lock_p_);16                 }17         }18         void unlock()19         {20                 if(lock_p_)21                 {22                         pthread_mutex_unlock(lock_p_);23                 }24         }25         ~Mutex()26         {}27 private:28         pthread_mutex_t* lock_p;29 };30 31 class LockGuard32 {33 public:34         LockGroud(pthread_mutex_t* mutex)35         :mutex_(mutex)36         {37                 mutex_.lock();//在构造函数里加锁38         }39         ~LockGroud()40         {41                 mutex_.unlock();//在析构函数里解锁42         }43 private:44         Mutex mutex_;45 };

测试代码:
文件main.cc

  1 #include"Mutex.hpp"2 int tickets = 1000;3 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;4 void* get_ticket(void* args)5 {6         string name = static_cast<const char*>(args);7         while(1)8         {9                 LockGuard lockguard(&lock);10                 if(tickets > 0)11                 {12                         usleep(1234);13                         cout<<name<<"正在抢票, 剩余票数:"<<tickets<<endl;14                         tickets--;15                 }16                 else17                 {18                         break;19                 }20         }21         return nullptr;22 }23 int main()24 {25         pthread_t t1, t2, t3, t4;26         pthread_create(&t1, nullptr, get_ticket, (void*)"thread 1");27         pthread_create(&t2, nullptr, get_ticket, (void*)"thread 2");28         pthread_create(&t3, nullptr, get_ticket, (void*)"thread 3");29         pthread_create(&t4, nullptr, get_ticket, (void*)"thread 4");30 31         pthread_join(t1, nullptr);32         pthread_join(t2, nullptr);33         pthread_join(t3, nullptr);34         pthread_join(t4, nullptr);35         return 0;36 }

运行:
在这里插入图片描述

3.可重入和线程安全

可重入

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

线程安全

线程安全:多个线程并发执行同一段代码,多次测试不会出现不同的结果(即,没有问题),常见的多线程对全局变量或静态变量进行操作,在没有锁保护的情况下会出现问题,例如:抢票。

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

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

常见的可重入的情况
1.每个线程对全局变量或静态变量只有读取的权限,没有修改(写入)的权限,一般来说,这些线程是安全的;
2.类或者接口对于线程来说都是原子操作,多个线程之间的切换不会导致该接口的执行结果存在二义性。
常见的不可重入的情况
1.调用了malloc/free函数:因为malloc函数是用全局链表来管理堆的(链表的插入等操作是不可重入的);
2.调用标准I/O库函数:标准I/O库函的很多实现都是以不可重入的方式使用全局数据结构;
3.可重入函数体使用了静态的数据结构。

4.死锁

概念

一组执行流(进程/线程)持有自己锁资源的同时,还想申请对方的锁(但是,锁是不可抢占的,只能等持锁的线程主动归还),这会使得多个执行流互相等待对方持有的资源,导致代码无法推进。这就是死锁
特殊的,一把锁也会导致死锁问题,在已经申请锁的情况下,又去申请一把锁,就会导致死锁问题。

为什么会导致死锁?
前提是使用了锁——锁可以保护临界资源的安全;为啥要保护临界资源——多线程并发访问临界资源会导致数据不一致的问题——多线程的大部分资源是临界资源(共享资源)——多线程的特性决定的。
为了解决一个问题,带来了新的问题:死锁。任何技术都有自己的边界,在解决一个问题的同时,一定会导致另一个新的问题。

造成死锁的四个必要条件

  1. 互斥:一个共享资源每次仅被一个执行流使用;
  2. 请求和保持:一个执行流因请求其它资源而阻塞,同时也不释放已有资源;
  3. 不剥夺:一个执行流获得的资源在未(使用完毕)主动释放之前,不能被强行剥夺;
  4. 环路等待:执行流之间形成环路问题,循环等待资源。

如何避免死锁

  1. 破坏死锁的四个必要条件(破坏其中一个及以上即可)。
  2. 加锁顺序保持一致;
  3. 避免锁未释放的场景(防止出现锁一直被占有,无法申请);
  4. 资源一次性分配(一个执行流需要的资源,一次性全部分配给它)。

二、Linux线程同步

1.引入

举一些生活中的例子:
游乐园的热门项目,先到先玩;打印机打印东西,先到的人先打印;上厕所时将门反锁,其他人无法进入……
这些例子中,离资源越近的人竞争力越强,就导致一直是同一个人在拿到资源、释放资源,造成其他人饥饿状态。我们本节内容和上节内容所举的例子:抢票系统就是这样,我们发现很长一段时间一直是同一个线程在抢票,造成其它线程的饥饿问题。
为了解决这个问题,我们在数据安全的情况下让这些线程按照一定的顺序申请资源,这就是线程同步
饥饿状态:得不到锁资源,而无法访问公共资源的线程,处于饥饿状态。它并没有错,但是不合理。
竞态条件:因为时序问题导致程序异常,我们称为竞态条件。
线程同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。

2.条件变量

当一个线程互斥的访问某个资源时,它有可能发现在其它线程改变状态之前,它不能对该资源进行操作。
例如:一个线程访问一个队列时,发现队列为空,它只能等待其它线程往该队列里添加节点,这种情况就需要用到条件变量。
条件变量通常是配合互斥锁一起使用的。
条件变量的使用:一个线程等待条件变量的条件成立而被挂起;另一个线程使条件成立后唤醒等待的线程。

3.条件变量接口

//初始化
int pthread_cont_init(pthread_cont_t* restrict cont, const pthread_condarr_t* restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//销毁
int pthread_cond_destroy(pthread_cond_t* cond);
//特定时间阻塞等待
int pthread_cond_timedwait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex, const struct timespec* restrict abstime);
//等待
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
//唤醒一批线程
int pthread_cond_broadcast(pthread_cond_t* cond);
//唤醒一个线程
int pthread_cond_signal(pthread_condt_t* cond);

4.理解条件变量

举例:公司进行招聘,很多应聘者来面试,只有一个面试官,一次只能有一个应聘者进入面试间进行面试。由于没有组织者进行组织,导致没有规则:上一个人面试完之后,所有人都拥挤到面试官面前申请面试,面试官只能选择离自己最近的那个人进行面试,这就导致一群人在外面等着,总有人抢不过别人一直没有面试机会,甚至有的人面试完一次再次申请面试的情况,造成其它人的饥饿问题。这种情况下,面试的效率很低下。
之后,面试官对面试的顺序制定了规则,设立了一个等待区,所有人按照到达的时间进行排队,这样一来所有人都有机会面试了。
而这个等待区就是条件变量,如果一个人想进行面试,就要先去等待区等待,未来所有的应聘者都要去条件变量等待。
在这里插入图片描述
条件不满足时,线程就必须去某些定义好的条件变量上进行等待
变量条件(struct cond, 结构体)里面包含状态、队列。条件变量里包含一个队列,不满足条件的线程就链接在这个队列上进行等待。
在这里插入图片描述

条件变量的使用

可以通过条件变量来控制线程的执行。由于条件变量本身并不具备互斥的功能,所以条件变量必须配合互斥锁使用:

一次唤醒一个线程

创建2个线程,通过条件变量一秒唤醒一个线程(或者全部唤醒)
文件test.cc

  1 #include"Mutex.hpp"2 int tickets = 1000;3 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;4 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//条件变量5 void* start_routine(void* args)6 {7         string name = static_cast<const char*>(args);8         while(1)9         {10                 LockGuard lockguard(&lock);11                 pthread_cond_wait(&cond, &lock);12                 if(tickets > 0)13                 {14                         cout<<name<<"正在抢票, 剩余票数:"<<tickets<<endl;15                         tickets--;16                 }17                 else18                 {19                         break;20                 }21         }22         return nullptr;23 }24 int main()25 {26         pthread_t t1, t2;27         pthread_create(&t1, nullptr, start_routine, (void*)"thread 1");28         pthread_create(&t2, nullptr, start_routine, (void*)"thread 2");29         while(1)30         {31                 sleep(1);32                 pthread_cond_signal(&cond);33                 cout<<"main thread wakeup one thread..."<<endl;34         }35         pthread_join(t1, nullptr);36         pthread_join(t2, nullptr);37         return 0;38 }

在这里插入图片描述

主线程一个一个去叫,按照一定顺序输出打印。

一次唤醒一批线程

文件test1.cc

  1 #include"Mutex.hpp"2 int tickets = 1000;3 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;4 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//条件变量5 void* start_routine(void* args)6 {7         string name = static_cast<const char*>(args);8         while(1)9         {10                 LockGuard lockguard(&lock);11                 pthread_cond_wait(&cond, &lock);12                 if(tickets > 0)13                 {14                         cout<<name<<"正在抢票, 剩余票数:"<<tickets<<endl;15                         tickets--;16                 }17                 else18                 {19                         break;20                 }21         }22         return nullptr;23 }24 int main()25 {26         pthread_t t1, t2, t3, t4, t5;27         pthread_create(&t1, nullptr, start_routine, (void*)"thread 1");28         pthread_create(&t2, nullptr, start_routine, (void*)"thread 2");29         pthread_create(&t3, nullptr, start_routine, (void*)"thread 3");30         pthread_create(&t4, nullptr, start_routine, (void*)"thread 4");31         pthread_create(&t5, nullptr, start_routine, (void*)"thread 5");32         while(1)33         {34                 sleep(1);35                 pthread_cond_broadcast(&cond);36                 cout<<"main thread wakeup one thread..."<<endl;37         }38         pthread_join(t1, nullptr);39         pthread_join(t2, nullptr);40         return 0;41 }

运行:
在这里插入图片描述


总结

以上就是今天要讲的内容,本文继上一篇文章继续介绍了线程安全的相关内容,主要介绍了锁以及条件变量等相关概念。作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!


http://www.ppmy.cn/news/431961.html

相关文章

这些带斑点的蛇实在太漂亮了,盘点18种带斑点的蛇

大自然充满了迷人的动植物&#xff0c;色彩绚丽&#xff0c;令人惊叹&#xff0c;尤其是蛇。许多沙漠蛇有中性的沙色&#xff0c;而另一些则有明亮、大胆的图案&#xff0c;很容易激发新的时尚潮流。一些蛇有大片斑块&#xff0c;而另一些则有各种形状和大小的条纹和斑点。今天…

razer金环蛇双击问题实战解决

近日&#xff0c;用了许久的razer金环蛇出现了双击问题。windows下想选中复制&#xff0c;结果是打开。有时候dota站在泉水用鼠标点回程想飞塔下去。。。结果。。。还是回到了原地。囧rz。。。&#xff01;插上原来的老罗技MX318&#xff0c;实在很不习惯&#xff0c;新鼠标又昂…

HCCDA

判断题 1、Kubernetes的声明式API通过提交一个个命令&#xff0c;以达到期望状态 False 2、可变基础设施和不可变基础设施之间最根本的区别在于它们的核心政策&#xff0c;前者在部署后会进行更改&#xff0c;后者保持不变并….换 true 3、容器中的进程ID与容器进程在宿主…

linux驱动由浅入深系列:块设备驱动之一(高通eMMC分区实例)

linux驱动由浅入深系列:块设备驱动之一(高通eMMC分区实例) linux驱动由浅入深系列:块设备驱动之二(从用户空间的read、write到实际设备物理操作整体架构分析)linux驱动由浅入深系列:块设备驱动之三(块设备驱动结构分析,以mmc为例) 块设备驱动的模型还是基本基于字符…

高通芯片刷机我的分析理解(启动分析故障分析)

安卓手机高通芯片刷机我的分析理解高通芯片手机是市面上比较流行的手机系列&#xff0c;例如&#xff1a;小米系列大部分机型&#xff0c;三星系列部分高端机型&#xff0c;中兴努比亚系列&#xff0c;联想手机高端机型&#xff0c;一加手机全部机型&#xff0c;还有华为手机一…

高通平台如何修改特殊电压

转载&#xff1a;https://blog.csdn.net/qq_36781842/article/details/103721013 高通平台如何设置LDO电压&#xff0c;以LDO17为例&#xff0c;默认给屏供电&#xff0c;设置为2.85V现在设置为3.3V。 修改的rpm和sbl部分代码&#xff0c;修改LDO17电压为3.3V 。 (1). --- a/R…

android 输出分辨率6,Android+高通 MIPI转LVDS显示屏调试之---基于SD65DSI84

1.硬件接口 显示屏分辨率是1920RGB1080 pixels。 1.1 硬件连接示意图 1.2 硬件连接引脚CPUSN65DSI84 DSI转LVDSLM3492HCMH 背光控制IC显示屏 GPIO126_LCD_1V8_ENVCC GPIO129_LCD_5V_EN5V_BL GPIO125_LCD_COMMCOMM MPP2_LCD_BL_PWMDIM1/CLK GPIO123_LCD_DIM2DIM2 GPIO7_LCD_I2C2…

高通平台 设置LDO电源域

高通平台如何设置LDO电压&#xff0c;以LDO17为例&#xff0c;默认给屏供电&#xff0c;设置为2.85V现在设置为3.3V。 修改的rpm和sbl部分代码&#xff0c;修改LDO17电压为3.3V 。 (1). --- a/RPM.BF.2.2/rpm_proc/core/systemdrivers/pmic/config/msm8937/pm_config_target.cb…