Linux多线程之线程互斥

(。・∀・)ノ゙嗨!你好这里是ky233的主页:这里是ky233的主页,欢迎光临~icon-default.png?t=N7T8https://blog.csdn.net/ky233?type=blog

点个关注不迷路⌯'▾'⌯

目录

一、互斥

1.线程间的互斥相关背景概念

2.互斥锁

三、可重入和线程安全

1.概念

2.常见的线程安全和不安全的问题

3.常见的可重入与不可重入情况

4.可重入与线程安全的联系

5.可重入与线程安全的区别

四、死锁

1.死锁的四个必要条件

2.避免死锁的方法


一、互斥

1.线程间的互斥相关背景概念

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

 接下来验证一个问题,如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响吗?

一个看似没问题的多线程抢票程序

void* getTickets(void* args)
{
    (void)args;
    while(1)
    {
        if(tickets>0)
        {
            usleep(1000);
            printf("%p:%d\n",pthread_self(),tickets);
            tickets--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t t1,t2,t3;
    pthread_create(&t1,nullptr,getTickets,nullptr);
    pthread_create(&t2,nullptr,getTickets,nullptr);
    pthread_create(&t3,nullptr,getTickets,nullptr);
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
}

抢票到最后,不仅仅出现了有两个11张票的情况,甚至还出现了剩余-1张票的情况!所以如果我们不加以保护的话就会引起这种现象

我们用的方式是--,我们都知道--分为三个动作,先在对应的线程把数据读取到CPU中,然后--,最后返回内存,可是如果在还没--的时候cpu进行调度切换了呢?就会导致这个线程认为还有999张票,下一个也是认为999张票,可是下一个线程做完操作了把998返回去了,然后这时候刚刚的线程进来了,那么这个时候就会把之前保存在线程上下文数据中的999返回到CPU中,然后继续没做完的操作并返回998,那么这个时候已经卖出去两张票了,可是剩余的却是998张票!

这样就导致了我们在并发访问的时候,导致了我们数据不一致的问题!

那么要怎么避免这样的问题产生呢!

2.互斥锁

1.用法

pthread_mutex_t mtx;//首先要先定义一把锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
//之后对锁进行初始化操作
  • 参数一:开始定义的锁
  • 参数二:初始化时互斥锁的相关属性设置,传递nullptr使用默认的属性
  • 返回值:成功为0,失败返回错误码
  • 如果是全局或者静态的也可以使用这个宏来初始化:PTHREAD_MUTEX_INITIAILZER

 那么如何进行加锁保护呢?

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 参数一:传入刚刚创建的锁
  • 我们在需要串行化的时候加锁就好了

在我们每个线程进入临界区的时候,就会带上一把锁,这个锁在同一时刻只有一个线程可以拿到,其他没有所得线程只能在这里阻塞等待,直到拿到锁的线程解锁之后,并且拿到锁之后才能进入临界区!这就叫做互斥特点

解锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数一:传入刚刚创建的锁

代码演示:

#include <iostream>
#include <pthread.h>
#include <stdio.h>
#include <errno.h>
#include <thread>
#include <string.h>
#include <unistd.h>

using namespace std;
// 创建锁
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
//临界资源
int tickets = 10000;

void *getTickets(void *args)
{
    (void)args;
    while (1)
    {
        // 加锁
        pthread_mutex_lock(&mtx);
        //临界区
        if (tickets > 0)
        {
            usleep(1000);
            tickets--;
            // 解锁
            pthread_mutex_unlock(&mtx);
            printf("%s:%d\n", (char*)args, tickets);
        }
        else
        {
            // 解锁
            pthread_mutex_unlock(&mtx);
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, getTickets, (void*)"线程1");
    pthread_create(&t2, nullptr, getTickets, (void*)"线程2");
    pthread_create(&t3, nullptr, getTickets, (void*)"线程3");
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
}

加锁的时候一定要保证我们加锁的粒度越小越好!如果是局部的锁,要记得在最后释放

pthread_mutex_destroy(&mtx);

2.加锁之后,线程在临界区是否会被切换呢?会有问题吗?原子性的体现在哪里

答案是会被切换的!但是不会出现上述问题了!虽然我们还是会被切换,但是我们一已经拿到唯一的一把锁了,而且我们又没有释放锁,所以其他线程不能访问临界区!所以也就不能来进入临界区修改资源

原子性体现:在其他线程看来,只有两种情况是有意义的,一种是:没有线程持有锁,这代表的是什么都没做,谁都可以申请锁!第二种:持有锁的线程释放锁了,这也代表的是谁都可以申请锁!所以只有这两种情况队以其他线程来说是有意义的!所以在其他线程来看,当持有锁的线程在进行对应的任务时,这就是原子的!

3.那么加锁就是串行执行了吗?

经过了上面的了解,那么我们对于这个问题也就迎刃而解了,是的,在访问临界区的时候一i当时串行执行的

4.锁本身是否是临界资源呢?

要访问临界资源,每一个线程都必须先看到同一把锁并访问,那么锁本身是否是临界资源呢?那么谁来保证锁的安全呢?

所以为了保证锁的安全,申请和释放锁必须是原子的!!

5.锁是如何实现的

首先先来补充一个背景知识:在执行流视角我们的寄存器的空间是被共享的,但是此村其里面的内容,是被每一个执行流私有的,属于执行流的上下文,切换的时候每个执行流都会带走属于自己的执行流!

第一步:在最开始的时候我们的锁就是个整数比如说是1,其中当我们的指令在执行第一条汇编的时候,把0放到%al寄存器里,这个0是属于我们线程的上下文的

第二步:我们交换把寄存器的值和锁的值相交换,所以寄存器里面的值就变为1,mtx里面的值变为0

第三步:线程来判断寄存器的内容是否是大于0的,如果大于0则返回,代表申请锁成功!如果不大于0,则被挂起等待

在这过程中,是随时都有可能被切换的,如果被切换了,线程1就带着%al中的数据一起被切换了,线程2则要从新开始置0、交换,这个时候mtx里面的数据是0,寄存器里面的数据也是0,交换后还是0,则不满足第三步获取锁的条件,则线程2被阻塞挂起,其他线程也是一样的,直到CPU再次调度回线程1,然后线程1,带着上次带走的数据继续放回寄存器里,然后继续未完成的步骤,直到申请所成功!

也就是说当1被线程1所拿到之后,就相当于这把锁已经被线程1拿到了,而这三部每一步都是原子的,执行每一步的过程中都是不可以被调度的!这样也就相当于获取锁是原子的了!

三、可重入和线程安全

1.概念

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

2.常见的线程安全和不安全的问题

不安全:

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

安全:

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

3.常见的可重入与不可重入情况

不可重入:

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

可重入:

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

4.可重入与线程安全的联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

5.可重入与线程安全的区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生 死锁,因此是不可重入的

四、死锁

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

1.死锁的四个必要条件

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

2.避免死锁的方法

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

canvas实现水印逻辑分析

目录 效果图一、相关文档二、分析三、实现1、将水印文字转为水印图片2、给刚生成的水印图片加入旋转以及间隔&#xff08;1&#xff09;旋转位移&#xff08;2&#xff09;间隔位移&#xff08;3&#xff09;最后使用toDataURL导出为png图片 3、将生成的水印图片依次排布在需要…

Python实现一个简单的主机-路由器结构(计算机网络)

说明 本系统模拟实现了一个路由器与两个主机节点。该路由器将接收原始以太网帧&#xff0c;并像真正的路由器一样处理它们&#xff1a;将它们转发到正确的传出接口&#xff0c;处理以太网帧&#xff0c;处理 IPv4 分组&#xff0c;处理 ARP分组&#xff0c;处理 ICMP 分组&…

Crow 编译和环境搭建

Crow与其说是编译&#xff0c;倒不如说是环境搭建。Crow只需要包含头文件&#xff0c;所以不用编译生成lib。 Crow环境搭建 boost&#xff08;可以不编译boost&#xff0c;只需要boost头文件即可&#xff09;asio &#xff08;可以不编译&#xff0c;直接包含头文件。不能直接…

事务【MySQL】

稍等更新图片。。。。 事务的概念 引入 在 A 转账 100 元给 B 的过程中&#xff0c;如果在 A 的账户已经减去了 100 元&#xff0c;B 的账户还未加上 100 元之前断网&#xff0c;那么这 100 元将会凭空消失。对于转账这件事&#xff0c;转出和转入这两件事应该是绑定在一起的…

C语言——函数指针——函数指针变量(详解)

函数指针变量 函数指针变量的作用 函数指针变量是指向函数的指针&#xff0c;它可以用来存储函数的地址&#xff0c;并且可以通过该指针调用相应的函数。函数指针变量的作用主要有以下几个方面&#xff1a; 回调函数&#xff1a;函数指针变量可以作为参数传递给其他函数&…

Docker基础教程 - 10 常用容器部署-Redis

更好的阅读体验&#xff1a;点这里 &#xff08; www.doubibiji.com &#xff09; 10 常用容器部署-Redis 下面介绍一下常用容器的部署。可以先简单了解下&#xff0c;用到再来详细查看。 在 Docker 中部署 Redis 容器。 10.1 搜索镜像 首先搜索镜像&#xff0c;命令&…

强大的项目管理软件:OmniPlan Pro 4 mac中文版

OmniPlan Pro 4 mac中文版是由The Omni Group为macOS和iOS操作系统开发的一款专业级项目管理软件。它允许用户创建和管理复杂的项目&#xff0c;从定义任务、分配资源到跟踪进度和生成报告&#xff0c;一应俱全。 这款软件提供了一系列强大的工具&#xff0c;帮助用户进行高效…

集合框架(一)Set系列集合

Set<E>是一个接口 特点 无序&#xff1a;添加数据的顺序和获取出的数据顺序不一致&#xff1b;不重复&#xff0c;无索引 注意&#xff1a;Set要用到的常用方法&#xff0c;基本上就是collection提供的!自己几乎没有额外新增一些常用功能! HashSet集合的底层原理 前置知…

GPU 和并行计算

还是那句话&#xff0c;互联网领域遇到的大多数问题&#xff0c;在现实世界早就有了解法&#xff0c;今天再分享一个。 视频来自安阳市最后的朋克&#xff0c;张教练的实拍&#xff0c;视频中展示的是血糕&#xff0c;安阳市特产&#xff0c;不了解的可以将其等同于 “一种必须…

【JavaScript】JavaScript 变量 ① ( JavaScript 变量概念 | 变量声明 | 变量类型 | 变量初始化 | ES6 简介 )

文章目录 一、JavaScript 变量1、变量概念2、变量声明3、ES6 简介4、变量类型5、变量初始化 二、JavaScript 变量示例1、代码示例2、展示效果 一、JavaScript 变量 1、变量概念 JavaScript 变量 是用于 存储数据 的 容器 , 通过 变量名称 , 可以 获取 / 修改 变量 中的数据 ; …

Util工具类功能设计与类设计(http模块一)

目录 类功能 类定义 类实现 编译测试 Split分割字符串测试 ReadFile读取测试 WriteFile写入测试 UrlEncode编码测试 UrlDecode编码测试 StatuDesc状态码信息获取测试 ExtMime后缀名获取文件mime测试 IsDirectory&IsRegular测试 VaildPath请求路径有效性判断测…

Day33-计算机基础3

Day33-计算机基础3 1.根据TCP/IP进行Linux内核参数优化1.1 例1&#xff1a;调整访问服务端的【客户端】的动态端口范围 &#xff0c;LVS&#xff08;10-50万并发&#xff09;&#xff0c;NGINX负载&#xff0c;SQUID缓存服务,1.2 企业案例&#xff1a;DOS攻击的案例&#xff1a…

工资低适合下班做的6大副业,每一个都值得尝试!

2024年是最适合发展个人副业的时候&#xff01;无论你是否有全职工作&#xff0c;如果你的主业还不能满足你的成就感&#xff0c;还不能满足你的生活需求&#xff0c;这6个下班可以做的副业都很值得尝试&#xff01; 千金宝库做简单的网络任务 近年来&#xff0c;随着互联网技…

算法详解——leetcode150(逆波兰表达式)

欢迎来看博主的算法讲解 博主ID&#xff1a;代码小豪 文章目录 逆波兰表达式逆波兰表达式的作用代码将中缀表达式转换成后缀表达式文末代码 逆波兰表达式 先来看看leetcode当中的原题 大多数人初见逆波兰表达式的时候大都一脸懵逼&#xff0c;因为与平时常见的表达式不同&am…

C语言学习笔记,学懂C语言,看这篇就够了!(中)

附上视频链接&#xff1a;X站的C语言教程 目录 第8章、函数8.1 函数是什么8.2 函数的分类8.2.1 库函数8.2.1.1 如何使用库函数 8.2.2 自定义函数 8.3 函数参数8.3.1 实际参数(实参)8.3.2 形式参数(形参) 8.4 函数调用8.4.1 传值调用8.4.2 传址调用8.4.3 练习 8.5 函数的嵌套调…

如何使用ArcGIS Pro进行坡度分析

坡度分析是地理信息系统中一种常见的空间分析方法&#xff0c;用于计算地表或地形的坡度&#xff0c;这里为大家介绍一下如何使用ArcGIS Pro进行坡度分析&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数据是从水经微图中下载的DEM数据&#xff0c;除了DEM数据&…

Python爬虫:http和https介绍及请求

HTTP和HTTPS 学习目标&#xff1a; 记忆 http、https的概念和区别记忆 浏览器发送http请求的过程记忆 http请求头的形式记忆 http响应头的形式了解 http响应状态码 1 为什么要复习http和https 在发送请求&#xff0c;获取响应的过程中 就是发送http或https的请求&#xff0c…

自然语言发展历程

一、基础知识 自然语言处理&#xff1a;能够让计算理解人类的语言。 检测计算机是否智能化的方法&#xff1a;图灵测试 自然语言处理相关基础点&#xff1a; 基础点1——词表示问题&#xff1a; 1、词表示&#xff1a;把自然语言中最基本的语言单位——词&#xff0c;将它转…

中国电子学会2021年9月份青少年软件编程Sc ratch图形化等级考试试卷四级真题

【 单选题 】 1.下面哪个选项程序可以交换下图列表中第2项和第3项的位置&#xff1f; A&#xff1a; B&#xff1a; C&#xff1a; D&#xff1a; 2.雷峰塔景区的门票价格政策是&#xff1a;成人40元/人&#xff1b;6周岁&#xff08;含6周岁&#xff09;以下的实行免票&#…

常用MII接口详解

开放式系统互连 (OSI) 模型 七层开放系统互连 (OSI) 模型中&#xff0c;以太网层 位于最底部两层 - 物理层和数据链路层。 从百兆以太网接口开始 首先是百兆以太网规定的两种接口 介质无关接口 (MII) Media Independent Interface 介质相关接口 (MDI) Medium Depen…