33Linux 多线程抢票Bug解析与互斥量解决方案

目录

[一 进程间互斥的概念及相关背景](#一 进程间互斥的概念及相关背景)

[1.2 互斥量](#1.2 互斥量)

[1.2.1 抢票案例](#1.2.1 抢票案例)

1.2.2引入互斥量(运行速度变慢了)

[1.2.3 从两个方面解析出现互斥(数据不一致问题)](#1.2.3 从两个方面解析出现互斥(数据不一致问题))

[1.2.3.1 ticket--(非主要矛盾,但也有关)](#1.2.3.1 ticket--(非主要矛盾,但也有关))

[1.2.3.2 ticket>0](#1.2.3.2 ticket>0)

[1.2.3.3 局部互斥量及其封装锁 利用RALL思想](#1.2.3.3 局部互斥量及其封装锁 利用RALL思想)

[1.2.4 线程切换时间点](#1.2.4 线程切换时间点)

二.理解锁:理解锁为什么是原子的

[2.1 硬件级实现](#2.1 硬件级实现)

[2.2 软件级实现](#2.2 软件级实现)


在前面章节的学习中,我们对于一个对于多线程共享的资源,加上了bug的注释,本章我们将介绍为什么是bug及如何解决

一 进程间互斥的概念及相关背景

共享资源
临界资源:多线程执⾏流被保护的共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起
保护作⽤
原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,
要么未完成

1.2 互斥量

⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量
归属单个线程,其他线程⽆法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完
成线程之间的交互。
多个线程并发的操作共享变量,会带来⼀些问题。

1.2.1 抢票案例

想看看没有引入互斥,在线程并发出现的问题

bash 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int ticket=100;
void * routine(void *args)
{
    string name=static_cast<char*>(args);
    while(true)
    {
        if(ticket>0)
        {
            usleep(1000);
            cout<<name<<"抢->"<<ticket<<endl;
            --ticket;
        }
        else
            break;
    }      
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4, t5;
    pthread_create(&t1, nullptr, routine, (void*)"thread-1");
    pthread_create(&t2, nullptr, routine, (void*)"thread-2");
    pthread_create(&t3, nullptr, routine, (void*)"thread-3");
    pthread_create(&t4, nullptr, routine, (void*)"thread-4");
    pthread_create(&t5, nullptr, routine, (void*)"thread-5");

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_join(t3,NULL);
    pthread_join(t4,NULL);
    pthread_join(t5,NULL);
    return 0;
}

发现抢票结果出现了负数

1.2.2引入互斥量(运行速度变慢了)

bash 复制代码
man 3 pthread_mutex_lock   # 查看加锁函数的手册
man 3 pthread_mutex_unlock # 查看解锁函数的手册
man 3 pthread_mutex_init   # 查看初始化函数的手册

结果正常

加锁后代码及结果

bash 复制代码
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int ticket = 100;
pthread_mutex_t lock =  PTHREAD_MUTEX_INITIALIZER;
void *routine(void *args)
{
    string name = static_cast<char *>(args);
    while (true)
    {
        pthread_mutex_lock(&lock);
        if (ticket > 0)
        {
            usleep(1000);
            cout << name << "抢->" << ticket << endl;
            --ticket;
            pthread_mutex_unlock(&lock);
       
        }
        else
        {
            pthread_mutex_unlock(&lock);

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

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    pthread_join(t5, NULL);
    return 0;
}

发现加锁后结果正常

1.2.3 从两个方面解析出现互斥(数据不一致问题)

1.2.3.1 ticket--(非主要矛盾,但也有关)

c/c++的ticket--是一条语句,但这并非是原子操作,可以被打断

汇编语句是原子操作,要么执行,要么不执行

将ticket--变成汇编语言,有三句

0XFF00(载入 从内存中将ticket载入到ebx(随便给的一个寄存器))

0XFF02(减少 ebx-1)

0XFF04(写入 将ticket写回到内存) (注意:指令也是有长度的,所以上面的地址非连续)

假设只有线程a 线程b 其中ticket为1000 还有一个pc指针指向要执行的命令

先切换到线程a,该过程中,将ticket读取到ebx,再进行-1,后,当pc指向OXFF04时,OS进行了调度,切换进程,那么此时的寄存器就要对a进行上下文保存,将ebx内的值和pc指针进行保存

然后再切换到线程b中,我们假设b被分配的时间片更多些,它将ticket读取到ebx,进行-1,再写回内存,重复直到恰好写回内存时,ticket为1了

此时再进行线程切换,切换为线程a,那么就会读取寄存器存放的线程a的上下文,发现pc的意思是将ticket=999写回内存中,此时就会出现,线程a这一操作,直接将线程b前面所有的努力都浪费了

1.2.3.2 ticket>0

CPU会进行计算,为逻辑计算和算数计算,上面的为算数计算,此处的ticket>0为逻辑计算

ticket>0也并非是原子操作,但此处我们直接假设它是,方便下面讲解

假设有n个线程,当第一个线程判断完ticket>0后,时间片到了,就会发生线程切换,寄存器保存第一个线程的上下文,第2个我们也是如此,直到第n个,

当n个线程都完成了线程切换,又回到了第一个线程,那么它从寄存器取回上下文数据,执行判断完之后的代码,进行--,然后时间到了,线程切换到第二个,也是如此,直到第n个,就会出现ticket减到为负数的情况

我们上面在判断大于0后,进行休眠,就是为了然时间片为0,触发线程切换,就是为了出现该现象

1.2.3.3 局部互斥量及其封装锁 利用RALL思想
bash 复制代码
 #include <pthread.h>

       int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);
bash 复制代码
  #include <pthread.h>

       int pthread_mutex_destroy(pthread_mutex_t *mutex);
       int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);
       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

mutex.cc

bash 复制代码
#include "mutex.hpp"
#include <unistd.h>
using namespace milestone;
int ticket = 100;
class ThreadData
{
public:
    ThreadData(const string &s,  Mutex &locka)
        : name(s), locks(&locka)
    {
    }
    ~ThreadData()
    {
    }
    Mutex *locks;
    string name;
};
void *routine(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    while (true)
    {
        Lockguard(*td->locks);//利用RALL思想
        if (ticket > 0)
        {
            usleep(1000);
            cout << "抢->" << ticket << endl;
            --ticket;
    
        }
        else
        {

            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4, t5;
    Mutex lock;
    ThreadData *td1 = new ThreadData("thread 1", lock);
    pthread_create(&t1, NULL, routine, td1);
    ThreadData *td2 = new ThreadData("thread 2", lock);
    pthread_create(&t3, NULL, routine, td2);

    ThreadData *td3 = new ThreadData("thread 3", lock);
    pthread_create(&t3, NULL, routine, td3);
    ThreadData *td4 = new ThreadData("thread 4", lock);
    pthread_create(&t4, NULL, routine, td4);
    ThreadData *td5 = new ThreadData("thread 5", lock);
    pthread_create(&t5, NULL, routine, td5);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    pthread_join(t5, NULL);
    return 0;
}

mutex.hpp

bash 复制代码
#include <iostream>
#include <pthread.h>
using namespace std;
namespace milestone
{

    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void lock()
        {
            int n = pthread_mutex_lock(&_mutex);
        }
        void unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }

    private:
        pthread_mutex_t _mutex;
    };
    class Lockguard
    {
    public:
        Lockguard(Mutex &_mutex) : _mutex(_mutex)
        {
            _mutex.lock();
        }
        ~Lockguard()
        {
            _mutex.unlock();
        }

    private:
        Mutex &_mutex;
    };
}

1.2.4 线程切换时间点

  1. 时间片为0

2.阻塞是IO

3.sleep

上面操作都会让OS陷入内核

所有选择新的进程,是从内核态返回用户态时,进行检查,是否需要切换

二.理解锁:理解锁为什么是原子的

2.1 硬件级实现

我们前面说过计算机内部有个时钟,硬件实现,即关闭时间中断即可,那么就不会出现切换

2.2 软件级实现

为了实现互斥操作,大多体系结构都提供了swap/exchange指令(只有一条指令,原子性),作用是把寄存器的数据和内存单元的数据进行交换

理解: mutex初始化会被设置为1

1.假设线程a运行,先lock movb $0 %al 此操作是将自己寄存器的值设为0(无论原来为何值),

2.接着进程xchgb交换,于mutex的内存单元的值进行交换(注意,是交换,不是拷贝)(可以确保只有1这个1把锁,所有1,谁就进行原子操作)

  1. 接着进行判断,如果线程寄存器的内容大于0,就进行返回(加锁成功),结束当前函数,执行临界区

4.执行完后,进行解锁,将当前线程寄存器的值于mutex进行交换,还锁,然后再唤醒之前在等锁的进程

5.在线程a加锁后,如果线程进行切换了,线程b再进行初始化为0,于mutex进行交换后,仍为0,就进行挂起等待了,线程c d e也是如此

6.直到线程a将锁还回去后,再进行唤醒线程b,进行加锁

🔥个人主页: Milestone-里程碑

❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>

<<Git>><<MySQL>>

🌟心向往之行必能至

相关推荐
乌鸦乌鸦你的小虎牙3 小时前
qt 5.12.8 配置报错(交叉编译环境)
开发语言·数据库·qt
无心水3 小时前
【OpenClaw:实战部署】5、全平台部署OpenClaw(Win/Mac/Linux/云服务器)——10分钟跑通第一个本地AI智能体
java·人工智能·ai·智能体·ai智能体·ai架构·openclaw
feifeigo1233 小时前
Leslie人口模型MATLAB实现(中长期人口预测)
开发语言·matlab
T06205143 小时前
【数据集】“银发经济”百度搜索指数数据(2024.1.8-2026.3.8)
大数据
写代码的二次猿3 小时前
安装openfold(顺利解决版)
开发语言·python·深度学习
一只大袋鼠4 小时前
Redis 安装+基于短信验证码登录功能的完整实现
java·开发语言·数据库·redis·缓存·学习笔记
Eward-an4 小时前
LeetCode 1980 题通关指南|3种解法拆解“找唯一未出现二进制串”问题,附Python最优解实现
python·算法·leetcode
程序员酥皮蛋4 小时前
hot 100 第四十题 40.二叉树的层序遍历
数据结构·算法·leetcode
※DX3906※4 小时前
Java排序算法--全面详解面试中涉及的排序
java·开发语言·数据结构·面试·排序算法
走遍西兰花.jpg5 小时前
spark的shuffle原理及调优
大数据·分布式·spark