【Linux从入门到精通】多线程 | 线程互斥(互斥锁)

news2024/11/26 12:22:40

  上篇文章我们对线程 | 线程介绍&线程控制介绍后,本篇文章将会对多线程中的线程互斥与互斥锁的概念进行详解。同时结合实际例子解释了可重入与不被重入函数、临界资源与临界区和原子性的概念。希望本篇文章会对你有所帮助。

文章目录

引入

一、重入与临界

1、1 可重入与不被重入函数

1、1、1 不可重入函数

1、1、2 可重入函数

1、2 临界资源与临界区

1、2、1 临界资源

1、2、2 临界区

1、3 原子性

二、 线程互斥

2、1 分析原因与再次理解概念

2、2 互斥锁

2、2、1 什么是互斥锁

2、2、2 pthread_mutex_t 

2、2、3 pthread_mutex_init 初始化

2、3 抢票加互斥锁

2、4 互斥锁总结

三、互斥锁实现原理


🙋‍♂️ 作者:@Ggggggtm 🙋‍♂️

👀 专栏:Linux从入门到精通  👀

💥 标题:线程互斥💥

 ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️

引入

  我们先看一段多线程抢票的代码:

int tickets=10000;

void* getTickets(void* args)
{
    while(true)
    {
        if(tickets>0)
        {
            usleep(1000);
            printf("%s: %d\n",(char*)args,tickets);
            tickets--;
        }
        else
            break;
    }

    return nullptr;
}
int main( void )
{
    srand(time(nullptr));
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, getTickets, (void*)"thread 1");
    pthread_create(&t2, NULL, getTickets, (void*)"thread 2");
    pthread_create(&t3, NULL, getTickets, (void*)"thread 3");
    pthread_create(&t4, NULL, getTickets, (void*)"thread 4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    return 0;
}

  上述代码就是创建了四个新线程同时去抢票,当票数为0时,就停止抢票。那我们看一下运行结果:

  怎么票数会被抢到负数呢?上述代码明明就是在票数为0的时候就终止了。这是为什么呢?我们接着往下看。

一、重入与临界

1、1 可重入与不被重入函数

  我们先看下图:

  上图就是一个链表的节点插入。我们之前学的是单进程进行链表的插入,也就是但是执行流进行的插入。当然,但执行流是没有任何问题。如果现在有多个执行流在执行链表的插入呢?会不会出现意想不到的问题呢?我们看如下情况。

  main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。我们发现就出现了一个节点的丢失。

1、1、1 不可重入函数

  不可重入函数(Non-reentrant Function),它在被多个任务并发调用时可能会出现竞争条件(Race Condition)或数据不一致的问题。 

  在上述情况中,我们发现插入节点的函数被多个执行流执行时,发生了意想不到的错误,出现了数据丢失的问题。我们称之为该函数不可被重入。

1、1、2 可重入函数

  可重入函数(Reentrant Function)是指可以被同时多个任务调用而不会发生错误或产生意外结果的函数。换句话说,可重入函数在被多个任务并发调用时,能够保持其内部状态的一致性。我们所学的大部分函数都是不可被重入的。

1、2 临界资源与临界区

1、2、1 临界资源

  临界资源(Critical Resource)是指在并发编程中,多个任务或线程之间共享的某一资源,但同时只能被一个任务或线程访问和操作的资源。

1、2、2 临界区

  临界区(Critical Section)是指代码中访问临界资源的那一部分代码段或区域。在临界区内部,任务或线程可以对临界资源进行读取、写入或其他操作。因为临界资源只能被一个任务或线程访问,所以必须确保在任何时刻只有一个任务或线程能够进入临界区,以避免对临界资源造成竞争条件和不确定的结果。 

  这里先给大家把概念引出,后面会再次结合例子来解释临界资源与临界区。 

1、3 原子性

  其实我们也不难发现上述情况问题的所在。一是有多个执行流在执行同一个操作。因为当一个进程在运行时,任何时刻都有可能被切换下去,去执行另一个进程。二是插入操作分为多个步骤完成。两点一结合,就是在插入没有完成的时候该执行流就被切换走了,进而导致出现问题。原子性是一个操作没有中间的状态。就是一个操作要执行就执行结束,要不就不执行,没有其他的状态。加入插入操作是原子性的,也就不会发生数据错误。

二、 线程互斥

2、1 分析原因与再次理解概念

  上面我们了解了重入与临界的概念后,我们再看引入中抢票的例子。抢票例子是多线程进行抢票。也就是有多个执行流在进行抢票。我们以为tickets--在CPU內部直接减1就完了吗?实际上并不是。具体如下图:

  假设线程1抢票时,此时票好有10000张。

  于是就进行判断,打印。刚打印完,把tickets=10000读到寄存器中,还没有抢票(tickets--),该线程的时间片到了被切换下去。此时线程1的上下文会被保存起来。然后线程2被调度。

  由于各种原因,线程2的优先级较高。线程二就行行疯狂抢票。一下子抢了1000多张票。然后线程2被切换下去,线程1重新被调度。操作系统会先回复线程1的上下文数据。此时CPU拿到的tickets又变成了10000,线程1再进行抢票。此时就出现了CPU与内存中的数据错乱的问题。

   我们再看tickets--。这也正是tickets能够变成负数的原因。当一个tickets==1时,发现能够抢票。当时刚执行完打印,也就是并没有抢票就被切换下去了,此时该线程也并没有保存tickets数据。同时其它线程被切换上来进行抢票后,票数变为0。再次恢复之前被切换下去的下线程,此时已经没票了,但是该线程以为还有票,于是当把tickets数据读到寄存器中后(tickets为0),进行计算--操作,再次写入内存票数就变成了负数。

  上述讲解了两种出现错误的情况。针对上述例子,我们再来看一下临界资源和临界区:

  tickets-- 操作并不是原子操作,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中
  • update : 更新寄存器里面的值,执行-1操作
  • store :将新值,从寄存器写回共享变量ticket的内存地址

要解决以上问题,需要做到三点:

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

  要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量

2、2 互斥锁

  什么是锁?在Linux操作系统中,锁(Lock)是一种同步机制,用于保护共享资源免受多个并发线程访问和修改的干扰。锁可以防止多个线程同时访问或修改共享资源,从而确保对共享资源的安全访问。

2、2、1 什么是互斥锁

   当有一个线程申请互斥锁资源,也就是加锁成功后,其他线程就不会申请所资源成功。其他线程也就会进入阻塞等待状态。可以理解为把其他线程阻塞在外面了,不能有多个线程同时执行去申请互斥锁。这就是线程互斥。同一个线程不能执行互斥锁内的资源,同时只有一个线程能够执行。当进行对所资源释放,也就是解锁后,其他线程才可申请所资源。

2、2、2 pthread_mutex_t 

  pthread_mutex_t是一个用于实现线程同步的互斥量类型,也就是我们所说的互斥锁的类型。pthread_mutex_t可以用来保护共享资源,防止多个线程同时访问和修改数据而导致的竞争条件。使用pthread_mutex_t时,通常需要进行以下步骤:初始化、加锁、解锁、销毁。

下面对每个步骤进行详细解释,并提供一个示例代码:

  1. 初始化互斥锁: 可以使用pthread_mutex_init函数来初始化互斥锁。初始化后的互斥锁状态为未锁定状态。

    示例代码:

    pthread_mutex_t mutex;  // 定义互斥锁变量
    
    // 初始化互斥锁
    int result = pthread_mutex_init(&mutex, NULL);
    if (result != 0) {
        // 初始化失败的处理逻辑
    }
  2. 加锁: 使用pthread_mutex_lock函数可以将互斥锁设置为锁定状态,如果互斥锁已被其他线程锁定,当前线程会进入阻塞状态,直到获取到锁。

    示例代码:

    // 加锁
    int result = pthread_mutex_lock(&mutex);
    if (result != 0) {
        // 加锁失败的处理逻辑
    }
  3. 解锁: 使用pthread_mutex_unlock函数可以将互斥锁设置为未锁定状态,释放锁供其他线程使用。

    示例代码:

    // 解锁
    int result = pthread_mutex_unlock(&mutex);
    if (result != 0) {
        // 解锁失败的处理逻辑
    }
  4. 销毁互斥锁: 在不再需要使用互斥锁时,可以使用pthread_mutex_destroy函数来销毁已初始化的互斥锁。

    示例代码:

    // 销毁互斥锁
    int result = pthread_mutex_destroy(&mutex);
    if (result != 0) {
        // 销毁失败的处理逻辑
    }

  注意事项:

  • pthread_mutex_t的初始化应该在使用前完成,并且应该确保互斥锁变量的作用域覆盖了所有对该锁的访问。
  • 对于每个加锁操作,都应该有对应的解锁操作,以避免资源泄漏和死锁的情况发生。
  • 建议在使用互斥锁时考虑锁的粒度,尽量保持锁的范围小并且锁的持有时间短,以提高并发性能。

  后面也会对注意事项接进行解释。线面我们先来看一下对pthread_mutex_t 进行初始化处理操作的详解。其他操作简单,就不再解释。

2、2、3 pthread_mutex_init 初始化

  pthread_mutex_init函数是一个用于初始化互斥锁的函数,它的原型如下:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

  该函数主要用于将传入的mutex指向的互斥锁对象进行初始化,使其可以正确地使用。

  参数说明:

  • mutex:指向要初始化的互斥锁对象的指针。
  • attr:指向包含互斥锁属性的对象的指针。可以通过该参数来设置互斥锁的属性,如果为NULL,则使用默认属性。

  返回值:

  • 若成功,返回0;
  • 若失败,返回错误代码。

  调用pthread_mutex_init函数会对互斥锁进行初始化,包括分配内存和设定默认属性。属性我们一般选择传入nullptr,就是默认属性。

  当pthread_mutex_t 对象为全局时,我们也可选择不用 pthread_mutex_init 进行初始化。可直接使用宏:PTHREAD_MUTEX_INITIALIZER来初始化。代码如下:

pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;

2、3 抢票加互斥锁

  我们对抢票的临界区加锁,这样就不会出现多个执行流去访问临界资源的情况了。

  代码如下:

int tickets=10000;
pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;

void* getTickets(void* args)
{
    while(true)
    {
        pthread_mutex_lock(&mtx);
        if(tickets>0)
        {
            usleep(1000);
            printf("%s: %d\n",(char*)args,tickets);
            tickets--;
            pthread_mutex_unlock(&mtx);
        }
        else
        {
            pthread_mutex_unlock(&mtx);
            break;
        }
        usleep(rand()%2000);
    }

    return nullptr;
}
int main( void )
{
    srand(time(nullptr));
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, getTickets, (void*)"thread 1");
    pthread_create(&t2, NULL, getTickets, (void*)"thread 2");
    pthread_create(&t3, NULL, getTickets, (void*)"thread 3");
    pthread_create(&t4, NULL, getTickets, (void*)"thread 4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    return 0;
}

  我们再看运行结果:

  上述代码是定义全局变量的互斥锁。我们再来看一下局部变量的互斥锁是怎么使用的。代码如下:

#define THREAD_NUM 5

int tickets=10000;

class ThreadData
{
public:
    ThreadData(const string& n,pthread_mutex_t* pm)
        :tname(n)
        ,pmtx(pm)
    {}

public:
    string tname;
    pthread_mutex_t* pmtx;
};

void* getTickets(void* args)
{
    ThreadData* data=(ThreadData*) args;
    while(true)
    {
        pthread_mutex_lock(data->pmtx);
        if(tickets>0)
        {
            usleep(1000);
            printf("%s: %d\n",data->tname.c_str(),tickets);
            tickets--;
            pthread_mutex_unlock(data->pmtx);
        }
        else
        {
            pthread_mutex_unlock(data->pmtx);
            break;
        }
        usleep(rand()%2000);
    }

    return nullptr;
}
int main( void )
{
    srand(time(nullptr));
    pthread_t t1, t2, t3, t4;

    pthread_mutex_t mtx;

    pthread_mutex_init(&mtx,nullptr);
    pthread_t t[THREAD_NUM];
    // 多线程抢票的逻辑
    for(int i = 0; i < THREAD_NUM; i++)
    {
        std::string name = "thread ";
        name += std::to_string(i+1);
        ThreadData *td = new ThreadData(name, &mtx);
        pthread_create(t + i, nullptr, getTickets, (void*)td);
    }

    for(int i = 0; i < THREAD_NUM; i++)
    {
        pthread_join(t[i], nullptr);
    }

    pthread_mutex_destroy(&mtx);
    return 0;
}

2、4 互斥锁总结

  加锁就是串行执行了吗?答案是是的。任何时候只允许申请锁成功的线程执行临界区代码。因为变为串行执行,所以我们要在加锁的时候,一定要保证加锁的粒度,越小越好!!!不必要的代码可放在锁外。

  加锁了之后,线程在临界区中,是否会切换,会有问题吗?答案是会被切换的。但是并不会出现问题。虽然被切换了,但是你是持有锁被切换的,所以其他抢票线程要执行临界区代码,也必须先申请锁,锁它是无法申请成功的,所以,也不会让其他线程进入临界区,就保证了临界区中数据一致性!!!只要一个线程,且需要访问临界资源,就必须申请锁。

  在没有持有锁的线程看来,对我最有意义的情况只有两种:1.线程1没有持有锁(什么都没做))2.线程1释放锁(做完),此时我可以申请锁!!!

  要访问临界资源,每一个线程都必须现申请锁,每一个线程都必须先看到同一把锁且访问它,锁本身不就是一种共享资源吗!!!那么谁来保证锁的安全呢?所以,为了保证锁的安全,申请和释放锁,必须是原子性的。所以是由它自己自己保证。那么是怎么保证的呢?

三、互斥锁实现原理

  经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题。

  为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 

  具体可结合下图理解:

  其实我们也不难发现,申请互斥锁资源的在底层本质就是一个交换指令。而交换的本质就是使其在内存中的共享资源变成了线程的私有资源!!!当交换后值为1时,就说明申请互斥锁资源成功。从头到尾,一直是在交换,一直是只有一个1。所以保证了只有一个线程能够申请互斥锁资源成功。

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

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

相关文章

网络安全深入学习第一课——热门框架漏洞(RCE-命令执行)

文章目录 一、RCE二、命令执行/注入-概述三、命令执行-常见函数四、PHP命令执行-常见函数1、exec&#xff1a;2、system3、passthru4、shell_exec5、反引号 backquote 五、PHP命令执行-常见函数总结六、命令执行漏洞成因七、命令执行漏洞利用条件八、命令执行漏洞分类1、代码层…

目标检测评估指标mAP:从Precision,Recall,到AP50-95【未完待续】

1. TP, FP, FN, TN True Positive 满足以下三个条件被看做是TP 1. 置信度大于阈值&#xff08;类别有阈值&#xff0c;IoU判断这个bouding box是否合适也有阈值&#xff09; 2. 预测类型与标签类型相匹配&#xff08;类别预测对了&#xff09; 3. 预测的Bouding Box和Ground …

C高级day5(Makefile)

一、Xmind整理&#xff1a; 二、上课笔记整理&#xff1a; 1.#----->把带参宏的参数替换成字符串 #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX(a,b) a>b?a:b #define STR(n) #n int main(int argc, const char *argv…

sheetjs實現頁面的數據導出execl

概述 需要給頁面的table做一個數據導出功能,發現一個好用sheetjs工具 只需要簡單的js語法如下,就可以將table的數據導出來 function load(){var date new Date();date.setTime(date.getTime() (8 * 60 * 60 * 1000));var table document.getElementById("tab");v…

神领物流 day02-网关与支付 Spring Cloud Alibaba 微服务

课程安排 单token存在的问题双token三验证用户端token校验与鉴权对接三方支付平台分布式锁 1、场景说明 新入职的你加入了开发一组&#xff0c;也接到了开发任务&#xff0c;并且你也顺利的修复了bug&#xff0c;完成了快递员、司机的鉴权&#xff0c;现在的你已经对项目的业…

C++qt day8

1.用代码实现简单的图形化界面&#xff08;并将工程文件注释&#xff09; 头文件 #ifndef MYWIDGET_H #define MYWIDGET_H //防止头文件冲突#include <QWidget> //父类的头文件class MyWidget : public QWidget //自定义自己的界面类&#xff0c;公共继承…

Linux vim的常见基本操作

目录 vim是一款多模式的编辑器 命令模式下&#xff1a; 用小写英文字母「h」、「j」、「k」、「l」&#xff0c;分别控制光标左、下、上、右移一格 gg&#xff1a;定位到代码第一行 nshiftg 定位到任意一行/最后一行 「 $ 」&#xff1a;移动到光标所在行的结尾 「 ^ 」&…

无涯教程-JavaScript - COLUMNS函数

描述 COLUMNS函数返回数组或引用中的列数。 语法 COLUMNS (array)争论 Argument描述Required/OptionalarrayAn array or array formula, or a reference to a range of cells for which you want the number of Columns.Required Notes COLUMNS(1:1)返回Excel中的列数,即…

字符串解码

题目链接 字符串解码 题目描述 注意点 s 由小写英文字母、数字和方括号 ‘[]’ 组成1 < s.length < 30s 保证是一个 有效 的输入s 中所有整数的取值范围为 [1, 300] 解答思路 利用栈先进后出的特点&#xff0c;将字符存储进栈中创建两个栈&#xff0c;一个数字栈&am…

照片批量处理 7000张

需求&#xff1a; 有6700照片导入系统&#xff1b; 系统只支持500张/每次&#xff1b; 6700 按机构分类复制提取出来&#xff1b; 分批次导入&#xff1b; 6700 分17份复制到对应文件夹中&#xff1b; 照片按照学号命名的&#xff1b; 20231715401.jpg 开始用bat脚本…

Matlab Simulink支持system generator插件

文章目录 前言一、System Generator 简介二、System Generator 特性三、System Generator 安装1、确定是否已安装 System Generator 工具2、vivado 卸载3、vivado 安装 四、解决版本不兼容问题五、使用 System Generator 前言 目前有在 Simulink 中开发完成后将其转换成 Veril…

七天学会C语言-第一天(C语言基本语句)

一、固定格式 这个是C程序的基本框架&#xff0c;需要记住&#xff01;&#xff01;&#xff01; #include<stdio.h>int main(){return 0; }二、printf 语句 简单输出一句C程序&#xff1a; #include<stdio.h> int main(){printf("大家好&#xff0c;&quo…

浅析Java责任链模式实现

一、概要 定义&#xff1a;责任链模式是一种行为设计模式&#xff0c; 允许你将请求沿着处理者链进行发送。收到请求后&#xff0c; 每个处理者均可对请求进行处理&#xff0c; 或将其传递给链上的下个处理者。 二、应用场景&#xff1a; 1.多条件流程判断&#xff1a;权限相关…

时序预测 | MATLAB实现BO-BiGRU贝叶斯优化双向门控循环单元时间序列预测

时序预测 | MATLAB实现BO-BiGRU贝叶斯优化双向门控循环单元时间序列预测 目录 时序预测 | MATLAB实现BO-BiGRU贝叶斯优化双向门控循环单元时间序列预测效果一览基本介绍模型搭建程序设计参考资料 效果一览 基本介绍 MATLAB实现BO-BiGRU贝叶斯优化双向门控循环单元时间序列预测。…

把握经济大势和个人财运密码必须读懂钱—现代金钱的魔力

原出处&#xff1a;天涯论坛之《把握经济大势和个人财运密码必须读懂钱—现代金钱的魔力》 原作者&#xff1a;真立派 原文完整版PDF&#xff08;可能很快就会被河蟹&#xff0c;建议先保存&#xff09;&#xff1a;https://pan.quark.cn/s/20c917683d8f 引子 过去一些年&…

数据结构入门 — 树的概念与结构

本文属于数据结构专栏文章&#xff0c;适合数据结构入门者学习&#xff0c;涵盖数据结构基础的知识和内容体系&#xff0c;文章在介绍数据结构时会配合上动图演示&#xff0c;方便初学者在学习数据结构时理解和学习&#xff0c;了解数据结构系列专栏点击下方链接。 博客主页&am…

【LeetCode-中等题】 151. 反转字符串中的单词

文章目录 题目方法一&#xff1a;双指针去除空格 题目 方法一&#xff1a;双指针去除空格 核心代码去除首尾以及中间多余空格(在原串上修改) //去除首尾以及中间多余空格(在原串上修改)public StringBuilder trimSpaces(String s) { int len s.length();StringBuilder str …

【结构型】代理模式(Proxy)

目录 代理模式(Proxy)适用场景代理模式实例代码&#xff08;Java&#xff09; 代理模式(Proxy) 为其他对象提供一种代理以控制对这个对象的访问。Proxy 模式适用于在需要比较通用和复杂的对象指针代替简单的指针的时候。 适用场景 远程代理 (Remote Proxy) 为一个对象在不同…

linux-线程条件变量(cond)

概述 与互斥锁不同&#xff0c;条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程&#xff0c;直到某特殊情况发生为止。通常条件变量和互斥锁同时使用 。 条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制&a…

Vue2的学习

computed计算属性 概念 基于现有数据&#xff0c;计算出来的新属性&#xff0c;依赖的数据变化&#xff0c;会自动重新计算 语法 声明在computed配置项中&#xff0c;一个计算属性对应一个函数这是一个属性{{计算属性名}}&#xff0c;不是方法注意不要忘记return <body…