013 Linux_互斥

前言

本文将会向你介绍互斥的概念,如何加锁与解锁,互斥锁的底层原理是什么

线程ID及其地址空间布局

在这里插入图片描述

每个线程拥有独立的线程上下文:一个唯一的整数线程ID, 独立的栈和栈指针,程序计数器,通用的寄存器和条件码。
和其他线程共享的进程上下文的剩余部分:整个用户虚拟地址空间,那就是上图的数据段,堆以及所有的共享库代码和数据区域,也共享所有打开文件的集合。
pthread_create函数会产生一个线程id,存放到第一个参数指向的地址中,如果你将这个id打印出来会发现特别大,其实这串数字是一个地址,这个地址就是一个虚拟地址,这样在主线程产生的临时数据都压在系统栈上,而其他线程则存储在pthread库提供的栈内。
这里要注意的是线程的寄存器的内容是不共享的,通常栈区是被相应线程独立访问的,但是还是可能出现一个线程去访问另一个线程中的栈区的情况。如果这个线程获得了指向另一个线程栈区的指针,那么它就可以读写这个栈的任何部分。

互斥相关概念

临界资源:多线程执行流共享的资源就叫做临界资源 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
互斥量: 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个 线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之 间的交互。
多个线程并发的操作共享变量,会带来一些问题。

模拟抢票

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using namespace std;
#define NUM 4
class threadData
{
public:
    //构造函数
    threadData(int number)
    {
        threadname = "thread-" + to_string(number);
    }
public:
    string threadname;      //线程名
};
int tickets = 1000;
void* getTicket(void *args)
{
    //安全类型转换
    threadData *td = static_cast<threadData *>(args);
    const char *name = td->threadname.c_str();
    while(tickets)
    {
        if(tickets > 0)
        {     
            usleep(10000);
            printf("who=%s, get a ticket: %d\n", name, tickets);
            tickets--; 
 
        }
        else break;
    }
    printf("%s ... quit\n", name);
    return nullptr;
}
 int main()
{
    vector<pthread_t> tids;
    vector<threadData *> thread_datas;
    for(int i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        //构造一个对象指针
        threadData *td = new threadData(i);
        //存放对象指针
        thread_datas.push_back(td);
        //创建线程,将对象指针作为参数传递给getTicket
        pthread_create(&tid, nullptr, getTicket, thread_datas[i-1]);
        //管理线程id
        tids.push_back(tid);
    }
    //线程等待
    for(auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }
    for(auto td : thread_datas)
    {
        delete td;
    }
    return 0;
}

在这里插入图片描述

现象:明明存在对tickets的条件判断,可是票数依旧被抢到了负数

在这里插入图片描述

原因是:多个线程可能同时检查tickets大于0,然后同时减少tickets的值。这可能导致tickets的值减少到负数,因为每个线程都可以执行tickets–操作,即使tickets已经为0。
从底层来看:
Ticket–这一步骤在汇编上是三条代码

1、先将tickets读入到cpu的寄存器中
2、cpu内部进行–操作
3、将计算结果的数据写回内存

举个例子
在这里插入图片描述
引入一个概念:线程在执行的时候,将共享数据,加载到CPU寄存器的本质:把数据的内容,变成了自己的上下文—以拷贝的方式,给自己单独拿了一份(也就是说会将数据保存到自己的上下文当中)

1、当执行线程一的时候,倘若线程一刚执行完第一步就被切走
2、假设线程2一直能执行完3个步骤,重复执行,没有被中断,最终票数被减到10了,当刚要执行第一步骤的时候此时被切换,此时将减到10的数据保存到自己的上下文当中
3、切换到线程一,并不是紧接着执行第二步,而是恢复自己的上下文数据1000到CPU当中,它认为数据是1000,最后将计算出来的999写回内存,此时线程二先前将票数减到10的工作白做了
4、而此时如果再次切换到线程二,线程二再将内存中的tickets读取到寄存器当中(票数就又变成了999),因此可以看出:多个线程并发的操作共享变量,会带来一些问题,是线程切换导致的
我们也可以看出无论是–还是++其实都不是原子性的,因为它们会在cpu调度是被打断。

互斥锁

为了解决上述抢票的问题(共享数据被读哦现场并发访问造成数据不一致的问题)
需要做到三点:

代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
在这里插入图片描述

加锁:模拟抢票

引入函数接口

互斥锁
方法1,静态分配:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法2,动态分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
参数:
mutex:要初始化的互斥量
attr:NULL

销毁互斥量需要注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误码

调用 pthread_ lock 时,可能会遇到以下情况: 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。


#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using namespace std;
#define NUM 4

class threadData
{
public:
    //构造函数
    threadData(int number, pthread_mutex_t *mutex)
    {
        threadname = "thread-" + to_string(number);
        lock = mutex;
    }
public:
    string threadname;      //线程名
    pthread_mutex_t *lock;
};
int tickets = 500;
void* getTicket(void *args)
{
    //安全类型转换
    threadData *td = static_cast<threadData *>(args);
    const char *name = td->threadname.c_str();
    while(true)
    {
   		//上锁
        pthread_mutex_lock(td->lock); 
        if(tickets > 0)
        {        
            usleep(1000);
            printf("who=%s, get a ticket: %d\n", name, tickets);
            tickets--; 
            pthread_mutex_unlock(td->lock);
        }
        else
        {
        	//解锁
            pthread_mutex_unlock(td->lock);
            break;
        }
    }
    printf("%s ... quit\n", name);
    return nullptr;
}
 int main()
{
    vector<pthread_t> tids;
    vector<threadData *> thread_datas;
    pthread_mutex_t lock;
    //初始化锁
    pthread_mutex_init(&lock, nullptr);
    for(int i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        //构造一个对象指针
        threadData *td = new threadData(i, &lock);
        //存放对象指针
        thread_datas.push_back(td);
        //创建线程,将对象指针作为参数传递给getTicket
        pthread_create(&tid, nullptr, getTicket, thread_datas[i-1]);
        //管理线程id
        tids.push_back(tid);
    }
    //线程等待
    for(auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }
    for(auto td : thread_datas)
    {
        delete td;
    }
    return 0;
}

现象:上锁了,但是票数都是由一个线程抢走了
在这里插入图片描述
为什么会有这样的现象呢?
故事时间

在纯互斥环境里,如果锁分配不够合理,容易导致其它线程的饥饿问题
比如:存在一个独立自习室,规矩:出去后,必须把钥匙放到指定的位置。倘若自习室里的人需要去吃饭,然后出门,看到一大堆人在等他出来,然后他又进去了,因为他不想失去这把🔑,但是由于他距离门更近一些,因此他对钥匙的竞争更强一些
我们应该对此再加一个规矩:出来的人,不能立马重新申请锁,想要继续申请,必须排到队列的最后面,外面来的,必须排队

了解上述的故事之后,我们可以对代码进行如下修改:
在这里插入图片描述

在这里插入图片描述

互斥量实现原理探究

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下
在这里插入图片描述
1、当线程切换时,线程要把它的上下文数据带走(即把在寄存器中的值拷贝一份),还要记录执行到哪一个位置了
2、当线程一执行完第一步就被切换,首先把0保存到自己的上下文当中,回来的时候要执行xchgb
3、 线程二来了:把0mov到寄存器里,让后与内存中的mutex=1作交换(此时内存中的值为0,cpu寄存器的值为1)当正要做判断的时候,被切换了,线程二要把寄存器中的内容带走,并记录即将执行if语句
4、线程一回来了:首先要恢复上下文数据,将0又恢复到寄存器里,然后执行交换,发现 跟内存交换完后,依旧是0
5、原因是线程二已经拿走了1,线程一申请锁失败,不会被调度,线程二再恢复寄存器中的数据,继续进行判断大于0,申请锁成功
锁本身就是共享资源,放在内存里这个数据(仅有一把锁)就是被所有线程共享的

在这里插入图片描述
此时持有锁的线程再将mutex中的值(此时为0)与1交换
在这里插入图片描述

小结

今日的分享就到这里啦,如果本文存在疏漏或错误的地方,还请您能够指出!
在这里插入图片描述

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

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

相关文章

【Python】成功解决IndexError: list index out of range

【Python】成功解决IndexError: list index out of range &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希望得到您的订…

整除光棍(pta团体天梯练习题)模拟手算除法c++

这里所谓的“光棍”&#xff0c;并不是指单身汪啦~ 说的是全部由1组成的数字&#xff0c;比如1、11、111、1111等。传说任何一个光棍都能被一个不以5结尾的奇数整除。比如&#xff0c;111111就可以被13整除。 现在&#xff0c;你的程序要读入一个整数x&#xff0c;这个整数一定…

朴素贝叶斯 | 多分类问题

目录 一. 贝叶斯公式的推导二. 朴素贝叶斯1. 离散的朴素贝叶斯朴素贝叶斯导入示例 离散的朴素贝叶斯训练 2. 连续的朴素贝叶斯3. 伯努利朴素贝叶斯4. 多项式朴素贝叶斯4.1 Laplace平滑4.2 Lidstone平滑 三. 概率图模型1. 贝叶斯网络(Bayesian Network)1.1 全连接贝叶斯网络1.2 …

【Redis知识点总结】(二)——Redis高性能IO模型剖析

Redis知识点总结&#xff08;二&#xff09;——Redis高性能IO模型及其事件驱动框架剖析 IO多路复用传统的阻塞式IO同步非阻塞IOIO多路复用机制 Redis的IO模型Redis的事件驱动框架 IO多路复用 Redis的高性能的秘密&#xff0c;在于它底层使用了IO多路复用这种高性能的网络IO&a…

[java入门到精通] 18 字符流,编码表,对象流,其他流

今日目标 编码表 字符输出流 字符输入流 字符缓冲流 转换流 对象操作流 装饰模式 commons-iojar包 1 编码表 1.1 思考&#xff1a; 既然字节流可以操作所有文件&#xff0c;那么为什么还要学习字符流 &#xff1f; 如果使用字节流 , 把文本文件中的内容读取到内存时…

ODP(Open Data Plane)

1. 摘要 本文档旨在指导新的ODP应用程序开发人员。 有关ODP的更多详细信息&#xff0c;请参见 ODP 主页。 Overview of a system running ODP applications ODP是一份API规范&#xff0c;为高性能网络应用程序的实现提供平台独立性、自动硬件加速和CPU扩展。 本文档介绍如何充…

DHCP中继实验(思科)

华为设备参考&#xff1a;DHCP中继实验&#xff08;华为&#xff09; 一&#xff0c;技术简介 DHCP中继&#xff0c;可以实现在不同子网和物理网段之间处理和转发DHCP信息的功能。如果DHCP客户机与DHCP服务器在同一个物理网段&#xff0c;则客户机可以正确地获得动态分配的IP…

OS-Copilot:实现具有自我完善能力的通用计算机智能体

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ AI 缩小了人类间的知识和技术差距 论文标题&#xff1a;OS-Copilot: Towards Generalist Computer Agents with Self-Improvement 论文链接&#xff1a;https://arxiv.org/abs/2402.07456 项目主页&a…

Hadoop生态选择(一)

一、项目框架 1.1技术选型 技术选型主要考虑因素:维护成本、总成本预算、数据量大小、业务需求、行业内经验、技术成熟度。 数据采集传输:Flume&#xff0c;Kafka&#xff0c;DataX&#xff0c;Maxwell&#xff0c;Sqoop&#xff0c;Logstash数据存储:MySQL&#xff0c;HDFS…

全网最最最详细的centos7如何设置静态ip

以下步骤假设你已经有了管理员权限&#xff08;或者可以使用sudo&#xff09;以及你的网络接口名称&#xff08;例如ens33&#xff09;。 步骤 1: 查找网络接口名称 打开终端。运行命令nmcli d来查看所有网络设备及其状态。找到你想配置的设备名称&#xff0c;比如ens33。 步…

结构指针的使用

结构指针的使用 指针类型变量&#xff1a; 指针类型&#xff0c;是变量类型的一种&#xff0c;它是专门用来存储变量的地址的。 例如 int *p; 表示p是一个指针变量&#xff0c;它用来存储某个整型变量的地址。 int a5; int *p&a; 这样&#xff0c;就将整型变量a的地…

Python语言元素之变量

程序是指令的集合&#xff0c;写程序就是用指令控制计算机做我们想让它做的事情。那么&#xff0c;为什么要用Python语言来写程序呢&#xff1f;因为Python语言简单优雅&#xff0c;相比C、C、Java这样的编程语言&#xff0c;Python对初学者更加友好。 一、一些计算机常识 在…

YOLOv9最新的改进项目来了!!

专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;主力高效涨点&#xff01;&#xff01;&#xff01; YOLOv9作为最新的YOLO系列模型&#xff0c;对于做目标检测的同学是必不可少的。本专栏将针对2024年最新推出的YOLOv9检测模型&#xff0c;使用当前流…

RabbitMQ - 06 - Topic交换机

目录 控制台创建队列与交换机 编写消费者方法 编写生产者测试方法 结果 Topic交换机与Direct交换机基本一致 可参考 这篇帖子 http://t.csdnimg.cn/AuvoK topic交换机与Direct交换机的区别是 Topic交换机接收的消息RoutingKey必须是多个单词&#xff0c;以 . 分割 Topic交…

虚拟化

什么是虚拟化 虚拟化&#xff08;Virtualization&#xff09;是一种资源分配和管理技术&#xff0c;是将计算机的各种实体资源,比如CPU、内存、磁盘空间、网络适配器等&#xff0c;进行抽象转换后虚拟的设备,可以实现灵活地分割、组合为一个或多个计算机配置环境&#xff0c;并…

初探深度学习-手写字体识别

前言 手写数字的神经网络识别通常指的是通过训练有素的神经网络模型来识别和分类手写数字图像的任务。这种类型的任务是机器学习和计算机视觉领域的一个经典问题&#xff0c;经常作为入门级的图像识别问题来展示和测试各种机器学习算法的能力。在实际应用中&#xff0c;手写数…

寒假作业Day 09

寒假作业Day 09 一、选择题 因为一开始的for循环&#xff0c;k<2NN&#xff0c;所以复杂度为2N方&#xff0c;而后面的M10的while循环&#xff0c;则是10&#xff0c;复杂度为常数级&#xff0c;所以2N方10&#xff0c;近似于N方&#xff0c;即O(N^2) 这是一个计算阶乘的递…

excel批量数据导入时用poi将数据转化成指定实体工具类

1.实现目标 excel进行批量数据导入时&#xff0c;将批量数据转化成指定的实体集合用于数据操作&#xff0c;实现思路&#xff1a;使用注解将属性与表格中的标题进行同名绑定来赋值。 2.代码实现 2.1 目录截图如下 2.2 代码实现 package poi.constants;/*** description: 用…

一键部署Tesseract-OCR环境C++版本(Windows)

环境&#xff1a;Windows 10 工具&#xff1a;git vcpkg vscode cmake 库&#xff1a;Tesseract 一键部署Tesseract-OCR环境C版本&#xff08;Windows&#xff09; 分享这篇文章的原因很简单&#xff0c;就是为了让后续的朋友少走弯路。自己在搜索相关C版本的tesseract部署时…

【python量化】基于okex API开发的海龟策略

介绍 基于okex api开发的海龟策略&#xff0c;okex海龟策略python实现方式。该程序目前只支持单品种&#xff0c;比如设置ETH后&#xff0c;只对ETH进行做多做空。该程序运行需要两样东西&#xff1a;apikey 和 标的 运行该程序之前&#xff0c;用户需要到okex网站去申请apiK…
最新文章