Linux -- 从抢票逻辑理解线程互斥

news2025/2/7 8:35:12

目录

抢票逻辑代码:

thread.hpp

thread.cc

运行结果:

为什么票会抢为负数?

概念前言

临界资源

临界区

原子性

 数据不一致

为什么数据不一致?

互斥

概念 

pthread_mutex_init(初始化互斥锁)

pthread_mutex_lock(申请互斥锁)

pthread_mutex_unlock(释放互斥锁)

pthread_mutex_destory(销毁互斥锁)

全局的互斥锁

thread.cc 代码

局部的互斥锁

thread.cc 代码


抢票逻辑代码:

thread.hpp

#ifndef __THREAD_HPP__
#define __THREAD_HPP__
#include<vector>
#include<iostream>
#include<string>
#include<functional>
#include<pthread.h>
#include<unistd.h>

namespace ThreadModule
{
    //给 函数参数为T(T为任意类型)的引用,返回值为 void 的函数 重命名为func_t
    template<typename T>
    using func_t=std::function<void(T&)>;

    template<typename T>
    class Thread
    {
    public:
        //线程的任务
        void Excute()
        {
            _func(_data);
        }
    public:
        //构造函数
        Thread(func_t<T> func, T &data,const std::string &name="none-name")
            :_func(func),_data(data),_threadname(name),_stop(true)
        {  }

        //如果没有static,由于 this 指针,函数的参数有2个,而pthread_create要求函数参数只能有void*
        //加上static,则要求函数不能访问类内的非静态成员变量,也就避免了this指针作为函数参数        
        static void* threadroute(void* args)
        {
            //参数从void* 类型转为Thread<T> *类型,static_cast是一种相对安全的类型转换方式
            Thread<T> *self=static_cast<Thread<T> *>(args);

            //由于没有了this指针,所以需要封装Excute函数来传递 _data参数,从而执行任务
            self->Excute();
            return nullptr;
        }
        //开始执行任务
        bool Start()
        {
            //创建线程
            int n=pthread_create(&_tid,nullptr,threadroute,this);
            if(!n)
            {              
                _stop=false;//修改状态
                return true;
            }
            else
            {
                return false;
            }
        }
        void Detach()
        {
            //有线程启动了才分离线程
            if(!_stop)
                pthread_detach(_tid);
        }
        void Join()
        {
            if(!_stop)
                pthread_join(_tid,nullptr);
        }
        std::string name()
        {
            return _threadname;
        }
        void Stop()
        {
            _stop=true;
        }
        //析构函数
        ~Thread()
        {  }
    private:
        std::string _threadname;//线程名
        bool _stop;//该线程是否启动,true表示未启动,false表示已启动
        pthread_t _tid;
        T &_data;
        func_t<T> _func;//线程调用的函数
    };
}
#endif

thread.cc

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<vector>
using namespace std;
#include"thread.hpp"
using namespace ThreadModule;

int g_tickets=10000;//共享资源,未保护
const int num=4;

void route(int &tickets)
{
    //票没抢完就一直抢
    while(true)
    {
        if(tickets>0)//还有票,可以继续抢
        {
            usleep(1000);
            //目前抢到的票
            printf("get tickets:%d\n",tickets);
            tickets--;
        }
        else//已经没票了,不能抢了,退出
        {
            break;
        }

    }
}

int main()
{
    std::vector<Thread<int>> threads;
    //创建线程
    for(int i=0;i<num;i++)
    {
        std::string name="thread-"+std::to_string(i+1);
        threads.emplace_back(route,g_tickets,name);
    }

    //启动线程
    for(auto &threads:threads)
    {
        threads.Start();
    }

    //等待线程
    for(auto &threads:threads)
    {
        threads.Join();
        std::cout<<"wait thread done, thread is: "<<threads.name()<<std::endl;
    }

    //
    return 0;
}

运行结果:

发现票数被减为了负数,且有的票被重复抢了,每次运行的结果都不一样。

 

为什么票会抢为负数?

概念前言

临界资源

多线程执行流共享的资源称为临界资源。

临界区

每个线程内部,访问了临界资源的代码称为临界区。

原子性

不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

 数据不一致

在多线程或分布式系统中,由于并发操作或其他因素导致的数据状态不符合预期的情况。当多个线程或进程同时访问和修改共享资源时,如果没有适当的同步机制,可能会出现数据不一致的问题。这可能导致系统的不稳定、错误的结果或难以调试的行为。例如,上述的运行结果中出现了剩余的票数为负数的情况,而剩余的票数不应该出现负数,数据状态不符合预期,即数据不一致

为什么数据不一致?

我们可以来模拟一下代码的整个运行过程。

在代码中,我们创建了4个线程,每个线程都要执行以下代码,函数内中一共有 3 个地方访问了临界资源,标为1、2、3:

void route(int &tickets)
{
    //票没抢完就一直抢
    while(true)
    {
        //还有票,可以继续抢
        if(tickets>0)//1
        {
            usleep(1000);
            //目前剩下的票数
            printf("get tickets:%d\n",tickets);//2
            tickets--;//3
        }
        else//已经没票了,不能抢了,退出
        {
            break;
        }

    }
}

假设现在只剩一张票了,即 g_tickets = 1

假设现在执行的是线程 1,线程 1 进行 tickets>0 的判断,这个判断过程是由 CPU 来完成的。

系统把 g_tickets 的值从内存读到 CPU 的寄存器 ebx 中,判断结果为真,线程 1 开始执行 if 的代码块,还没执行到打印操作,线程 1 被挂起并切走了,切走时线程 1 带走了寄存器的上下文数据,g_tickets 还是 1,还没有写回到内存中

此时轮到线程 2 执行函数了,线程 2 也进行了 tickets>0 的判断,由于 g_tickets 的值依旧为 1,和线程 1 的过程一样,所以线程 2 也执行 if 的代码块, 线程 2 也还没有执行到打印操作,就被挂起并切走了。线程 3 同理。

再次轮到线程 1 时,由于已经进行过 if 判断了,线程 1 直接执行打印操作,对 tickets -- 并把 tickets 的值写回到内存中,g_tickets 的值变为 0。 这里我们需要了解到,tickets-- 看似只有一句代码,其实要分为三个过程来执行:

  1. 把 tickets 从内存中读到 CPU 中;
  2. CPU 进行 -- 操作;
  3. 把 tickets 的值写回内存中。

再次轮到线程 2,因为线程 2 已经进行过 if 判断了,线程 2 以为 tickets 还是1,也直接执行打印操作,把 tickets-- 并写回到内存中,但线程 2 在进行 tickets -- 操作时,读到的 tickets 已经是 0 了,-- 操作后,tickets 变为 -1,内存中的 g_tickets 的值变为 -1.

线程 3 也是同理,-- 后 tickets 变为 -2,写回到内存后,内存中的 g_tickets 的值变为 -2.

就这样 g_tickets 的值被减到了负数!

也就是说,多线程访问共享资源 g_tickets 时,由于共享资源 g_tickets 未被保护,且 -- 操作不是原子的在执行任何一个步骤时线程都可能被切换,导致产生了计算过程的中间状态

互斥

由于多个执行流访问全局数据的代码,所以会发生上面的问题,多个线程共享的全局数据就是临界资源,访问了全局数据的代码其实就是临界区,换句话说,保护临界区,就可以保护临界资源,就可以解决上面数据不一致的问题。

概念 

任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,对临界资源起保护作用。

pthread_mutex_init(初始化互斥锁)

#include <pthread.h>

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

初始化一个互斥锁。 

mutex:指向要初始化的互斥锁对象的指针

attr:指向互斥锁属性对象的指针,可以为NULL以使用默认属性。

pthread_mutex_lock(申请互斥锁)

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);

尝试获取互斥锁。如果锁已经被其他线程持有,则当前线程将被阻塞,直到锁可用。

mutex:指向要锁定的互斥锁对象的指针

pthread_mutex_unlock(释放互斥锁)

#include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);

释放一个互斥锁,允许其他等待的线程获取该锁

mutex:指向要解锁的互斥锁对象的指针

pthread_mutex_destory(销毁互斥锁)

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);

销毁一个互斥锁,释放相关资源。 

mutex:指向要销毁的互斥锁对象的指针

加锁

注意加锁应该精细,只需要在临界区加锁,非临界区不需要加锁! 加锁成功后,只有申请到互斥锁的线程才可以访问临界区,其他线程阻塞等待,直到锁释放后,其他线程成功竞争到互斥锁,才可以访问临界区!

全局的互斥锁

如果互斥锁是全局的,或者静态的,则不需要 init 和 destory

thread.cc 代码

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<vector>
using namespace std;
#include"thread.hpp"
using namespace ThreadModule;

int g_tickets=10000;//共享资源,未保护
const int num=4;

//一把全局的锁
pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;

void route(int &tickets)
{
    //票没抢完就一直抢
    while(true)
    {
        pthread_mutex_lock(&gmutex);//申请锁
        //还有票,可以继续抢
        if(tickets>0)//1
        {
            usleep(1);
            //目前剩下的票数
            printf("get tickets:%d\n",tickets);//2
            tickets--;//3
            pthread_mutex_unlock(&gmutex);//释放锁
        }
        else//已经没票了,不能抢了,退出
        {
            pthread_mutex_unlock(&gmutex);//释放锁
            break;
        }

    }
}

int main()
{
    std::vector<Thread<int>> threads;
    //创建线程
    for(int i=0;i<num;i++)
    {
        std::string name="thread-"+std::to_string(i+1);
        threads.emplace_back(route,g_tickets,name);
    }

    //启动线程
    for(auto &threads:threads)
    {
        threads.Start();
    }

    //等待线程
    for(auto &threads:threads)
    {
        threads.Join();
        std::cout<<"wait thread done, thread is: "<<threads.name()<<std::endl;
    }

    //
    return 0;
}

不再出现抢到负数的票和抢到重复的票的情况了,且每次运行结果都一样:

局部的互斥锁

thread.cc 代码

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<vector>
#include<mutex>
using namespace std;
#include"thread.hpp"
using namespace ThreadModule;

int g_tickets=10000;//共享资源,未保护
const int num=4;

//因为锁变成局部的,为了让route可以访问互斥锁,且统计每个线程抢到了多少张票,定义一个类
class ThreadData
{
public:
    ThreadData(int &tickets,std::string name,pthread_mutex_t &mutex)
        :_tickets(tickets),_name(name),_mutex(mutex),_total(0)
    { }
    ~ThreadData()
    {   }
public:
    int &_tickets;//所有线程最终都会引用同一个全局变量g_tickets
    std::string _name;
    int _total;
    pthread_mutex_t &_mutex;
};

void route(ThreadData *td)
{
    //票没抢完就一直抢
    while(true)
    {
        pthread_mutex_lock(&td->_mutex);//申请锁
        //还有票,可以继续抢
        if(td->_tickets>0)//1
        {
            usleep(1);
            //目前剩下的票数
            printf("%s running, get tickets:%d\n",td->_name.c_str(),td->_tickets);//2
            td->_tickets--;//3
            td->_total++;
            pthread_mutex_unlock(&td->_mutex);//释放锁
        }
        else//已经没票了,不能抢了,退出
        {
            pthread_mutex_unlock(&td->_mutex);//释放锁
            break;
        }

    }
}

int main()
{
    //一把局部的锁
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex,nullptr);//初始化互斥锁

    std::vector<Thread<ThreadData*>> threads;
    std::vector<ThreadData*> datas;
    //创建线程
    for(int i=0;i<num;i++)
    {
        std::string name="thread-"+std::to_string(i+1);
        ThreadData* td = new ThreadData(g_tickets,name,mutex);
        threads.emplace_back(route,td,name);
        datas.emplace_back(td);
    }

    //启动线程
    for(auto &threads:threads)
    {
        threads.Start();
    }

    //等待线程
    for(auto &threads:threads)
    {
        threads.Join();
        //std::cout<<"wait thread done, thread is: "<<threads.name()<<std::endl;
    }

    for(auto data:datas)
    {
        std::cout<<data->_name<<" : "<<data->_total<<std::endl;
        delete data;
    }
    pthread_mutex_unlock(&mutex);
    //
    return 0;
}

封装成类

LockGuard.hpp 代码

#ifndef __LOCK_GUARD_HPP__
#define __LOCK_GUARD_HPP__
#include<pthread.h>
#include<iostream>
class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex)
        :_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);//加锁
    }

    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);//解锁
    }
private:
    pthread_mutex_t *_mutex;
};

#endif

thread.cc 代码

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<vector>
#include<mutex>
using namespace std;
#include"thread.hpp"
#include"LockGuard.hpp"
using namespace ThreadModule;
 
int g_tickets=10000;//共享资源,未保护
const int num=4;
 
//因为锁变成局部的,为了让route可以访问互斥锁,且统计每个线程抢到了多少张票,定义一个类
class ThreadData
{
public:
    ThreadData(int &tickets,std::string name,pthread_mutex_t &mutex)
        :_tickets(tickets),_name(name),_mutex(mutex),_total(0)
    { }
    ~ThreadData()
    {   }
public:
    int &_tickets;//所有线程最终都会引用同一个全局变量g_tickets
    std::string _name;
    int _total;
    pthread_mutex_t &_mutex;
};
 
void route(ThreadData *td)
{
    //票没抢完就一直抢
    while(true)
    {
        LockGuard guard(&td->_mutex);//临时对象
     
        //还有票,可以继续抢
        if(td->_tickets>0)//1
        {
            usleep(1);
            //目前剩下的票数
            printf("%s running, get tickets:%d\n",td->_name.c_str(),td->_tickets);//2
            td->_tickets--;//3
            td->_total++;
            
        }
        else//已经没票了,不能抢了,退出
        {
            
            break;
        }
 
    }
}
 
int main()
{
    //一把局部的锁
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex,nullptr);//初始化互斥锁
 
    std::vector<Thread<ThreadData*>> threads;
    std::vector<ThreadData*> datas;
    //创建线程
    for(int i=0;i<num;i++)
    {
        std::string name="thread-"+std::to_string(i+1);
        ThreadData* td = new ThreadData(g_tickets,name,mutex);
        threads.emplace_back(route,td,name);
        datas.emplace_back(td);
    }
 
    //启动线程
    for(auto &threads:threads)
    {
        threads.Start();
    }
 
    //等待线程
    for(auto &threads:threads)
    {
        threads.Join();
        //std::cout<<"wait thread done, thread is: "<<threads.name()<<std::endl;
    }
 
    for(auto data:datas)
    {
        std::cout<<data->_name<<" : "<<data->_total<<std::endl;
        delete data;
    }
    pthread_mutex_unlock(&mutex);
    //
    return 0;
}

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

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

相关文章

1.微服务灰度发布落地实践(方案设计)

前言 微服务架构中的灰度发布&#xff08;也称为金丝雀发布或渐进式发布&#xff09;是一种在不影响现有用户的情况下&#xff0c;逐步将新版本的服务部署到生产环境的策略。通过灰度发布&#xff0c;你可以先将新版本的服务暴露给一小部分用户或特定的流量&#xff0c;观察其…

从 Coding (Jenkinsfile) 到 Docker:全流程自动化部署 Spring Boot 实战指南(简化篇)

前言 本文记录使用 Coding (以 Jenkinsfile 为核心) 和 Docker 部署 Springboot 项目的过程&#xff0c;分享设置细节和一些注意问题。 1. 配置服务器环境 在实施此过程前&#xff0c;确保服务器已配置好 Docker、MySQL 和 Redis&#xff0c;可参考下列链接进行操作&#xff1…

丢失的MD5

丢失的MD5 源代码&#xff1a; import hashlib for i in range(32,127):for j in range(32,127):for k in range(32,127):mhashlib.md5()m.update(TASCchr(i)O3RJMVchr(j)WDJKXchr(k)ZM)desm.hexdigest()if e9032 in des and da in des and 911513 in des:print des 发现给…

基于51单片机的交通灯外部中断proteus仿真

地址&#xff1a; https://pan.baidu.com/s/1WSlta_7pz5HdWsyIGoviHg 提取码&#xff1a;1234 仿真图&#xff1a; 芯片/模块的特点&#xff1a; AT89C52/AT89C51简介&#xff1a; AT89C52/AT89C51是一款经典的8位单片机&#xff0c;是意法半导体&#xff08;STMicroelectro…

JavaWeb(一) | 基本概念(web服务器、Tomcat、HTTP、Maven)、Servlet 简介

1. 基本概念 1.1、前言 web开发&#xff1a; web&#xff0c;网页的意思&#xff0c;www.baidu.com静态 web html,css提供给所有人看的数据始终不会发生变化&#xff01; 动态 web 淘宝&#xff0c;几乎是所有的网站&#xff1b;提供给所有人看的数据始终会发生变化&#xf…

C语言性能优化:从基础到高级的全面指南

引言 C 语言以其高效、灵活和功能强大而著称&#xff0c;被广泛应用于系统编程、嵌入式开发、游戏开发等领域。然而&#xff0c;要写出高性能的 C 语言代码&#xff0c;需要对 C 语言的特性和底层硬件有深入的了解。本文将详细介绍 C 语言性能优化的背后技术&#xff0c;并通过…

C语言-数据结构-查找

目录 一,查找的概念 二,线性查找 1,顺序查找 2,折半查找 3,分块查找 三,树表的查找 1,二叉排序树 (1)查找方式: (2)、二叉排序树的插入和生成 (3)、二叉排序树的删除 2,平衡二叉树 (1)、什么是平衡二叉树 (2)、平衡二叉树的插入调整 &#xff08;1&#xff09;L…

[江科大编程技巧] 第1期 定时器实现非阻塞式程序 按键控制LED闪烁模式——笔记

提前声明——我只是写的详细其实非常简单&#xff0c;不要看着多就放弃学习&#xff01; 阻塞&#xff1a;执行某段程序时&#xff0c;CPU因为需要等待延时或者等待某个信号而被迫处于暂停状态一段时间&#xff0c;程序执行时间较长或者时间不定 非阻塞&#xff1a;执行某段程…

如何理解:产品线经营管理的战略、组织、业务、项目、流程、绩效之间的逻辑关系?-中小企实战运营和营销工作室博客

如何理解&#xff1a;产品线经营管理的战略、组织、业务、项目、流程、绩效之间的逻辑关系&#xff1f;-中小企实战运营和营销工作室博客 产品线经营管理中&#xff0c;战略、组织、业务、项目、流程、绩效之间存在着紧密的逻辑关系&#xff0c;它们相互影响、相互作用&#xf…

【CSS in Depth 2 精译_096】16.4:CSS 中的三维变换 + 16.5:本章小结

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第五部分 添加动效 ✔️【第 16 章 变换】 ✔️ 16.1 旋转、平移、缩放与倾斜 16.1.1 变换原点的更改16.1.2 多重变换的设置16.1.3 单个变换属性的设置 16.2 变换在动效中的应用 16.2.1 放大图标&am…

Oracle 11G还有新BUG?ORACLE 表空间迷案!

前段时间遇到一个奇葩的问题&#xff0c;在开了SR和oracle support追踪两周以后才算是有了不算完美的结果&#xff0c;在这里整理出来给大家分享。 1.问题描述 12/13我司某基地MES全厂停线&#xff0c;系统卡死不可用&#xff0c;通知到我排查&#xff0c;查看alert log看到是…

三只脚的电感是什么东西?

最近在做加湿器&#xff0c;把水雾化的陶瓷片需要有专门的驱动电路。 我参考了某宝卖家的驱动板以及网上的开源项目&#xff0c;发现了驱动电路的核心就是一个三脚电感。 在此之前我都没注意过这玩意&#xff0c;三脚电感不也还是电感嘛&#xff1f; 今天我们就来看看三脚电…

pyQT + OpenCV相关练习

一、设计思路 1、思路分析与设计 本段代码是一个使用 PyQt6 和 OpenCV 创建的图像处理应用程序。其主要功能是通过一个图形界面让用户对图片进行基本的图像处理操作&#xff0c;如灰度化、翻转、旋转、亮度与对比度调整&#xff0c;以及一些滤镜效果&#xff08;模糊、锐化、边…

【Git_bugs】remote error GH013 Repository rule violations found for.md

背景 1 在一个分支上的提交顺序如下&#xff1a;-> 代表新的提交 在提交 E 中&#xff0c;文件包含了 GitHub 生成的 token提交 F 是一次普通的提交&#xff0c;不包含 token A -> ... -> E -> F (敏感信息在 E 中)附&#xff1a;给提交起名是为了方便说明问题。…

Day1 微服务 单体架构、微服务架构、微服务拆分、服务远程调用、服务注册和发现Nacos、OpenFeign

目录 1.导入单体架构项目 1.1 安装mysql 1.2 后端 1.3 前端 2.微服务 2.1 单体架构 2.2 微服务 2.3 SpringCloud 3.微服务拆分 3.1 服务拆分原则 3.1.1 什么时候拆 3.1.2 怎么拆 3.2 拆分购物车、商品服务 3.2.1 商品服务 3.2.2 购物车服务 3.3 服务调用 3.3.1 RestTemplate 3.…

安卓执法仪Android接入国标GB28181平台实现实时监控、对讲、报警、定位等管理方案

最近协助不少企业完成了4G无线设备国标接入的需求&#xff0c;尤其是国产芯片的接入&#xff0c;国标发展了十年的时间&#xff0c;目前协议从完成度、性能、AI等各个方面&#xff0c;都已经非常完美地满足各种各样的场景需求&#xff0c;尤其是GB28181-2022的推出&#xff0c;…

SpringMVC学习(二)——RESTful API、拦截器、异常处理、数据类型转换

一、RESTful (一)RESTful概述 RESTful是一种软件架构风格&#xff0c;用于设计网络应用程序。REST是“Representational State Transfer”的缩写&#xff0c;中文意思是“表现层状态转移”。它基于客户端-服务器模型和无状态操作&#xff0c;以及使用HTTP请求来处理数据。RES…

国内独立开发者案例及免费送独立开发蓝图书

独立开发者在国内越来越受到关注&#xff0c;他们追求的是一种自由且自给自足的工作状态。 送这个&#xff1a; 少楠light&#xff08;Flomo、小报童、如果相机&#xff09;&#xff1a;他们是独立开发者的典范&#xff0c;不仅开发了多款产品&#xff0c;还坚信“剩者为王”…

【JavaEE进阶】@RequestMapping注解

目录 &#x1f4d5;前言 &#x1f334;项目准备 &#x1f332;建立连接 &#x1f6a9;RequestMapping注解 &#x1f6a9;RequestMapping 注解介绍 &#x1f384;RequestMapping是GET还是POST请求&#xff1f; &#x1f6a9;通过Fiddler查看 &#x1f6a9;Postman查看 …

一文详解MacOS+CLion——构建libtorch机器学习开发环境

对于希望在本地环境中进行深度学习开发的开发者来说&#xff0c;配置合适的工具链是至关重要的一步。本文旨在帮助您在 macOS 操作系统上&#xff0c;利用 CLion IDE 和 PyTorch 的 C依赖库——libtorch&#xff0c;快速搭建起一个高效的开发环境。这里我们将一步步地讲解如何下…