【Linux】线程互斥

news2024/11/27 15:45:51

目录

  • 🌈前言
  • 🌸1、Linux线程互斥
    • 🍧1.1、线程间互斥相关背景概念
    • 🍨1.2、互斥量(锁)相关背景
    • 🍯1.3、互斥量(锁)相关API
      • 🍯1.3.1、初始化和销毁互斥锁
      • 🍰1.3.2、互斥量加锁和解锁
      • 🍲1.3.3、互斥锁的实现原理

🌈前言

这篇文章给大家带来线程同步与互斥的学习!!!


🌸1、Linux线程互斥

🍧1.1、线程间互斥相关背景概念

这些名词,我们在共享内存中已经了解过⭐⭐

概念
  • 临界资源:多线程执行流共享(都能看到,并且能访问)的资源就叫做临界资源
  • 临界区:每个线程执行流内部,访问临界资源的代码,就叫做临界区
  • 互斥任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源(全局、静态变量、共享内存等),通常对临界资源起保护作用
  • 原子性:不会被任何调度机制打断的操作,该操作只有两种状态,要么完成,要么未完成,没有中间状态

🍨1.2、互斥量(锁)相关背景

互斥量mutex
  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互
  • 多个线程并发的操作共享变量,会带来一些问题(原子性问题)

验证:设置一个多线程来进行抢票,票数为共享资源 – 售票系统代码

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
using namespace std;

int ticket = 10000;

void *GrabTickets(void *args)
{
    // 多线程一直抢票,直到票数<=0为止
    const char *name = static_cast<const char *>(args);
    while (true)
    {
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s: 抢到了票, 票的编号为: %d\n", name, ticket);
            ticket--;
        }
        else
        {
            printf("%s: 已经放弃抢票了,因为没有了...\n", name);
            break;
        }
    }
    return nullptr;
}

int main()
{
    // 定义线程id
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_t tid4;

    // 创建线程
    if (pthread_create(&tid1, nullptr, GrabTickets, (void *)"Thread1") != 0)
    {
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tid2, nullptr, GrabTickets, (void *)"Thread2") != 0)
    {
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tid3, nullptr, GrabTickets, (void *)"Thread3") != 0)
    {
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tid4, nullptr, GrabTickets, (void *)"Thread4") != 0)
    {
        exit(EXIT_FAILURE);
    }

    // 线程等待 -- 不获取线程退出码
    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);
    return 0;
}

一次运行结果:出现溢出抢票的情况!!!

Thread3: 已经放弃抢票了,因为没有了...
Thread2: 抢到了票, 票的编号为: 3
Thread2: 已经放弃抢票了,因为没有了...
Thread4: 抢到了票, 票的编号为: -1
Thread4: 已经放弃抢票了,因为没有了...
Thread1: 抢到了票, 票的编号为: -2
Thread1: 已经放弃抢票了,因为没有了...
为什么可能无法获得正确的结果呢?
  • if 语句判断条件为真以后,代码可以并发的切换到其他线程
  • usleep函数用于模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段(代码区)
  • ticke自减操作本身就不是一个原子操作有中间动作,线程切换时会被挂起
  • CPU内的寄存器是被所有执行流(线程)共享的,但是寄存器里面的数据是属于当前执行流的上下文数据
  • 线程被切换时,需要保存上下文数据。线程被换回时,要恢复上下文数据
  // 取出ticket--部分的汇编代码
  // 指令:objdump -d a.o > test.objdump
  //-------------------------------------------------------------------------------------
  44:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 4a <_Z11GrabTicketsPv+0x4a>
  4a:   83 e8 01                sub    $0x1,%eax
  4d:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # 53 <_Z11GrabTicketsPv+0x53>
ticket自减操作对应三条汇编指令
  • load :将共享变量ticket从内存加载到寄存器中
  • update : 更新寄存器里面的值,执行-1操作
  • store :将新值,从寄存器写回共享变量ticket的内存地址

在这里插入图片描述

为什么说ticket不是原子操作呢?

多线程访问共享资源问题
  • 因为CPU在运算ticket自减操作时(比如计算完后),线程的时间片到了,需要进行线程切换,但是ticket计算完后的数据没有拷贝回内存,就被切换了
  • 线程切换时,将保存线程的上下文,下一个线程运算完后,ticket的值变成9999
  • 随后切换回原来的线程,恢复线程的上下文,将运算好的9999拷回内存的ticket中,导致数值不一样问题(应该变成9998)!

在这里插入图片描述

要解决以上问题,需要做到三点
  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
  • 要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量
    在这里插入图片描述

🍯1.3、互斥量(锁)相关API

🍯1.3.1、初始化和销毁互斥锁

互斥锁概念
  • 互斥锁只能对临界区进行加锁,加锁的本质是让线程执行临界区代码串行化
  • 加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加。如果一部分代码加,一部分不加,会出现bug
  • 临界区加锁时,加锁的粒度约细越好,否则可能出现死锁的情况(没有解锁)
  • 加锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁
  • 多线程竞争和申请锁的过程,就是原子的

初始化互斥量有二种方法

第一种方法:静态分配

#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
代码解析
  • pthread_mutex_t是互斥锁,它是一个联合体,里面有一个结构体描述锁的属性
  • PTHREAD_MUTEX_INITIALIZER:它是一个,用于初始化互斥锁

第二个方法:动态分配

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
                       const pthread_mutexattr_t *restrictattr);
函数解析
  • mutex:要初始化的互斥锁(pthread_mutex_t变量的地址
  • restrictattr:设置互斥锁的属性,一般为NULL/nullptr
  • 返回值:初始化成功返回0,失败返回一个错误码errno

销毁互斥锁:

#include <pthread.h》
int pthread_mutex_destroy(pthread_mutex_t *mutex)
函数解析
  • mutex:要销毁的互斥锁(pthread_mutex_t变量的地址
  • 返回值:初始化成功返回0,失败返回一个错误码errno
销毁互斥锁需要注意
  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥锁不需要销毁
  • 不要销毁一个已经加锁的互斥锁
  • 已经销毁的互斥锁 ,要确保后面不会有线程再尝试加锁

🍰1.3.2、互斥量加锁和解锁

加锁和解锁:

#include <pthread.h》
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
函数解析
  • mutex:要加锁或解锁的互斥量(pthread_mutex_t变量的地址
  • 返回值:初始化成功返回0,失败返回一个错误码errno

调用 pthread_ lock 时,可能会遇到以下情况:⭐⭐

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功

  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

修改前面的售票系统代码:使用动态分配互斥锁,需要释放互斥锁(pthread_mutex_destroy)

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
using namespace std;

// 1、定义互斥锁,主线程初始化
pthread_mutex_t Mutex;

int ticket = 10000;

void *GrabTickets(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (true)
    {
        // 3、加锁
        pthread_mutex_lock(&Mutex);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s: 抢到了票, 票的编号为: %d\n", name, ticket);
            ticket--;
            // 解锁 -- 互斥量的粒度越细越好
            pthread_mutex_unlock(&Mutex);
        }
        else
        {
            // 解锁 -- 如果没有解锁,线程再次加锁时,会一直阻塞
            pthread_mutex_unlock(&Mutex);
            printf("%s: 已经放弃抢票了,因为没有了...\n", name);
            break;
        }
    }
    return nullptr;
}

int main()
{
    // 2、初始化互斥锁 -- 动态分配互斥锁
    pthread_mutex_init(&Mutex, nullptr);

    // 定义线程id
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_t tid4;

    // 创建线程
    if (pthread_create(&tid1, nullptr, GrabTickets, (void *)"Thread1") != 0)
    {
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tid2, nullptr, GrabTickets, (void *)"Thread2") != 0)
    {
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tid3, nullptr, GrabTickets, (void *)"Thread3") != 0)
    {
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tid4, nullptr, GrabTickets, (void *)"Thread4") != 0)
    {
        exit(EXIT_FAILURE);
    }

    // 线程等待 -- 不获取线程退出码
    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);

    // 释放互斥锁 -- 动态申请的互斥锁
    pthread_mutex_destroy(&Mutex);
    return 0;
}

修改前面的售票系统代码:使用静态分配互斥锁,不需要释放互斥锁

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
using namespace std;

// 1、定义互斥锁,主线程初始化 -- 静态分配互斥锁
pthread_mutex_t Mutex = PTHREAD_MUTEX_INITIALIZER;

int ticket = 10000;

void *GrabTickets(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (true)
    {
        // 3、加锁
        pthread_mutex_lock(&Mutex);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s: 抢到了票, 票的编号为: %d\n", name, ticket);
            ticket--;
            // 解锁 -- 互斥量的粒度越细越好
            pthread_mutex_unlock(&Mutex);
        }
        else
        {
            // 解锁 -- 如果没有解锁,线程再次加锁时,会一直阻塞
            pthread_mutex_unlock(&Mutex);
            printf("%s: 已经放弃抢票了,因为没有了...\n", name);
            break;
        }
    }
    return nullptr;
}

int main()
{
    // 2、初始化互斥锁
    pthread_mutex_init(&Mutex, nullptr);

    // 定义线程id
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_t tid4;

    // 创建线程
    if (pthread_create(&tid1, nullptr, GrabTickets, (void *)"Thread1") != 0)
    {
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tid2, nullptr, GrabTickets, (void *)"Thread2") != 0)
    {
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tid3, nullptr, GrabTickets, (void *)"Thread3") != 0)
    {
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tid4, nullptr, GrabTickets, (void *)"Thread4") != 0)
    {
        exit(EXIT_FAILURE);
    }

    // 线程等待 -- 不获取线程退出码
    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);
    return 0;
}

临界区的临界资源被锁后,当前线程时间片到了,还能进行线程间切换吗?加锁 == 不会切换?⭐⭐⭐

结论
  • 完全可以切换,因为线程执行加锁解锁对应的也是代码
  • 但是线程加锁是原子的,要么拿到锁,要么没拿到(多线程竞争申请互斥锁资源)

比如:我们有线程A和其他线程

  • 线程A申请到了锁,执行临界区代码中途被切走了,切走时也是把锁抱走的
  • 在线程A被切走的时候,绝对不会有线程进入临界区
  • 因为进入临界区要申请互斥锁的资源,但是线程A已经申请了,其他线程只能一直阻塞等待资源就绪,然后竞争资源
  • 线程A访问临界区只有进入和使用完毕二种状态(原子性),这样才对其他线程有意义
总结
  • 不要再临界区做过多的事情,临界区代码最好越短越好
  • 因为可能执行到一部分时,时间片就到了,然后其他线程一直阻塞等待,耗时长

在这里插入图片描述


🍲1.3.3、互斥锁的实现原理

前言
  • 经过上面的例子,我们都已经意识到单纯的 ticket++ 或者 ++ticket 都不是原子的,有可能会导致数据一致性问题
  • 为了实现互斥锁操作,大多数计算机体系结构都提供了swap或exchange汇编指令
  • 该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性
  • 即使是多处理器平台(多核CPU),访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期
  • swap或exchange交换指令只有一句,意味着只有没做和做完二种状态,它是原子操作

在这里插入图片描述

当执行完二条汇编语句时,时间片到了,线程切换后,会出问题吗?

没有问题

当一个线程在执行时,CPU中一组寄存器中保存的值被称为该线程的上下文

  • 因为线程切换时,会将寄存器中的数据全部带走!!!
  • 凡是在寄存器中的数据,全部都是线程内部的上下文!!!
  • 寄存器是在多线程看来,是共享的资源(CPU只有一套寄存器),但是在线程看来是自己的私有资源(因为线程会拿着寄存器的数据切换走)
  • 多线程看起来同时在访问寄存器,但是它们互不影响

如果多线程同时竞争锁时,同时将0数据传输到al寄存器中,会出现问题吗?

比如:mutex = 1
  • 不会出现问题,因为多线程中竞争资源时,至少有一个线程执行第二条交换指令
  • 当第一个线程执行完这个指令后,寄存器的数据就会变成1,内存的数据变成0,而其他线程执行第二条指令,0跟0交换,没有发生变化
  • 第一次执行交换指令的线程,会进入if语句,并且返回0表示申请锁成功,而其他线程会一直挂起/阻塞等待

在这里插入图片描述

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

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

相关文章

Python爬虫403错误的解决方案

前言程序使用一段时间后会遇到HTTP Error 403: Forbidden错误。 因为在短时间内直接使用Get获取大量数据&#xff0c;会被服务器认为在对它进行攻击&#xff0c;所以拒绝我们的请求&#xff0c;自动把电脑IP封了。 解决这个问题有两种方法。一是将请求加以包装&#xff0c;变成…

1.浮动float

提示&#xff1a;如果多一个盒子&#xff08;都设置浮动&#xff0c;则它们会按照属性值一行内显示并且顶端对齐排列&#xff09; 注意&#xff1a; 浮动的元素是互相贴靠在一起的&#xff0c;&#xff08;没有缝隙&#xff09;&#xff0c;如果父级宽度装下这些浮动盒子&#…

MyBatis 详解 (2) -- 增删改操作

MyBatis 详解 2 -- 增删改操作前言一、准备工作1.1 创建数据库和表1.2 添加实体类1.3 添加 mapper 接口 (数据持久层)1.4 创建与接口对应的 xml 文件二、增加操作2.1 默认返回受影响的行数2.2 特殊的新增&#xff1a;返回自增 id三、删除操作四、修改操作五、实现完整交互5.1 添…

爆肝9万字,我已从小白晋升ARM嵌入式工程师!带你从零熟悉常用的M4嵌入式功能,建议收藏(含码源)

&#x1f4da; 前言 &#x1f4d1;博客主页&#xff1a;丘比特惩罚陆 &#x1f496;欢迎关注&#xff1a;点赞收藏⭐留言✒ &#x1f4ac;系列专栏&#xff1a;web前端、嵌入式、笔记专栏 &#x1f3ae; 加入社区&#xff1a; 丘比特惩罚陆 &#x1f947;人生格言&#xff1a;选…

【教学赛】金融数据分析赛题1:银行客户认购产品预测(0.9676)

本文是对天池教学赛&#xff0c;银行客户认购产品预测的记录&#xff0c;教学赛网址如下&#xff1a; 【教学赛】金融数据分析赛题1&#xff1a;银行客户认购产品预测_学习赛_天池大赛-阿里云天池 1. 读取数据 import pandas as pd# 加载数据 train pd.read_csv(train.csv) …

P5587 打字练习————C++

题目 打字练习 题目描述 R 君在练习打字。 有这样一个打字练习网站&#xff0c;给定一个范文和输入框&#xff0c;会根据你的输入计算准确率和打字速度。可以输入的字符有小写字母、空格和 .&#xff08;英文句号&#xff09;&#xff0c;输入字符后&#xff0c;光标也会跟…

c语言小练pintia11-20

11.计算平均分已知某位学生的数学、英语和计算机课程的成绩分别是87分、72分和93分&#xff0c;求该生3门课程的平均成绩&#xff08;结果按整型输出&#xff09;。输入格式&#xff1a;本题无输入输出格式&#xff1a;按照下列格式输出结果&#xff1a;math 87, eng 72, com…

深耕地市区县市场,新华三智行中国走新路

2022年就这样结束了&#xff0c;但是企业数字化的进程从未结束。回顾这一年&#xff0c;对于任何企业而言&#xff0c;数字化优先的战略仍然在继续。不仅如此&#xff0c;数字化走向地市区县市场&#xff0c;带来了更多的机遇和发展&#xff0c;让我们看到了中国的数字经济还有…

Flurry iOS端调研和使用

Flurry iOS端调研使用 flurry官方资料较少&#xff0c;且只有英文文档没有代码demo。公司项目确定要用Flurry&#xff0c;所以深入调研&#xff0c;顺便记录过程。有需要的小伙伴也可以顺便参考 一.创建应用拿api_key 官网&#xff1a;https://www.flurry.com/ 用邮箱去官网…

【目标检测】55、YOLOv8 | YOLOv5 团队 Ultralytics 再次出手,又实现了 SOTA

文章目录一、YOLO 系列算法的简单回顾二、YOLOv8 简介2.1 安装和简单使用2.2 Ultralytics HUB2.2.1 Upload Dataset2.3 YOLOv8 主要改动三、YOLOv8 细节详述论文&#xff1a;暂无 官方文档&#xff1a;https://docs.ultralytics.com/ 代码&#xff1a;https://github.com/ult…

【算法数据结构初阶篇】:链表问题

一、反转单双链表 一、数据结构图 二、代码演示 public class Code01_ReverseList {public static class Node {public int value;public Node next;public Node(int data) {value data;}}public static class DoubleNode {public int value;public DoubleNode last;public …

Spring Cloud Gateway从注册中心自动注册配置路由信息

环境信息Spring Boot&#xff1a;2.0.8.RELEASESpring Boot内置的tomcat&#xff1a;tomcat-embed-core 8.5.37Spring Cloud Gateway&#xff1a;2.0.4.RELEASENacos&#xff1a;2.0.4.RELEASE需求Spring Cloud Gateway注册到注册中心&#xff08;这里是Nacos&#xff0c;其它注…

Spring学习系列(二)

Spring_特殊值的注入问题和各种类型的自动装配1.set两种方式的区别第4中赋值方式&#xff1a;自动装配&#xff08;只适用于ref类型&#xff09;使用注解定义bean1.set两种方式的区别 &#xff08;1&#xff09;把值写到value属性里面&#xff0c;必须加引号&#xff0c;写到子…

【学习】计算机系统硬件和数据结构

学习内容描述&#xff1a; 1、CPU包含哪些部分&#xff1f; 2、数据结构基础知识。 重点知识&#xff1a; 1、CPU(中央处理器&#xff0c;Central Processing Unit) 主要包括运算器、控制器两大部件&#xff1b;内部结构包含控制单元、运算单元、存储单元和时钟等几个主要部…

虚拟直播(虚拟场景直播)要怎么做?

阿酷TONY / 2022-11-21 / 长沙 绿幕抠像 虚拟场景&#xff08;三维场景&#xff09;实时渲染&#xff0c;来一场虚拟直播。 附案例效果&#xff1a;PC端案例、移动端案例效果。 目录 1. 绿幕虚拟直播间 2. 虚拟场景(炫酷舞台) 3. 案例&#xff1a;PC端 4. 案例&#xff1a…

光纤内窥镜物镜光学设计

光纤内窥镜物镜光学设计 工作原理 典型的光纤传像束内窥镜主要由前置物镜、光纤传像束、目镜/耦接镜、 探测器等组成&#xff0c;如图1所示。通过物镜把目标成像于光纤传像束的前端面上&#xff0c;该端面上的图像被离散分布的大量光纤采样&#xff0c;每根光纤都有良好的光学…

[极客大挑战 2019]Upload

目录 解题步骤 常见的绕过思路 解题步骤 直接上传shell 回显&#xff1a;Not image! bp抓包 修改类型 Content-Type: application/octet-stream改为Content-Type: image/jpg 回显&#xff1a;NOT&#xff01;php! 修改后缀为phtml 回显&#xff1a;NO! HACKER! your file inc…

SAP MM 物料分类账的启用配置

一、前言 物料账就是一本账&#xff0c;管理物料的数量和价值。 通常物料有两种计价方法&#xff1a; 移动平均价V&#xff08;移动加权平均&#xff09; 标准价S 移动平均价的优点&#xff1a;能够及时响应市场原材料价格的波动。 价差科目&#xff08;总账科目&#xff09;…

MyBatis-Plus基本操作

依赖 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version> </dependency>基础操作 DAO层 public interface UserMapper extends BaseMapper<User…

nohup + 命令实现后台不挂断地运行程序

nohup&#xff1a; nohup&#xff1a;不挂断地运行程序&#xff0c;就是即便关闭终端的窗口&#xff0c;程序还会继续执行。 $ nohup python merge_reasons.py可以看到终端仍在被占用&#xff0c;同级目录下生成了一个nohup.out文件&#xff0c;本来输出到终端的信息&#xf…