【Linux】线程互斥

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

🌈前言

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


🌸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表示申请锁成功而其他线程会一直挂起/阻塞等待

在这里插入图片描述

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux