linux---线程安全(同步与互斥)

2023-05-16

1. 什么是线程安全
2. 如何实现线程安全
3. 什么是互斥和互斥的实现
4. 死锁
5. 什么是同步和同步的实现
6. 可重入和不可重入函数

1. 什么是线程安全
多个线程同时操作临界资源,而不会出现数据的二义性就说明这个线程就是线程安全。比如看下面的例子,当我们调用下面的例子的时候会出现不一样的结果,因为在线程中我们对num的操作是一个非原子性操作,在这个里面我们的理想的结果是输出5,在我们调用程序的时候可能会出现7或者10等等的结果,是因为我们开辟两个线程都同时对这个num进行了操作。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int num = 0;
void* thr_start(void* arg){
	num+=2;
	sleep(5);
	num+=3;
	printf("%d\n",num);
}
int main(){
	int a = 0;
	int ptid[2];
	for(int i = 0;i < 2; i++){
		pthread_create(&ptid[i],NULL,th_start,NULL);
		pthread_detach(ptid[i]);
	}
	sleep(10);
	return 0;
}

理解上面的概念:

  • 临街资源:多线程执行流共享的资源就叫做临街资源,上面程序就是我们的num
  • 临界区:每个线程内部,访问临街自娱的代码,就叫做临界区
  • 原子性:不会被任何调度禁止打断的操作,该操作只有两种状态,要么完成要么未完成。
    我们判断线程是不是安全:判断在线程中是否对临界资源进行了非原子性的操作。
    2. 如何实现我们的线程安全
    实现我们的线程安全就使用同步与互斥,同步就是控制临界资源的合理访问(时序可控),互斥就是临界资源同一时间的唯一访问(我访问的时候别人不能去访问)。
    3. 什么是互斥和互斥的实现
    任何时刻,互斥保证有且只有一个执行流进入到临界区对临界资源进行操作,通常对临街资源起保护作用。
    通常来说线程的函数中处理的都是一些局部变量,如果在线程函数中处理了我们的全局变量或者static变量的话,在多个线程并发的时候就出现了数据的二义性,此时我们通常会采用互斥锁来解决问题,
    互斥锁:就是一个1/0计数器。1表示可以加锁,加锁就是计数-1,操作完之后进行解锁操作,解锁就是计数+1,0表示不可以加锁,不能加锁则等待,
    在这里插入图片描述
    实际上是从寄存器中映射到我们的内存中,是寄存器和我们的内存进行直接的交互,当我们的寄存器为0的时候,内存就变为0,则不能等待加锁。在大多数的体系结构都提供了swap和exchange指令,该指令的作用就是把寄存器和内存单元的数据进行交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先来后到,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。这就是我们互斥锁的实现。
    看下面一个代码(一个抢票程序)
  #include <stdio.h>
  #include <unistd.h>
  #include <stdlib.h>
  #include <pthread.h>
 int num = 100;
 pthread_mutex_t mutex;
 void* thr_a(void* arg){
      while(1){
         if(num > 0){
              printf("----%d---抢到了%d号票\n",(int)arg,--num);
         }else{
             return NULL;
          }
     }
      return NULL;
  }
  int main(){
     pthread_t tid[4];
     int i = 0;
     for(; i < 4; i++){
        pthread_create(&tid[i],NULL,thr_a,(void*)i);
      }
     for(i = 0;i < 4; ++i){
        pthread_join(tid[i],NULL);
    }
     return 0;
 }        

此时会出现结果是一张票会多次被抢,此时使用我们的互斥锁进行处理
互斥锁接口:

  • 定义一个互斥锁变量
pthraed_mutex_t _mutex;
  • 初始化互斥锁变量
       #include <pthread.h>
       int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

初始化方式有两种,一种是静态初始化,在定义的时候就进行初始化。一种是函数初始化。互斥锁变量一定要使用此锁的线程都能访问。
参数:
mutex:是定义的互斥锁变量
attr:互斥锁的属性,一般置为NULL。
返回值:成功是返回0,不成功返回errno

  • 加锁解锁操作
       int pthread_mutex_lock(pthread_mutex_t *mutex);
       int pthread_mutex_trylock(pthread_mutex_t *mutex);
       int pthread_mutex_unlock(pthread_mutex_t *mutex);

加锁:在临界资源操作之前,要在线程中任意有可能退出的地方进行加锁。
参数:mutex,定义是的互斥锁

   int pthread_mutex_trylock(pthread_mutex_t *mutex);是尝试加锁,如果不成功就立即放回。
   int pthread_mutex_lock(pthread_mutex_t *mutex);加锁,不能加锁则等待
	int pthread_mutex_unlock(pthread_mutex_t *mutex);解锁操作
  • 销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

销毁互斥锁
修改上述程序解决互斥的问题

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <pthread.h>
  5 int num = 100;
  6 pthread_mutex_t mutex;
  7 void* thr_a(void* arg){
  8     while(1){
  9         pthread_mutex_lock(&mutex);
 10         if(num > 0){
 11             printf("----%d---抢到了%d号票\n",(int)arg,--num);
 12         }else{
 13         pthread_mutex_unlock(&mutex);
 14             return NULL;
 15         }
 16         pthread_mutex_unlock(&mutex);
 17     }
 18     return NULL;
 19 }
 20 int main(){
 21     pthread_t tid[4];
 22     pthread_mutex_init(&mutex,NULL);
 23     int i = 0;
 24     for(; i < 4; i++){
 25         pthread_create(&tid[i],NULL,thr_a,(void*)i);
 26     }
 27     for(i = 0;i < 4; ++i){
 28         pthread_join(tid[i],NULL);
 29     }
 30     pthread_mutex_destroy(&mutex);
 31     return 0;
 32 } 

4. 死锁
死锁:死锁就是因为在加锁之后诶呦进行解锁而导致程序卡死(对一些无法加锁的锁进行加锁而导致程序卡死)
死锁产生的4个必要条件

  • 互斥条件(我操作的时候别人不可操作)
  • 不可剥夺条件(我的锁别人不能释放)
  • 请求与保持条件(一个执行流因请求资源而阻塞时,对已有资源保持你不放)
  • 环路等待条件(形成了互相等待的情况)
    死锁产生的场景
  • 忘记释放锁
void data_process()
{
EnterCriticalSection();
if(/* error happens, forget LeaveCriticalSection */)
return;
LeaveCriticalSection();
}

当我们不释放锁的时候别人加锁的时候就会一直等待,从而出现了死锁的情况,导致别人就一直不能加锁,程序卡死

  • 单线程重复申请锁
void sub_func()
{
EnterCriticalSection();
do_something();
LeaveCriticalSection();
}
void data_process()
{
EnterCriticalSection();
sub_func();
LeaveCriticalSection();
}

单线程重复加锁的时候是因为我们单线程申请一个锁之后我们没有释放的时候又进行加锁操作,此时我们上一个锁没有进行释放,此时我们加锁就加锁不上,就会一直出现等待的情况,所以出现程序卡死

  • 多线程多锁申请
void data_process1()
{
EnterCriticalSection(&cs1);  // 申请锁的顺序有依赖
EnterCriticalSection(&cs2);
do_something1();
LeaveCriticalSection(&cs2);
LeaveCriticalSection(&cs1);
}
void data_process2()
{
EnterCriticalSection(&cs2);  // 申请锁的顺序有依赖
EnterCriticalSection(&cs1);
do_something2();
LeaveCriticalSection(&cs1);
LeaveCriticalSection(&cs2);
}

多线程申请多锁的时候对顺序有依赖,当我们两个线程对cs1和cs2锁分别进行加锁的时候,当我们队线程1对cs1加锁成功,线程2对cs2加锁成功的话我们线程1就不能对cs2加锁。线程2不能对cs1加锁,此时就会导致两个线程互相等待锁的释放,但是我们此时两个线程都出现等待。所以程序就会导致卡死。

  • 多线程环形锁
    在这里插入图片描述
    多个线程等待互相等待,线程1等待线程2释放锁,线程2等待线程3释放锁,线程3等待线程4释放锁,线程4等待线程1释放锁。从而导致哪一个都不会释放锁,导致程序卡死。
    避免死锁的条件:
  • 破坏必要条件(预防条件)
    避免死锁算法:
    - 银行家算法
    当一个进程申请使用资源的时候,银行家算法通过先试探分配给该进程资源,然后通过安全性算法判断分配后的系统是否处于安全状态,若不处于安全状态的话试探分配作废,让该进程继续等待。
    在这里插入图片描述
    在银行家算法的进程:
  • 包含进程pi的需求资源数量(也是最大需求资源数量,MAX)
  • 已经分配的给该进程的资源A(Allocation)
  • 还需要的资源数量N(Need=M-A)

Available为空闲资源数量,即资源池,资源池的剩余资源数量+已经分配给所有进程的资源的数量=系统中的资源总量
假设资源P1申请资源,银行家算法先试探的分配给它(当然先要看看当前资源池中的资源数量够不够),若申请的资源数量小于等于Available,然后接着判断分配给P1后剩余的资源,能不能使进程队列的某个进程执行完毕,若没有进程可执行完毕,则系统处于不安全状态(即此时没有一个进程能够完成并释放资源,随时间推移,系统终将处于死锁状态)。
若有进程可执行完毕,则假设回收已分配给它的资源(剩余资源数量增加),把这个进程标记为可完成,并继续判断队列中的其它进程,若所有进程都可执行完毕,则系统处于安全状态,并根据可完成进程的分配顺序生成安全序列(如{P0,P3,P2,P1}表示将申请后的剩余资源Work先分配给P0–>回收(Work+已分配给P0的A0=Work)–>分配给P3–>回收(Work+A3=Work)–>分配给P2–>······满足所有进程)。

  1. 什么是同步和同步的实现
    在数据安全保证的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题。
    同步其实是对临界资源访问合理性的问题,就是我们生产了才能使用,没有资源就一直等待,生产了资源后就唤醒等待。
    通常使用条件变量对线程进行我们的同步操作,条件变量就是当一个线程互斥地访问某一个变量的时候,他可能发现在其他线程改变状态之前,它什么也做不了。
    条件变量接口
  • 条件变量初始化
       int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);
       pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

一个是静态初始化,在定义条件变量的时候就直接初始化,一个是函数初始化,和我们的互斥锁类似。
参数
cond:定义的条件变量的变量
attr:条件变量的属性
返回值:成功返回0,失败返回errno

  • 条件变量销毁
       #include <pthread.h>
       int pthread_cond_destroy(pthread_cond_t *cond);

cond:就是定义的条件变量的变量
返回值,成功返回0,失败返回-1.

  • 等待
      #include <pthread.h>
       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);

分为了定时等待和永久等待。定时等待就是在设定的时间之内进行等待,如果超出时间就报错返回,永久等待就一直等待下去,直到有人唤醒操作。
wait操作不只是简单的等待的操作,包含了解锁后挂起的操作,其实是完成了三个操作
1. 解锁操作 2. 休眠,挂起操作 3. 被唤醒后加锁操作
为什么等待操作需要搭配锁的使用
因为条件变量本身只提供等待与唤醒的功能,具体什么时候等待需要用户来进行判断,这个条件判断,通常涉及到我们队临界资源的操作(其他线程要通过修改条件来促使条件满足),而这个临界资源应该受保护,所以我们此时需要搭配锁的使用

  • 唤醒
       #include <pthread.h>
       int pthread_cond_broadcast(pthread_cond_t *cond);
       int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_broadcast():唤醒所有人
pthread_cond_signal():唤醒至少一个人

根据我们同步与互斥就可以写出我们的生产者和消费者模型,下节见真章。

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

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生 死锁,因此是不可重入的
    可重入和不可重入的线程的安全
    可重入是线程安全的,而不可重入不一定是线程不安全的,在没有对全局或者静态的变量进行我们的操作的时候可能是安全的,需要在具体的场景下进行判断。
    不可重入函数例子:
  • malloc函数
  • 调用标准I/O库函数
    常见可重入情况
  • 不使用全局变量或静态变量 -
  • 不使用用malloc或者new开辟出的空间 -
  • 不调用不可重入函数 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

linux---线程安全(同步与互斥) 的相关文章

  • Notepad++全选一整列的靠谱办法

    遇到行数较少的可以直接按住ALT手动选取 xff0c 但遇到行数较多 xff0c 就得这么干 xff1a 鼠标放在第一行某一列 xff0c 按住Alt 43 Shift xff0c 然后鼠标选择最后一行该列 xff0c 输入内容即可 xff
  • 对象转xml格式工具类

    import com ruiyun gui store haikang haikang bean FCSearchDescription import com ruiyun gui store haikang haikang bean FD
  • 【无标题】MQ静态图片获取

    public void getImageV40 String path Integer buildingProjectId HttpServletResponse response JSONObject param 61 new JSONO
  • 数据加解密时Base64异常:Illegal base64 character 3a

    现象 用base64工具类对中文进行处理时出现异常 xff0c 在数据加解密场景中经常使用 java lang IllegalArgumentException Illegal base64 character 3a at java uti
  • Winsock编程实例---TCP&UDP

    0x1 基于TCP的通信 1 服务端 1 1 创建基本流程 创建一个TCP服务端的程序需要调用的函数流程 xff1a 初始化函数库 gt gt WSAStartup 创建套接字 gt gt socket 绑定套接字 gt gt bind 监
  • 数据结构---选择排序(直接选择排序和堆排序图解)

    选择排序思想 xff1a 每一次从待排序的数据元素中选出最小 xff08 或最大 xff09 的一个元素 xff0c 存放在序列的起始位置 xff0c 直到全部待排序的 数据元素排完 直接选择排序 在元素集合array i array n
  • Java HttpUtils类

    Java HttpUtils类 Java HttpUtils类 定义 Public class HttpUtils 收集HTTP Servlet使用的静态的有效方法 方法 1 getRequestURL public static Stri
  • Ubuntu打造家用NAS三——网盘与影视中心

    Ubuntu打造家用NAS三 网盘与影视中心 一 Ubuntu 挂载硬盘 通过 Putty 连接 NAS查看硬盘位置 xff1a sudo fdisk l找到需要挂载的硬盘 xff0c 我的是 Disk dev sdb xff1a xxx
  • Ardupilot笔记:Rover auto模式下的执行流程

    先从mode auto cpp的update 开始分析 流程如图 xff1a 进入函数update 后会执行函数navigate to waypoint mode auto cpp span class token keyword void
  • 串口通信协议 UART+I2C+SPI

    UART 异步 串行 全双工 I2C SPI 不同通信协议比较 UART UART协议详解 UART通信 xff0c 接收与发送 xff08 详细版 xff0c 附代码 xff09 UART串行通信详解 待整理 UART是Universal
  • c语言---宏

    宏 1 仅仅替换 2 不能定义宏参类型 3 不会检查宏参有没有定义 定义带参数的宏 define JH a b t t 61 a a 61 b b 61 t xff0c 对两个参数a b的值进行交换 xff0c 下列表述中正确的是 xff0
  • Ros安装rosdep update出错,解决办法(从根本入手)

    博主作为一个ros刚入门的新手 xff0c 之前也安装过ros ros2但是在Ros安装在进行rosdep update 时运气与网络是成功的关键 xff0c 在尝试了好多次 xff0c 运气好一次就成功了 xff0c 运气不好得不停的试错
  • vscode使用方法

    01 ctrl 43 u 返回上一个光标焦点 02 发送请求插件 到VSCode插件中搜索REST Client 搜索到 xff0c 点击install进行安装 创建一个 http文件 编写测试接口文件 右键选择 发送请求 xff0c 测试
  • 自主飞行无人机开发--SALM cartographer开源框架 rplidar A2/3

    参考学习网站 xff1a https google cartographer ros readthedocs io en latest 问题提出 xff1a 四旋翼搭载激光雷达A3进行SLAM室内定位 xff0c 其怎样Running Ca
  • C#串口通信中的奇偶性校验、CRC校验函数

    一般来说 xff0c 通信协议中的通用数据格式是 包头 43 指令码 43 数据长度 43 有效数据 43 校验码 43 包尾 其中 xff0c 校验方式有多种 xff0c 最流行的是CRC校验方式 xff0c 其次是简单的奇偶性校验 校验
  • 测试软件安装步骤

    目录 Ja 目录 Java安装 jdk下载 jdk环境配置 phpstudy安装 禅道的安装 xampp安装 postman安装 requests parameterized jmeter安装 JVM监控 Locust SecureCRT软
  • linux---进程信号

    进程的功能以及概念信号的生命周期以及相关的接口自定义信号的捕捉流程信号集以及阻塞信号集了解一个SIGCHLD信号 信号的功能以及概念 信号的功能 xff1a 信号就是通知我们某一个事件的发生 信号的概念 xff1a 信号就是一个软件中断 x
  • 双目立体视觉:四(双目标定matlab,图像校正,图像匹配,计算视差,disparity详解,)

    二郎也比较忙 xff0c 在某大场工作 xff0c 有时候没有时间回复 如果希望二郎尽快帮忙 xff0c 可以将代码 xff0c 数据和问题发给二郎 xff0c 谢谢大家理解 glwang20 64 mails jlu edu cn 不过还
  • conda解决 An HTTP error occurred when trying to retrieve this URL.问题

    遇到 xff1a Collecting package metadata current repodata json failed CondaHTTPError HTTP 000 CONNECTION FAILED for url lt h
  • ubuntu20.04 安装 WPS 2019

    ubuntu自带的文字处理软件对来自windows下office或在WPS创建的ppt有点不兼容 xff0c 看到WPS有linux版本的 xff0c 便果断安装试一试 一 卸载原生liboffice sudo apt get remove

随机推荐