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

目录

抢票逻辑代码:

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_ttemplate<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);//2tickets--;//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);//2tickets--;//3pthread_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_ticketsstd::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);//2td->_tickets--;//3td->_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_ticketsstd::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);//2td->_tickets--;//3td->_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.mzph.cn/bicheng/65050.shtml

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

相关文章

浅谈目前我开发的前端项目用到的设计模式

浅谈目前我开发的前端项目用到的设计模式 前言 设计模式很多&#xff0c;看到一个需求&#xff0c;项目&#xff0c;我们去开发的时候&#xff0c;肯定是做一个整体的设计进行开发&#xff0c;而在这次我项目中&#xff0c;我也做了一个整体的设计&#xff0c;为什么要设计&a…

ubuntu笔记

1.系统下载与虚拟机设置 系统下载https://cn.ubuntu.comhttps://releases.ubuntu.com 虚拟机设置: 桥接模式 在桥接模式下, 虚拟出来的操作系统就像是局域网中的一台独立的主机, 它可以访问网内任何一台机器主机网卡和虚拟网卡的IP地址处于同一个网段, 子网掩码、网关、DNS等…

开放世界目标检测 Grounding DINO

开放世界目标检测 Grounding DINO flyfish Grounding DINO 是一种开创性的开放集对象检测器&#xff0c;它通过结合基于Transformer的检测器DINO与基于文本描述的预训练技术&#xff0c;实现了可以根据人类输入&#xff08;如类别名称或指代表达&#xff09;检测任意对象的功…

【基础篇】1. JasperSoft Studio编辑器与报表属性介绍

编辑器介绍 Jaspersoft Studio有一个多选项卡编辑器&#xff0c;其中包括三个标签&#xff1a;设计&#xff0c;源代码和预览。 Design&#xff1a;报表设计页面&#xff0c;可以图形化拖拉组件设计报表&#xff0c;打开报表文件的主页面Source&#xff1a;源代码页码&#xff…

电子应用设计方案71:智能客厅窗帘系统设计

智能客厅窗帘系统设计 一、引言 智能客厅窗帘系统为用户提供了更加便捷、舒适和个性化的窗帘控制方式&#xff0c;提升了家居的智能化水平和生活品质。 二、系统概述 1. 系统目标 - 实现客厅窗帘的自动开合控制&#xff0c;可通过多种方式操作。 - 能够根据时间、光照强度和用…

免杀对抗—Behinder魔改流量特征去除

前言 在现实的攻防中&#xff0c;往往webshell要比主机后门要用得多&#xff0c;因为我们首先要突破的目标是网站嘛&#xff0c;而且waf也往往会更注重webshell的检测。webshell的免杀分为两个&#xff0c;一是静态查杀&#xff0c;二是流量查杀。静态查杀不用多说了&#xff…

高阶:基于Python paddleocr库 提取pdf 文档高亮显示的内容

预览 第1步&#xff1a;理解基本结构和导入必要的库 # 1. 首先导入需要的库 import os # 用于处理文件和路径 import cv2 # 用于图像处理 import numpy as np # 用于数值计算 from paddleocr import PaddleOCR # 用于文字识别 from pdf2image import convert_from_path #…

如何查看pad的console输出,以便我们更好的进行调试,查看并了解实际可能的问题。

1、以下是baidu AI回复&#xff1a; 2、说明&#xff1a; 1&#xff09;如果小伙伴们经常做android开发的话&#xff0c;这个不陌生&#xff0c;因为调试都是要开启这个开发者模式。并启用USB调试模式。 2&#xff09;需要连上USB线&#xff0c;有的时候会忘记&#xff0c;然…

GitHub 桌面版配置 |可视化界面进行上传到远程仓库 | gitLab 配置【把密码存在本地服务器】

&#x1f947; 版权: 本文由【墨理学AI】原创首发、各位读者大大、敬请查阅、感谢三连 &#x1f389; 声明: 作为全网 AI 领域 干货最多的博主之一&#xff0c;❤️ 不负光阴不负卿 ❤️ 文章目录 桌面版安装包下载clone 仓库操作如下GitLab 配置不再重复输入账户和密码的两个方…

智慧社区电子商务系统:实现社区资源的数字化管理

2.1vue技术 Vue (读音 /vjuː/&#xff0c;类似于 view) 是一套用于构建用户界面的渐进式JavaScript框架。 [5] 与其它大型框架不同的是&#xff0c;Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层&#xff0c;不仅易于上手&#xff0c;还便于与第三方库或既有项…

【软件项目管理】-期末突击

区别常见的项目和活动 项目和活动的区别&#xff1a; 定义&#xff1a; 项目&#xff1a;为创造独特成果而进行的临时性工作。活动&#xff1a;日常运营中的重复性工作。 目标&#xff1a; 项目&#xff1a;实现特定成果&#xff0c;一次性。活动&#xff1a;维持日常运作&am…

【C++】ceil 和 floor 函数的实现与分析

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;ceil 和 floor 函数的基础介绍1. ceil 函数定义与功能示例代码输出结果功能分析使用场景 2. floor 函数定义与功能示例代码输出结果功能分析使用场景 &#x1f4af;自行实现…

合合信息:探索视觉内容安全新前沿

2024年12月13日-15日&#xff0c;中国图象图形学学会在杭州召开。大会期间&#xff0c;来自合合信息的图像算法研发总监郭丰俊进行了主题为“视觉内容安全技术的前沿进展与应用”的演讲&#xff0c;介绍了视觉内容安全问题&#xff0c;并总结了现今的技术发展&#xff0c;对我很…

【JetPack】Navigation知识点总结

Navigation的主要元素&#xff1a; 1、Navigation Graph&#xff1a; 一种新的XML资源文件,包含应用程序所有的页面&#xff0c;以及页面间的关系。 <?xml version"1.0" encoding"utf-8"?> <navigation xmlns:android"http://schemas.a…

教师如何打造专属私密成绩查询系统?

期末的校园&#xff0c;被一种特殊的氛围所笼罩。老师们如同辛勤的工匠&#xff0c;精心打磨着每一个教学环节。复习阶段&#xff0c;他们在知识的宝库中精挑细选&#xff0c;把一学期的重点内容一一梳理&#xff0c;为学生们打造出系统的复习框架。课堂上&#xff0c;他们激情…

专栏二十三:Python读取和分析空间数据的经验杂谈

部分情况同样适合单细胞的分析结果 读取数据阶段 1.错误的library_id 包括sc和sq的两种读取方式&#xff0c;大同小异。 理论上有h5数据和spatial文件夹就可以读取成功&#xff0c;并且自动赋予和文件名一样的library_id&#xff0c;例如 slide sq.read.visium("/ho…

如何在谷歌浏览器中启用语音搜索

想象一下&#xff0c;你正在拥挤的地铁上&#xff0c;双手都拿着沉重的购物袋&#xff0c;突然你想搜索附近的咖啡馆。此时如果你能通过语音而不是打字来进行搜索&#xff0c;那将多么的便利&#xff01;在谷歌浏览器中&#xff0c;启用语音搜索功能就是这么简单而高效&#xf…

GCP GCA认证考试Case错题库1(JenciMart+Helicopter+EHR)

GCP GCA认证考试Case错题库1(JenciMartHelicopterEHR) 整理by Moshow郑锴https://zhengkai.blog.csdn.net/ JenciMart 在生产和开发资源之间进行管理职责分离的最小权限模型中&#xff0c;最佳实践是每个应用程序的每个阶段都有自己的项目。这种设置确保权限是细化的&#xf…

MySQL HA 方案 MMM、MHA、MGR、PXC 对比

MySQL高可用架构 MMM (Multi Master Replication Manager) 资源数量说明主DB2用于主备模式的主主复制从DB0~N台可以根据需要配置N台从服务器IP地址2n1N为MySQL服务器的数量监控用户1用户监控数据库状态的MySQL用户(replication)代理用户1用于MMM代理端改变read_only状态 故障…

java12.24日记

运算符&#xff1a; 算术运算符&#xff1a; 顾名思义进行算数运算的 多为&#xff1a;四则运算&#xff0c;加一个取余 &#xff0c;-&#xff0c;*&#xff0c;/以及 %&#xff08;取余&#xff09; 而外的&#xff1a;自增 以及自减--&#xff0c;对原数进行1或者-1 i…