【线程的同步与互斥】初识互斥量与锁

文章目录

线程互斥

一些背景知识

在讨论线程互斥的话题之前,我想先和大家说明一下下面的几个概念

  • 共享资源:在我们前面讨论线程的时候,我们知道线程的绝大多数资源都是共享的,我们把这些多执行流可以共享的资源叫作共享资源;共享资源不会导致数据不一致问题,只有访问了共享资源才有可能导致数据不一致问题
  • 临界资源:如果我们把多执行流的共享资源保护起来,那么被保护起来的共享资源就叫临界资源,如管道
  • 临界区:在每个线程内部,我们把访问临界资源的那一部分代码叫作临界区
  • 互斥:在任何时刻,只允许一个执行流进入临界区访问临界资源就叫做互斥
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

互斥量

共享资源的并发访问问题

在谈论互斥量之前,我想先带领大家思考一个问题,如果一个共享资源没有被保护,会存在什么问题?我们来看下面的一个代码:

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>

int count=100;

void*ThreadRoutine(void*arg)
{
    char*name=(char*)arg;
    while(true)
    {
        if(count>0){
            usleep(1000);
            printf("%s线程开始执行,count的值为:%d\n",name,count);
            count--;
        }
        else{
            break;
        }
    }
    return (void*)0;
}

int main()
{
    pthread_t t1,t2,t3,t4,t5;
    pthread_create(&t1,nullptr,ThreadRoutine,(void*)"thread-1");
    pthread_create(&t2,nullptr,ThreadRoutine,(void*)"thread-2");
    pthread_create(&t3,nullptr,ThreadRoutine,(void*)"thread-3");
    pthread_create(&t4,nullptr,ThreadRoutine,(void*)"thread-4");
    pthread_create(&t5,nullptr,ThreadRoutine,(void*)"thread-5");

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);
    pthread_join(t5,nullptr);

    return 0;
}

运行结果如下所示:

我们看到,此时竟然出现了负数,那么此时我们就有一个问题:为什么会出现减到负数的情况?

首先我们来看上面的代码,我们可以看到,无论是全局变量还是局部变量,最终都会在物理内存中存储,而我们对全局变量--和判断的操作最终都会在cpu上进行运算,也就是说,对于算数和逻辑问题,都会由CPU帮我们完成,而数据却在内存当中,所以注定了一次运算会有三个不同的阶段:

  • 取指令:将数据从内存当中加载到CPU内部
  • 分析指令:是逻辑运算还是算数运算
  • 执行指令:进行计算

也就是说,站在硬件的角度,CPU为了能够完成计算,需要把指令和数据拷贝到CPU内部的寄存器里面,然后在CPU内部进行算数运算和逻辑运算,而CPU为了完成运算的过程必定会以进程或线程为载体执行;站在操作系统的角度,把数据从内存搬迁到寄存器时,内存中的数据属于共享资源,寄存器属于硬件上下文,而线程要进行切换时需要保存自己的上下文数据,但是寄存器只有一份,但寄存器里面的数据每个线程都有一份;

换句话说,当我们把数据从内存拷贝到寄存器当中,CPU必定在执行某一个线程对应的代码,同时也是把该变量的内容变成私有化内容

如果我们存在两个线程A和B,假设线程A正在执行逻辑判断,满足逻辑判断后进入到代码里面,如果此时线程A的时间片到了,那么线程A就需要被切换,但是线程A会保存自己的硬件上下文;接着线程B开始执行,线程B也开始执行逻辑判断,判断为真,进入到代码逻辑里面,如果此时线程B把全局变量减到了0时间片到了,此时线程A重新开始运行,线程A此时的硬件上下文表示全局变量还没有减到0,因此接着上次的代码继续向后运行,那么此时线程A对全局变量进行减一时就会把全局变量减到负数,由此我们就解释了为什么count会被减到负数,也就是说,因为if判断的存在,所以导致了全局变量count被减到了负数

那么此时我们还有一个问题,对于全局变量进行--的操作是安全的吗?

3这里我们只看到count--着一条代码,但实际上,count--这句代码在编译成汇编代码的时候是被编译成了三条语句

  • 第一步,将内存中的变量拷贝到cpu寄存器当中
  • 第二步:执行--操作
  • 第三步:将结果写回到内存中

那么有没有可能,我们在执行其中一步操作的时候时间片到了,然后A线程的上下文被保存,同时将B线程切换进来,假设线程B将count减到了0,然后线程A被重新切换回来,此时线程A继续在原来被切换的位置继续向下执行,并把count的值又变成了非0的值,此时可能会出现重复的值

综上所述,在多线程的场景下,对全局变量进行并发访问本身不是线程安全的

问题反思引出概念

对于上面的代码,我们来分析一下上面的背景知识里面所提到的概念:

  • 共享资源:就是count全局变量
  • 临界资源:如果我们把count保护起来,那么count就变成了临界资源
  • 临界区:访问count变量的代码

为了解决共享资源的并发访问问题,我们就需要把共享资源保护起来,形成临界区,为了保护共享资源,pthread库提出了互斥锁的概念

互斥锁

pthread库所定义的互斥锁是用pthread_mutex_t这个类型去定义的

定义互斥锁:
  • 静态分配
    如果是定义的是静态或全局的,可以使用PTHREAD_MUTEX_INITALIZER去初始化
  • 动态分配
    如果我们的互斥锁是在栈上开辟的,可以使用pthread_mutex_init这个函数去初始化
  • 我们可以使用pthread_mutex_destroy这个函数去销毁,如果我们定义的是全局的互斥锁,那么就不需要销毁,如果我们定义的是局部的,那么就需要调用pthread_mutex_destroy去销毁

我们用上面的互斥锁对上面的代码进行加锁解锁的操作:

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>

//全局的锁
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

int count=100;

void*ThreadRoutine(void*arg)
{
    char*name=(char*)arg;
    while(true)
    {
        pthread_mutex_lock(&mutex);
        if(count>0){
            usleep(1000);
            printf("%s线程开始执行,count的值为:%d\n",name,count);
            count--;
            pthread_mutex_unlock(&mutex);
        }
        else{
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return (void*)0;
}

int main()
{
    pthread_t t1,t2,t3,t4,t5;
    pthread_create(&t1,nullptr,ThreadRoutine,(void*)"thread-1");
    pthread_create(&t2,nullptr,ThreadRoutine,(void*)"thread-2");
    pthread_create(&t3,nullptr,ThreadRoutine,(void*)"thread-3");
    pthread_create(&t4,nullptr,ThreadRoutine,(void*)"thread-4");
    pthread_create(&t5,nullptr,ThreadRoutine,(void*)"thread-5");

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);
    pthread_join(t5,nullptr);

    return 0;
}

此时我们来看这个代码,我们有下面几个问题:其中的互斥锁不就是全局的吗?怎么保证锁的安全?所有的线程都需要申请锁吗?如果线程进入到临界区,会因为切换导致并发问题吗?

首先来看问题一:实际上这个互斥锁是原子的,不用担心其他线程去抢占它,具体原因我们放在之后进行分析

对于第二个问题:不能所有线程都申请锁,但是如果要加锁必须加同一把锁,如果加锁了,所有线程在访问共享资源的时候都要严格遵守先加锁再解锁的逻辑,不存在并发问题

对于第三个问题:如果对临界区加锁了,线程进入到临界区,也会被切换,但是不会因为切换而导致并发问题

而我们加锁之后,访问临界区就变成了原子的

相关推荐
C_心欲无痕2 小时前
Next.js Script 组件详解
开发语言·javascript·ecmascript·next.js
wjs20242 小时前
PHP $_GET 变量详解
开发语言
Zxxxxxy_2 小时前
Spring MVC
开发语言·spring·maven
ChoSeitaku2 小时前
28.C++进阶:map和set封装|insert|迭代器|[]
java·c++·算法
_李小白2 小时前
【Android 美颜相机】第十天:YUV420SP和RGB
android·数码相机
思茂信息2 小时前
CST仿真实例:手机Type-C接口ESD仿真
c语言·开发语言·单片机·嵌入式硬件·智能手机·cst·电磁仿真
2501_944526422 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 收藏功能实现
android·java·开发语言·javascript·python·flutter·游戏
2501_944526422 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 个人中心实现
android·java·javascript·python·flutter·游戏
u0104058362 小时前
企业微信审批事件回调的安全验证与Java HMAC-SHA256校验实现
java·安全·企业微信