Linux(十)线程安全 上

news2024/11/26 2:45:24

目录

一、概念

二、互斥锁实现互斥

三、条件变量实现同步

        银行家算法

生产者与消费者模型


一、概念

概念:在多线程程序中,如果涉及到了对共享资源的操作,则有可能会导致数据二义性,而线程安全就指的是,就算对共享资源进行操作也不会导致数据二义

实现:如何实现多线程中对共享资源的操作不会出问题

        互斥:通过同一时间对资源访问的唯一性,保证访问安全

                 互斥的实现:互斥锁(读写锁、自旋锁...)

        同步:通过条件控制,让多执行对资源的获取更加合理

                 同步实现:条件变量、信号量

二、互斥锁实现互斥

实现对共享资源的唯一访问

1)互斥锁实现互斥的原理

        本质:就是一个1/0计数器,通过0/1标记资源的访问状态(0-不可访问、1-可访问)

                   在访问资源之前进行加锁(通过状态判断是否可访问,不可访问则阻塞)

                   在访问资源之后进行解锁(将资源状态置为可访问状态,唤醒其他阻塞的线程)

        也有另一种理解:访问资源之前加锁(获取锁资源-获取不到就阻塞)

                                     访问完毕解锁(归还锁资源)

        多个线程想要实现互斥,就必须访问同一个锁才可以,也就意味着锁也是一个共享资源

 互斥锁的操作本身必须是安全的:互斥锁本身计数器的操作时原子操作

2)互斥锁如何实现自身操作安全的原理

 我们知道内存与cpu之间的数据传输:当进行加锁操作时,先将锁中的1置入cpu(先将变量数据从内存加载到cpu)然后才能从cpu中对数据进行处理(转化为0)然后在从cpu中将数据加载到内存指定位置(完成将1替换为0)

然而这样一种操作就会产生问题,如果1从内存加载到cpu还没有将处理后的0加载回内存时,这时切换其他线程运行访问到的锁资源仍然为1,就会继续进行加锁,这无疑是致命的。

所以对于锁资源本身来说,计数器的操作必须是原子性的才可以。

有个指令类似于exchange,功能是交换指定寄存器与内存中的数据

        1、先将指定寄存器中的值修改为0,

        2、将寄存器与内存中的数据进行互换

        3、判断是否符合获取锁的条件或者说判断是否能够加锁

 这样使用exchange指令就可以实现计数器操作为原子操作。

3)接口

互斥锁类型变量      pthread_mutex_t

初始化互斥锁

        int pthread_mutex_init(pthread_mutex_t *mutex,  pthread_mutexattr_t *attr)

                mutex:互斥锁变量的地址        attr:互斥锁变量属性(通常置NULL)

访问资源前加锁

        int pthread_mutex_lock(pthread_mutex_t *mutex)        阻塞加锁(老实人)

        int pthread_mutex_trylock(pthread_mutex_t *mutex)        非阻塞加锁(海王)

访问资源后解锁

        int pthread_mutex_unlock(pthread_mutex_t *mutex)

释放销毁互斥锁

        int pthread_mutex_destroy(pthread_mutex_t *mutex)

        pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER (这种初始化不需要销毁)

4)代码模拟

当不使用互斥锁线程之间对同一变量的访问情况如何?

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>

int ticket = 100;
void *Scalper(void *arg)
{
    while(1)
    {
        if(ticket > 0)
        {
          usleep(10);
          printf("%p:我抢到了第 %d 号票\n",pthread_self(), ticket);
          ticket--;
        }
        else{
          printf("%p:票完了,我的工作结束了\n", pthread_self());
          break;
        }
    }
    return NULL;
}
int main()
{
    pthread_t tid[4];
    for(int i = 0; i < 4; i++)
    {
        int ret = pthread_create(&tid[i], NULL, Scalper, NULL);
        if(ret != 0){
          perror("create error");
          return -1;
        }
    }
    for(int i = 0; i < 4; i++)
    {
        pthread_join(tid[i], NULL);
    }
    return 0;
}

发现出现了很多意外情况,重复、负数票号,

为什么呢?就是因为每个线程之间访问同一变量,一个线程还正对变量进行操作的时候另一个线程也进行操作,于是就发生了同一变量的多次出现。

 使用互斥锁保护临界区

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>

int ticket = 100;
void *Scalper(void *arg)
{
    while(1)
    {
        pthread_mutex_lock(arg);    // 访问之前加锁
        if(ticket > 0)
        {
          usleep(10);
          printf("%p:我抢到了第 %d 号票\n",pthread_self(), ticket);
          ticket--;
          pthread_mutex_unlock(arg);    // 在所有有可能退出的地方解锁
        }
        else{
          printf("%p:票完了,我的工作结束了\n", pthread_self());
          pthread_mutex_unlock(arg);    // 在所有有可能退出的地方解锁
          break;
        }
        usleep(1);
    }
    return NULL;
}
int main()
{
    pthread_mutex_t mutex;    // 定义锁变量 mutex
    pthread_mutex_init(&mutex, NULL);    // 初始化锁资源
    pthread_t tid[4];
    for(int i = 0; i < 4; i++)
    {
        int ret = pthread_create(&tid[i], NULL, Scalper, &mutex); 
                                    // 注意锁资源通过线程创建第四个参数传入入口函数内
        if(ret != 0){
          perror("create error");
          return -1;
        }
    }
    for(int i = 0; i < 4; i++)
    {
        pthread_join(tid[i], NULL);
    }
    return 0;
}

5)死锁

死锁是一种状态,是一种因为资源争抢不当导致程序流程卡死无法继续向下推进的状态。

多个线程对锁资源的争抢使用不当导致程序流程卡死,无法继续向下推进的状态

1、加锁之后没有释放就退出,导致其他线程获取不到锁资源卡死

2、多锁使用时,加锁顺序不当,线程1加锁顺序为AB,线程2加锁顺序为BA

发生死锁的必要条件

        ① 互斥条件        同一时间一把锁只能被一个线程所获取到

        ② 不可剥夺条件        一个线程加的锁,只能自己释放,其他线程无法释放

        ③ 请求与保持        一个线程请求了A锁之后请求B锁,如果请求不到就不会释放A锁

        ④ 环路等待        线程1加了A锁后请求B锁,线程2加了B锁后请求A锁

死锁的预防:破坏死锁产生的必要条件

        ① 和 ② 无法修改

        写代码时要注意:

        1、线程之间的加解锁顺序尽量一致 -- 尽可能预防环路等待

        2、采用非阻塞加锁,如果加不上锁,则把已经加上的锁释放 -- 破坏请求与保持

                (请求不到新的,则释放已有的)

避免:银行家算法、死锁检测算法……

银行家算法http://t.csdn.cn/1YxZj

三、条件变量实现同步

1)概念

同步:通过条件控制,保证资源访问的合理性

条件变量:主要是一个pcb等待队列,以及唤醒和阻塞线程的接口

原理:

        线程1 获取资源时进行判断,如果线程不符合资源获取条件,则调用阻塞接口进行阻塞

        线程2 促使资源获取条件满足之后(生产资源),通过唤醒接口唤醒阻塞的线程

条件变量需要搭配互斥锁来使用

举例:

        顾客 与 厨师 (消费者与生产者)

顾客来到柜台,看到柜台有饭则吃饭,否则阻塞

厨师来到柜台,看到柜台上没有饭则做饭,否则阻塞

顾客:

        0、加锁(关门)

        1、访问柜台有没有饭

                有饭则吃饭,没有饭就阻塞

             (这里阻塞时需要进行解锁,否则厨师就无法访问柜台,也就无法做饭,产生死锁)

                阻塞则需先解锁,再阻塞,被唤醒之后再加锁

        2、吃饭

        3、吃完了,再来一碗       唤醒厨师

        4、解锁

厨师:

        0、加锁

        1、访问柜台有没有饭

                没饭则做饭,有饭则阻塞

                (这里是有饭就需要解锁,让顾客能够吃饭,否则会死锁)

                阻塞需先解锁,再阻塞,被唤醒后加锁

        2、做饭

        3、做好了,唤醒顾客

        4、解锁

2)接口

定义条件变量:

        pthread_cond_t 条件变量的变量类型

初始化条件变量:

        pthread_cond_t cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);

阻塞接口:条件变量是搭配互斥锁一起使用的,就体现在阻塞这一步

        int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex) -- 阻塞接口

        int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex,

                                                  struct timespec *t ) -- 有时长限制的阻塞

唤醒接口:

        int pthread_cond_signal(pthread_cond_t *cond)        唤醒至少一个阻塞队列中的线程

        int pthread_cond_broadcast(pthread_cond_t *cond)        唤醒等待队列中所有的线程

销毁接口:

        int pthread_cond_destroy(pthread_cond_t *cond)

3)模拟实现

#include<stdio.h>
#include<pthread.h>

int counter = 0;    // 定义柜台状态 0——没饭   1——有饭 
pthread_mutex_t mutex;    // 初始化锁
pthread_cond_t cond;    // 初始化条件变量

void* customer(void *arg)
{
    while(1)
    {
      pthread_mutex_lock(&mutex);    // 先加锁
      if(counter==0){
        pthread_cond_wait(&cond, &mutex);    // 没饭则解锁 并阻塞,等待唤醒,唤醒后加锁
      }
      printf("真好吃,再来一碗\n");
      counter = 0;
      pthread_cond_signal(&cond);    // 吃完了唤醒厨师做饭
      pthread_mutex_unlock(&mutex);    // 解锁
    }
}

void* cook(void *arg)
{
    while(1)
    {
      pthread_mutex_lock(&mutex);    // 加锁
      if(counter == 1){
        pthread_cond_wait(&cond, &mutex);    // 有饭则 解锁并阻塞,等待唤醒,唤醒后加锁
      }
      printf("你的饭好了\n");
      counter = 1;
      pthread_cond_signal(&cond);    // 饭做好了唤醒顾客吃饭
      pthread_mutex_unlock(&mutex);    // 解锁
    }
}



int main()
{
    pthread_mutex_init(&mutex, NULL);    // 初始化定义mutex 和 cond
    pthread_cond_init(&cond, NULL);
    pthread_t cook_tid;    // 初始化定义线程ID(顾客和厨师)
    pthread_t cus_tid;
    int ret;
    ret = pthread_create(&cook_tid, NULL, cook, NULL);    // 分别创建对应线程
    if(ret != 0){
      perror("create error");
      return -1;
    }
    ret = pthread_create(&cus_tid, NULL, customer, NULL);
    if(ret != 0){
      perror("create error");
      return -1;
    }

    pthread_join(cook_tid, NULL);    // 执行线程等待
    pthread_join(cus_tid, NULL);
    pthread_mutex_destroy(&mutex);    // 执行mutex与cond的销毁
    pthread_cond_destroy(&cond);
}

 实现四个顾客四个厨师

在同步实现的代码中,如果存在多种角色,就应该定义多个条件变量,各自处于各自的pcb等待队列中。

#include<stdio.h>
#include<pthread.h>

int counter = 0;
pthread_mutex_t mutex;
pthread_cond_t cond_cus;    // 使用不同的条件变量,防止死锁
pthread_cond_t cond_cook;
void* customer(void *arg)
{
  while(1)
  {
    pthread_mutex_lock(&mutex);
    while(counter <= 0){
      pthread_cond_wait(&cond_cus, &mutex);
    }
    printf("真好吃,再来一碗: %d\n",counter);
    counter--;
    pthread_cond_signal(&cond_cook);
    pthread_mutex_unlock(&mutex);
  }
}

void* cook(void *arg)
{
  while(1)
  {
    pthread_mutex_lock(&mutex);
    while(counter > 0){
      pthread_cond_wait(&cond_cook, &mutex);
    }
    printf("你的饭好了:%d\n", counter);
    counter++;
    pthread_cond_signal(&cond_cus);
    pthread_mutex_unlock(&mutex);
  }
}


int main()
{
  pthread_mutex_init(&mutex, NULL);
  pthread_cond_init(&cond_cus, NULL);
  pthread_cond_init(&cond_cook, NULL);
  pthread_t cook_tid[4];    // 定义四个顾客、四个厨师
  pthread_t cus_tid[4];
  int ret;
  for(int i = 0; i < 4; i++) // 创建这八个线程
  {
    ret = pthread_create(&cook_tid[i], NULL, cook, NULL);
    if(ret != 0){
      perror("create error");
      return -1;
    }
    ret = pthread_create(&cus_tid[i], NULL, customer, NULL);
    if(ret != 0){
      perror("create error");
      return -1;
    }
  }

  pthread_join(cook_tid[0], NULL);
  pthread_join(cus_tid[0], NULL);
  pthread_mutex_destroy(&mutex);
  pthread_cond_destroy(&cond_cook);
  pthread_cond_destroy(&cond_cus);
}

 生产者与消费者模型

生产者与消费者模型icon-default.png?t=N176http://t.csdn.cn/GvZlZ

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MFC】菜单与状态栏(15)

菜单 一般菜单的使用步骤&#xff1a; 1.编辑菜单资源&#xff0c;设置菜单属性&#xff08;包括菜单名和ID&#xff09;&#xff1b; 2.用ClassWizard自动映射菜单消息和成员函数&#xff1b; 3.手工编辑成员函数&#xff0c;加入菜单消息处理代码。 单文档窗口可以设置默…

2年时间,涨薪20k,想拿高薪还真不能老老实实的工作...

2016年开始了我的测试生活。 2016年刚到公司的时候&#xff0c;我做的是测试工程师。做测试工程师是我对自己的职业规划。说实话&#xff0c;我能得到这份工作真的很高兴。 来公司的第一个星期&#xff0c;因为有一个项目缺人&#xff0c;所以部门经理提前结束了我的考核期&a…

C语言预处理

文章目录 目录 文章目录 前言 一、程序编译的过程 二、编译阶段 1.预处理(*.i&#xff09; 2.编译(*.s) 3.汇编(*.o) 4.链接 总结 前言 提示&#xff1a;使用vs code(gcc编译器)与vs2022来演示c语言的预处理 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面…

KEIL编译报错,解决方法汇总

目录 背景 最近在跟着野火码uCosiii的代码时&#xff0c;感觉非常完美&#xff0c;结果一编译&#xff0c;报了120个莫名其妙的问题&#xff0c;下面是踩过的坑&#xff0c;一起记录下&#xff0c;免得下次又掉进去了~ 1. 编译汇编文件&#xff0c;报错 error: unexpected t…

看海泰方圆类ChatGPT技术模型!

ChatGPT&#xff0c;上线2个月便以破亿的用户群引爆了全网。 ChatGPT是由OpenAI公司开发的AI聊天机器人程序&#xff0c;于2022年11月底推出&#xff0c;能够通过学习和理解人类的语言来进行对话、互动&#xff0c;甚至能完成撰写邮件、视频脚本、文案、翻译、代码等任务&…

南卡和JBL无线蓝牙耳机哪款更值得买?横向评测后秒懂差距!

蓝牙耳机想必大家都很熟悉&#xff0c;无论是商务办公还是休闲娱乐&#xff0c;它都起到了至关重要的作用。但蓝牙耳机发展速度太快&#xff0c;耳机品质也参差不齐&#xff0c;最近看到大多数人都有一个疑问&#xff1a;买什么蓝牙耳机比较好&#xff1f;作为一个资深的耳机爱…

动态规划:状态压缩DP(2)(P1896 互不侵犯 ,矩阵计数)

文章目录互不侵犯矩阵计数互不侵犯 题目传送门 在NN的棋盘里面放K个国王&#xff0c;使他们互不攻击&#xff0c;共有多少种摆放方案。国王能攻击到它上下左右&#xff0c;以及左上左下右上右下八个方向上附近的各一个格子&#xff0c;共8个格子。 升级版的八皇后问题&#xf…

(一)随处可见的LED广告屏是怎么工作的呢?

笔者前段时间项目上正好用到一块32*128分辨率的单色LED&#xff0c;正式介入开发的时候&#xff0c;才发现网上的资料少之又少&#xff0c;供应商也给不出有价值的参考信息。故打算分成三篇详细的介绍下开发过程中遇到的问题。本篇主要介绍LED屏幕的一些基础信息&#xff0c;第…

C#互联网医院源码 智慧医院小程序源码 在线问诊在线开方源码

互联网医院平台源码 智慧医院管理系统源码 开发环境&#xff1a;ASP.NET C# VS2019 SQL2008 依托于实体医院利用互联网技术对接院内业务信息系统&#xff0c;向患者提供基于线上问诊、预约挂号、缴费结算、医患互动、诊后随访、健康科普和复诊等全面的医疗健康互联网服务。…

关于apifox和postman接口工具我有话要说~~

Apifox 和 Postman 都是流行的接口测试工具&#xff0c;各自有其优势和缺点。Apifox 的优势在于它提供了强大的可视化界面&#xff0c;可以方便地测试和监控 API。它还支持多种请求方式&#xff0c;并且支持对请求和响应进行代码生成。但是&#xff0c;Apifox 的缺点在于它不太…

详解指针(1)(初阶版)

前言&#xff1a;本篇是详解指针&#xff08;1&#xff09;&#xff0c;内容包括&#xff1a;指针是什么&#xff0c;指针和指针类型&#xff0c;野指针 part 1&#xff1a;指针是什么 1 指针就是地址&#xff0c;口语中说的指针通常指的是指针变量 2 指针变量&#xff1a;存…

shiro721——CVE-2019-12422

这两个漏洞主要区别在于Shiro550使⽤已知密钥碰撞&#xff0c;后者Shiro721是使⽤ 登录后rememberMe {value}去爆破正确的key值 进⽽反序列化&#xff0c;对⽐Shiro550条件只要有 ⾜够密钥库 &#xff08;条件⽐较低&#xff09;、Shiro721需要登录&#xff08;要求⽐较⾼鸡肋 …

Unity基于GraphView的行为树编辑器

这里写自定义目录标题概述基于GitHub上&#xff1a;目前这只是做了一些比较基础的功能节点开发&#xff0c;仅仅用于学习交流&#xff0c;非完成品。项目GitHub连接&#xff1a;[https://github.com/HengyuanLee/BehaviorTreeExamples](https://github.com/HengyuanLee/Behavio…

GNSS 精密钟差产品介绍与DCB改正详解

文章目录前言参考前言 IGS 提供的 GNSS 轨道产品和钟差产品的解算基准并非完全一样, 对于精密产品&#xff0c;各 GNSS 系统的参考基准均为双频无电离层组合&#xff1b;对于广播星历&#xff0c;则区分 GPS 类卫星(GPS,Galileo,QZSS) 基于双频无电离组合的伪距以及 BDS 卫星系…

CAPL(vTESTStudio) - DoIP - TCP发送_05

TCP发送 参数定义 版本号:02 FD or 01 FE or 其他任意值数据类型:00 05 or 00 06 or 80 01 or其他任意值数据长度:想要发送的任意长度

Node.js http 模块详解:request 对象

前言 前文介绍了 http 模块的基本用法&#xff0c;主要就是调用 createServer 和 listen 方法来创建和启动服务。要处理具体的 HTTP 请求&#xff0c;就要在 createServer 方法中写点什么。本文来介绍处理请求的两个核心对象之一的 request 。 URL HTTP 协议最早设计出来&am…

车载TBOX嵌入式设备软件的性能测试

作者 | 李伟 上海控安安全测评中心安全测评部总监 来源 | 鉴源实验室 01 ECU软件和通用软件性能测试的区别 通用软件进行性能测试时通常会通过压力测试、负载测试、稳定性测试、疲劳强度测试、用户并发访问测试等等方法来了解当前软件系统的各项性能指标数据&#xff0c;并在…

【mybatis】实现分页查询

一 .使用原生分页器的实体类 1.1 java代码部分 方法多 不易书写 package cn.bdqn.entity;public class Page {private Integer pageIndex;//页码private Integer pageSize;//页大小 显示多少行数据private Integer totalCounts;//数据的总行数private Integer totalPages;//…

docker安装部署dragonfly2镜像加速服务

Dragonfly安装部署文档 ​ Dragonfly 作为龙蜥社区的镜像加速标准解决方案&#xff0c;是一款基于 P2P 的智能镜像和文件分发工具。它旨在提高大规模文件传输的效率和速率&#xff0c;最大限度地利用网络带宽。在应用分发、缓存分发、日志分发和镜像分发等领域被大规模使用。 …

鸟哥的Linux私房菜读书笔记:Linux磁盘与文件系统管理

系统管理员很重要的任务之一就是管理好自己的磁盘文件系统&#xff0c; 每个分区不可太大也不能太小&#xff0c; 太大会造成磁盘容量的浪费&#xff0c; 太小则会产生文件无法储存的困扰。 前面谈到的文件权限与属性中&#xff0c; 这些权限与属性分别记录在文件系统的哪个区块…