Linux基础内容(26)—— 线程的互斥

news2024/11/25 16:56:49

Linux基础内容(25)—— 线程控制和线程结构_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/131372872?spm=1001.2014.3001.5501

目录

1.线程互斥

1.问题引入

 2.问题原因

3.安全问题

互斥

加锁

加锁后的特点

如何理解锁

原子性的理解

加锁原则

4.互斥锁的原理

5.锁的封装

2.线程安全与可重入函数

1.重入

1.不可重入

2.可重入

2.线程安全问题情况

1.线程不安全

2.线程安全

3.线程安全与可重入函数关系

4.死锁

1.死锁的必要条件

2.破坏死锁的条件


1.线程互斥

1.问题引入

我们设计一个类似的购票程序,其思路就是有10000个ticket,我们生成几个线程进行抢票。如果购票到0,说明此时没有票。但是如果执行下面的逻辑进行抢票,那么会出现抢到负值。这就是当前线程的问题。

int tickets = 10000;
void *get_tickets(void *args)
{
    std::string username = static_cast<const char *>(args);
    while (true)
    {
        if (tickets > 0)
        {
            usleep(1234); // 模拟真实抢票花费时间
            std::cout << username << "正在抢票:" << tickets << std::endl;
            tickets--;
        }
        else
            break;
    }
    return nullptr;
}

当然这个设计有一些前提:

为了出现我们想要的问题,就需要尽可能的让多个线程交叉执行,多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换,线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。线程是在什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换

 2.问题原因

1.其实当ticket为1时,多个进程能同时进入函数进行判断。这是因为当进行判断时,cpu会读取内存中的数据,随后再判断。

2.cpu如果只有一个,那么其实多个线程进行判断后,就不能进入到判断内部,因为线程和cpu处理一一对应。

3.那么我们能知道真正错误的原因是:当一个线程usleep后,线程的上下文就会被存储挂起。那么多个线程进入后,执行usleep语句进行挂起后,这些线程都认为自己存储的ticket为1。随这usleep时间到了,那么此时多线程的执行语句仍然在函数中,那么将内存当前的ticket取出,随后对ticket进行-1。那么以此往复,就出现了负数的ticket。

3.安全问题

线程的共享变量进行++/--操作时,其实只是C语言层面只有一行指令,但是对标CPU其实至少有三条:1.将内存的数据放入cpu 2.cpu中的数据进行逻辑算数运算 3.将运算得到的数据返回给内存 。那么上面的问题产生就是因为2和3操作之间,线程切换了,cpu的资源就被挂起。也就是说一旦一个线程挂起,另一个线程执行当前的共享资源。随这另一个线程完成,挂起的线程苏醒后操作就会出现所谓的安全问题。

互斥

1.多个执行流进行安全访问的共享资源为临界资源

2.我们把多个执行流访问临界资源的代码为临界区

3.想要多个线程串行访问共享资源的操作就是互斥

4.如果一个资源进行访问,要么做就做完,要么不做,该性质被称为原子性

加锁

当前的问题解决为加锁

pthread_mutex_init:局部锁的创建

如果锁位全局或者静态的,初始化直接pthread_mutex_t的变量初始化为PTHREAD_MUTEX_INITALIZER

如果是局部的,需要最后进行pthread_mutex_destory

pthread_mutex_lock:加锁

pthread_mutex_unlock:解锁

int tickets = 10000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void *get_tickets(void *args)
{
    std::string username = static_cast<const char *>(args);
    while (true)
    {
        pthread_mutex_lock(&lock);
        if (tickets > 0)
        {
            usleep(1234); // 模拟真实抢票花费时间
            std::cout << username << "正在抢票:" << tickets << std::endl;
            tickets--;
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    return nullptr;
}

 只不过当前的执行流并不是轮换进行的。

加锁后的特点

1.加锁和解锁的过程是多个线程串行执行的,以至于代码的执行速度变低

2.锁只规定互斥访问,并没有规定让谁优先执行,所以锁直接就是竞争关系

如何理解锁

1.锁需要被所有的线程看见才能使用,那么锁的使用也就意味着锁是共享资源

2.锁是用于保护线程安全保护全局资源的,那么锁也理所应当要被保证安全,加锁也必须是安全的

3.加锁的过程是原子性的,要加锁则要么一定成功,要么一定失败

4.如果线程在申请加锁时暂时失败,那么我们的执行流会被阻塞,直到锁被释放,并且该线程得到锁,随后继续进行执行流的工作。谁持有锁,谁进入临界区

原子性的理解

1.原子性就是只有两种执行的结果,要么做就做完,要么不做

2.其实有一种很直观的原子性代码,就是站在cpu角度的编译代码只有一行的,轻易体现出要做就做完的原子性原则

3.相互竞争锁的线程来说,当前一个线程申请锁成功,那么也就意味着其他竞争的线程只能被挂起等待处在阻塞状态;当然申请锁成功的线程也可以随时被cpu切换走,不过此时的线程会将锁一起带走被切换,那么对应竞争的线程而言,依然没有锁,依然阻塞等待,直到申请成功的线程释放锁。对于其他竞争的线程而言,其实只有两态:有锁/没有锁。那么对于其他的线程而言其实也是原子性的。

加锁原则

1.加锁的粒度要低,优化时间性能

2.加锁必须一视同仁,而不是一部分加锁一部分不加针对同一份公共资源

4.互斥锁的原理

1.加锁的本质其实是原子性的

2.在编译中,存在一条swap或者exchange语句,只需要这一条就可以将cpu的寄存器里的值与内存中的值相互转换

3.寄存器只有一套,被所有执行流共享的。cpu的执行流在cpu的寄存器中的内容是私有的,称为运行时的上下文

4.加锁:线程进入时,线程会将自己的锁状态填入cpu的寄存器,随后要加锁时,直接swap或者exchange一条汇编语句交换,此时如果内存中有锁,那么交换后,线程就将锁纳入了线程的上下文。如果此时断开,线程也会将锁一起带走挂起,那么其他线程进行相同的载入和拿取时判断直到锁并没有申请成功,自然线程不会进行下面的操作,使得原子性得到体现

5.解锁:就是将锁归还到内存,这样就使得其他的线程能调度锁了

5.锁的封装

// RAII风格的加锁
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock_p)
    :lock_p_(lock_p)
    {}

    void lock()
    {
        if(lock_p_)
            pthread_mutex_lock(lock_p_);
    }

    void unlock()
    {
        if(lock_p_)
            pthread_mutex_unlock(lock_p_);
    }

    ~Mutex()
    {}
private:
    pthread_mutex_t *lock_p_;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex)
    :mutex_(mutex)
    {
        mutex_.lock(); //构造函数中进行加锁
    }

    ~LockGuard()
    {
        mutex_.unlock(); //析构函数后自动解锁
    }
private:
    Mutex mutex_;
};
void *get_tickets(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    while (true)
    {
        LockGuard lockguard(td->mutex_p_);
        {
            if (tickets > 0)
            {
                usleep(1234); // 模拟真实抢票花费时间
                std::cout << td->threadname_ << "正在抢票:" << tickets << std::endl;
                tickets--;
                //pthread_mutex_unlock(td->mutex_p_);
            }
            else
            {
                //pthread_mutex_unlock(td->mutex_p_);
                break;
            }
        }
        // 抢票完了还有其他工作
        usleep(1000); // 线程生成订单
    }
    return nullptr;
}

2.线程安全与可重入函数

1.重入

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

1.不可重入

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

2.可重入

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

2.线程安全问题情况

1.线程不安全

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

2.线程安全

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


3.线程安全与可重入函数关系

1.是否为可重入函数针对函数,线程是否安全针对线程,其实二者并没有直接的关系。线程安全一定是必须保证的,而可重入函数需要根据需求进行设计

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

3.可重入函数是线程安全函数的一种

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

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

4.死锁

1.在线程之间存在多把锁的情况下,不同的线程申请成功不同的锁,一旦互相之间需要申请对方的锁。那么此时就会出现线程互相等待对方的锁,造成双方都挂起,进而存在死锁的问题。

2.一个线程申请成功一个锁,此后忘记自己存在锁,随后又申请同样的锁,那么此时自己等待自己释放锁的奇怪现象出现导致单一线程的单一锁出现死锁问题。

1.死锁的必要条件

1.互斥 -- 锁的基本条件

2.请求与保持 -- 线程自己需要锁但是不会释放线程自己的锁

3.不剥夺 -- 需要线程自愿释放自己的锁

4.环路等待条件 -- 线程互现申请成功锁并且需要对方的锁

2.破坏死锁的条件

1.互斥不允许破坏

2.破坏请求与保持,只需要在申请另一个锁失败时,自动释放原先自己拥有的锁

3.破坏不剥夺,根据优先级来剥夺优先级低的线程的锁

4.破坏环路等待,防止环路的语句

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

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

相关文章

每天一道C语言编程:Cylinder(圆柱体问题)

题目描述 使用一张纸和剪刀&#xff0c;您可以通过以下方式切出两个面形成一个圆柱体&#xff1a; 水平切割纸张&#xff08;平行于较短的边&#xff09;以获得两个矩形部分。 从第一部分开始&#xff0c;切出一个最大半径的圆。圆圈将形成圆柱体的底部。 将第二部分向上滚动&…

2023网络安全面试题汇总(附答题解析+配套资料)

随着国家政策的扶持&#xff0c;网络安全行业也越来越为大众所熟知&#xff0c;相应的想要进入到网络安全行业的人也越来越多&#xff0c;为了更好地进行工作&#xff0c;除了学好网络安全知识外&#xff0c;还要应对企业的面试。 所以在这里我归总了一些网络安全方面的常见面…

牛顿修正法在二阶近似方法中的应用

使用optimtool的牛顿修正法来应用学习 pip install optimtool --upgrade pip install optimtool>2.4.2optimtool包所依据的理论支撑中&#xff0c;还没有为二阶微分方法作邻近算子的近似与修正&#xff0c;所以二阶近似方法是研究无不可微项的可微函数的算子。 牛顿修正法…

docker部署mysql8主从集群(互为主从),keepalived保证高可用

一、准备2台物理机器master-1、master-2&#xff0c;目标虚拟VIP。   VIP:172.50.2.139   master-1:172.50.2.41   master-2:172.50.2.42 二、然后分别在2台物理机器master-1、master-2上使用docker-compose安装mysql8&#xff0c;并配置互为主从。1&#xff09;配置mas…

2023-7-19-第二十式迭代器模式

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f4a5;&#x1f4a5;&#x1f4a5;欢迎来到&#x1f91e;汤姆&#x1f91e;的csdn博文&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f49f;&#x1f49f;喜欢的朋友可以关注一下&#xf…

深入理解Java虚拟机(五)虚拟机类加载机制

代码编译的结果从本地机器码转变为字节码&#xff0c;是存储格式发展的一小步&#xff0c;却是编程语言发展的一大步。 一、概述 Java虚拟机把描述类的数据从Class文件加载到内存&#xff0c;并对数据进行校验、转换解析和初始化&#xff0c;最终形成可以被虚拟机直接使用的Jav…

队列的表示和操作

队列&#xff1a;队列是仅在表尾进行插入操作&#xff0c;在表头进行删除操作的线性表。 表尾即an端&#xff0c;称为队尾&#xff0c;表头即a1端&#xff0c;称为队头。 队列的存储方式&#xff1a;顺序队列和链式队列 队列顺序表示 #define MAXQSIZE 100 //最大队列长度 …

Vue 1 - 安装、快速上手

文章目录 关于 Vue创建应用 关于 Vue 官网&#xff1a; https://cn.vuejs.org官方文档-快速上手&#xff1a; https://cn.vuejs.org/guide/quick-start.htmlgithub : https://github.com/vuejsVue SFC Playground : https://play.vuejs.org/ 其他 作者 尤雨溪 Evan You 主页 …

如何把caj文件改成PDF格式?分享三个免费的方法!

在学术研究中&#xff0c;我们可能会遇到CAJ文件&#xff0c;这是一种在中国学术界广泛使用的文档格式。然而&#xff0c;与PDF文件相比&#xff0c;CAJ文件的查看和分享并不那么便捷。下面&#xff0c;我会为你介绍三种免费且简便的方法&#xff0c;帮助你将CAJ文件转化为PDF格…

PSP - Jackhmmer 搜索 EMBL 序列数据库的相似序列

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/131817060 EMBL (European Molecular Biology Laboratory&#xff0c;欧洲分子生物实验室)&#xff1a;EMBL 数据库是一个由欧洲生物信息学研究所…

Microsoft Outlook如何定时发送邮件

点击New Emai 选择Options→Delay Delivery→Do not deliver before→Close

基于linux下的高并发服务器开发(第二章)- 2.13 匿名管道通信案例

实现 ps aux | grep xxx 父子进程间通信 子进程&#xff1a; ps aux, 子进程结束后&#xff0c;将数据发送给父进程 父进程&#xff1a;获取到数据&#xff0c;过滤 pipe() execlp() 子进程将标准输出 stdout_fileno 重定向到管道的写端。 dup2 07 / 匿名管道…

java项目之班级同学录网站(ssm+mysql+jsp)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的班级同学录网站。技术交流和部署相关看文章末尾&#xff01; 开发环境&#xff1a; 后端&#xff1a; 开发语言&#xff1a;Java 框架&a…

强化学习之DQN(deep Q-network)算法

一、简介 DQN算法是深度学习领域首次广泛应用于强化学习的算法模型之一。它于2013年由DeepMind公司的研究团队提出&#xff0c;通过将深度神经网络与经典的强化学习算法Q-learning结合&#xff0c;实现了对高维、连续状态空间的处理&#xff0c;具备了学习与规划的能力。 二、…

使用Pandas计算两个系统客户名称的相似度

引言&#xff1a; 在日常业务处理中&#xff0c;我们经常会面临将不同系统中的数据进行匹配和比对的情况。特别是在涉及到客户管理的领域&#xff0c;我们需要确保两个系统中的客户记录是准确、一致和无重复的。 本文将介绍如何使用Python的Pandas库来处理这个问题。我们将以…

Linux: cannot read file data

报错&#xff1a; Could not load library libcudnn_cnn_infer.so.8. Error: /home/qc/miniconda3/envs/DNAqc/lib/python3.10/site-packages/torch/lib/libcudnn_cnn_infer.so.8: cannot read file data Please make sure libcudnn_cnn_infer.so.8 is in your library path! A…

淘宝商品详情数据接口(APP端,H5端),监控淘宝商品历史价格及价格走势,接口代码对接

淘宝商品详情数据接口APP端&#xff0c;H5端代码如下&#xff1a; 公共参数 名称类型必须描述交流中心18179014480keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;注册Key和secret接入secretString是调用密钥api_nameString是API接口名称&#xff08;包括…

JMeter做http接口功能测试

1. 普通的以key-value传参的get请求 e.g. 获取用户信息 添加http请求&#xff1b;填写服务器域名或IP&#xff1b;方法选GET&#xff1b;填写路径&#xff1b;添加参数&#xff1b;运行并查看结果。 2. 以Json串传参的post请求 e.g. 获取用户余额 添加http请求&#xff1b;…

行业数据和报告到底应该如何去找?

信息时代&#xff0c;经常要对行业信息进行分析。这时首先就是要进行信息收集和筛选&#xff0c;如果我们懂得构建自己的工作工具和数据来源&#xff0c;效率会蹭蹭往上涨。 找行业报告、了解行业趋势&#xff0c;提高效率。 1. 国家权威 国家统计局&#xff1a;这个网站覆盖…

C++无锁编程——无锁队列(queue)

C无锁编程——无锁队列(queue) 贺志国 2023.7.11 上一篇博客给出了最简单的C数据结构——栈的几种无锁实现方法。队列的挑战与栈的有些不同&#xff0c;因为Push()和Pop()函数在队列中操作的不是同一个地方。因此同步的需求就不一样。需要保证对一端的修改是正确的&#xff0…