Linux之封装线程库和线程的互斥
Linux之封装线程库和线程的互斥与同步
- 一.封装线程库
- 二.线程的互斥
- 2.1互斥量的概念
- 2.2初始化和销毁互斥量
- 2.3加锁和解锁
- 2.4互斥量的原理
- 2.5可重入和线程安全
- 2.6死锁
一.封装线程库
其实在我们C++内部也有一个线程库而C++中的线程库也是封装的原生线程库的函数,所以我们也可以自己来封装一个自己的线程库。
//mythread.hpp
#pragma once #include <iostream>
#include <pthread.h>
#include <string>
#include <functional>
#include <unistd.h>template<class T>
using func_t = std::function<void(T)>;template<class T>
class Thread
{
public:Thread(const std::string &ThreadName,func_t<T> func,T date):_ThreadName(ThreadName),_func(func),_date(date),_tid(0),_isrunning(false){}~Thread(){}//注意:这是类内成员函数,所以它不止是有void* args这个参数//还有一个this指针当作参数,所以就会造成pthread_create调用出错//因为pthread_create函数第三个参数是一个返回值为void*参数为void*的函数指针//所以想要解决这个问题我们可以利用static关键字将其变为全局函数//并且再将this指针当作参数传给它,这样也就可以解决全局函数无法访问私有成员变量的问题static void* ThreadRoutine(void* args){Thread* ts = static_cast<Thread*>(args);ts->_func(ts->_date);return nullptr;}bool Start(){int n = pthread_create(&_tid,nullptr,ThreadRoutine,this);if(n == 0){_isrunning = true;return true;} else{return false;}}bool Join(){if(!_isrunning){return true;}int n = pthread_join(_tid,nullptr);if(n == 0){_isrunning = false;return true;}return false;}std::string Theadname(){return _ThreadName;}bool IsRunning(){return _isrunning;}private:pthread_t _tid;std::string _ThreadName;bool _isrunning;func_t<T> _func;T _date;
};
//mythread.cc
#include "mythread.hpp"std::string GetThreadName()
{static int number = 1;char name[64];snprintf(name,sizeof(name),"Thread-%d",number++);return name;
}void Print(int num)
{while(num){std::cout << "i am a new thread"<< num-- << std::endl;sleep(1);}
}int main()
{Thread<int> t(GetThreadName(),Print,10);t.Start();t.Join();return 0;
}

二.线程的互斥
2.1互斥量的概念
在聊线程的互斥之前我们可以通过一个实验来观察到我们多线程代码的一个问题
// mythread.cc
#include "mythread.hpp"std::string GetThreadName()
{static int number = 1;char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}void Print(int num)
{while (num){std::cout << "i am a new thread" << num-- << std::endl;sleep(1);}
}class ThreadDate
{
public:ThreadDate(const std::string threadname): _threadname(threadname){}std::string _threadname;
};//抢票程序
int ticket = 10000;
int GetTicket(ThreadDate* td)
{while (true){if (ticket > 0){//充当抢票的时间usleep(1000);std::cout << td->_threadname.c_str() << " get a ticket:" << ticket << std::endl;ticket--;}else{break;}}
}int main()
{std::string name1 = GetThreadName();ThreadDate* td1 = new ThreadDate(name1);Thread<ThreadDate*> t1(name1,GetTicket,td1);std::string name2 = GetThreadName();ThreadDate* td2 = new ThreadDate(name2);Thread<ThreadDate*> t2(name2,GetTicket,td2);std::string name3 = GetThreadName();ThreadDate* td3 = new ThreadDate(name3);Thread<ThreadDate*> t3(name3,GetTicket,td3);std::string name4 = GetThreadName();ThreadDate* td4 = new ThreadDate(name4);Thread<ThreadDate*> t4(name4,GetTicket,td4);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();delete td1;delete td2;delete td3;delete td4;return 0;
}
正常来说我们运行这个抢票程序最后打印的应该是抢到1号票,后面就不会再继续抢票了那么事实真的像我们想的这样吗?


我们发现并不像我们所说的那样,线程居然会抢到0号,-1号,-2号票,这根本不符合我们的代码逻辑啊这是为什么呢?
这就要提到我们之前学习进程间通信的知识了
在学习进程间通信的时候我们提到过公共资源的概念而公共资源是需要保护的这是因为如果多个执行流去访问公共资源很容易产生数据不一致的情况并且数据不一致现象的出现本质上来说也是因为访问公共资源的程序不是原子性的所以公共资源是需要被保护的。那么被保护起来的公共资源就是我们说的临界资源而访问临界资源的代码就是临界区,保护的手段当时我们也提了一下分为互斥和同步。互斥简单来说就是让任何时刻只允许一个执行流去访问公共资源,而同步则是让多个执行流保持着一种顺序去访问公共资源。
那么我们可以思考一下了我们刚刚定义的全局变量ticket是否也算是一种公共资源呢?我们模拟的抢票程序不就是在访问公共资源吗?而我们并没有对ticket进行保护它不就会出现数据不一致的情况吗?
在知道了这种情况产生的原因后我们来简单阐述一下产生的过程

在Linux中我们不太好观察汇编语句我们可以用vs来观察一下。

我们老是说因为多线程访问公共资源的操作不是原子的从而导致数据不一致的情况产生但是从来没说这个情况到底是怎么产生的其中的过程到底是什么样的。接下来我结合图和文字来为大家解释。

首先我用文字来为大家描述一下数据不一致的整个过程。
假设有两个线程A和B并且此时ticket已经为1了,线程A先执行当它在进行第一步即读取ticket的值1传到自己的寄存器后,A的时间片到了CPU切换到了线程B而A就带着自己寄存器的内容即它的上下文去等待下一次的调度。现在线程B来执行这五步操作,执行的非常顺利它成功的抢到了最后一张票并将内存中ticket的减为了0。然后CPU切换回了线程A让其继续执行这五步所以线程A将自己的上下文特别是ticket的值1写入到了寄存器中,随后继续执行未完成的第二步即让CPU来判断寄存器中存储的ticket是否大于0,判断结果是大于0的。
注意:此时的线程A的判断结果其实是错误的,因为ticket的已经被线程B减到0了但是由于线程A在切换之前已经将ticket的值读取到了自己的寄存器中而CPU进行判断又是判断的是寄存器中的内容而不是此时内存中的内容。这就导致了线程A判断结果是大于0的。
在判断结果是大于0后线程A继续执行剩下的三步,先将内存中ticket的0读取到寄存器中再进行减一最后写入到内存中从而导致了线程A抢到了本不应该存在的第0号票!
而这只是两个线程并发访问的情况要知道我们刚刚可是创建了四个线程来抢票,所以更容易产生这样的问题即可能有三个线程都是ticket=1的时候读取了ticket的值到寄存器中随后在要进行第二步的时候被切换走了,之后第四个线程完成了–的操作让ticket为0。但是那三个线程在被切换回来后还是继续执行了剩下的四步导致抢到了第0号,第-1号,第-2号。
那么要如何解决这个问题呢?如果我们只允许一个线程进入临界区去访问临界资源不就不会产生这种问题了吗,这种解决办法用专业一点的术语就是加锁也就是利用互斥。而这把锁也就叫做互斥量。
那么接下来我们来介绍一下加锁的一些接口,先了解接口我们再使用并且理解。
2.2初始化和销毁互斥量
初始化互斥量有两种方法分为静态初始化互斥量和动态初始化互斥量
- 静态初始化互斥量

非常的简单,我们可以在任意地方静态初始化互斥量并且静态的互斥量不需要销毁 - 动态初始化和销毁互斥量

动态初始化互斥量中第一个参数是一个互斥量变量第二个参数是互斥量的属性通常我们设为NULL即可。
销毁互斥量只需要传入一个互斥量变量即可。
注意:不要销毁一个被加锁的互斥量并且已经被销毁的互斥量之后要确保没有线程再对其进行加锁
2.3加锁和解锁

第一个函数就是加锁,第二个函数是检测是否能加锁,第三个函数是解锁
在加锁的时候可能会遇到两种情况:
- 这个互斥量没有被加锁,那么调用lock函数就会加锁成功并且返回0
- 这个互斥量已经被加锁或者和其他线程同时申请这个互斥量时没有竞争过其他线程,那么调用lock函数就会加锁失败并且会将次线程移入到阻塞队列中进行等待直到互斥量解锁。
而如果不想让线程在申请加锁失败的情况下被阻塞的话就可以使用第二个函trylock,使用这个函数加锁失败时只会返回错误码。
那么我们来尝试使用一下互斥量吧


2.4互斥量的原理
可是就如同我们面对信号量时的问题又出来了,这么多的线程去访问互斥量那它不就变成一个公共资源了那它不也需要被保护吗?但是它肯定不能再去让别人保护它吧所以对于互斥量的保护只能从它自己入手也就是让它对加锁的操作变成原子的。这又是如何做到的呢?

我们也可以用图来解释

想要理解这部分的内容需要大家知道几个知识
寄存器在硬件中只有一份但是寄存器的内容是具有多份的这就导致每个线程都有一个属于自己的上下文。
而xchgb的作用就是将一个共享的mutex资源交换到一个线程的自己的上下文中从而让其属于线程自己进而完成加锁的工作!!!
2.5可重入和线程安全
在之前学习进程间通信的时候我们谈到过可重入函数的概念,如今我们学会了并发多线程访问公共资源就更需要关注可重入的问题了
我们先来复习一下可重入函数的概念
当同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
同时我们要注意:可与不可重入函数是不分褒贬不分好坏的,可不可以重入只是函数的一个特点而已!
在我们学习了线程后肯定要谈论到线程的安全问题
对于线程的不安全问题一般是有以下几个情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
那么线程的不安全问题和重入函数又有什么关系呢?
我们一般来说函数是可重入那么就是线程安全的,如果函数不可重入那就不可以让多线程去使用不然就会引发线程的安全问题。并且如果一个函数中全局变量那么整个函数既不是可重入的也不是线程安全的。
而线程安全又和可重入函数有什么区别呢?为什么要把这两个概念放在一起讲述
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,但是可重入函数一定是线程安全的。
不可重入函数不一定是线程不安全的,线程不安全也不一定是不可重入函数。
线程安全与否是线程的一种特征,而函数是否可重入则是函数的一种特点。
2.6死锁
在我们了解了锁后我们现在来思考使用锁是否会带来一些问题呢?
在我们刚刚使用锁后很明显的问题的就是代码的执行速度变慢了这也很正常因为本来是多线程去访问临界资源的使用锁后变成了只能一个一个的去访问临界资源,速度当然会变慢。
但是锁的问题不仅仅出现在这里我们来假设一个场景
在这个场景中有两个线程有两把锁,A线程拥有A锁B线程拥有B锁但是于此同时呢A线程又去申请B锁B线程又去申请A锁这件造成这两个线程全部都申请失败并且进入阻塞状态从而导致整个临界区卡住了,因为两个执行流全部都阻塞住了他们俩都不愿意放下自己的锁并且还想申请别的锁。这种问题就是死锁问题。

所以死锁是指在一组进程中的各个线程均占有不会释放的资源,但因互相申请被其他线程所占用不会释放的资源而处于的一种永久等待状态。
我们上面演示的只是死锁的一种出现情况而已其实对于死锁来说有着四个必要条件:
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
这四个必要条件我们可以继续用上面的例子来进行解释:
只要你用了锁就满足了互斥条件。
像A线程那样有了A锁又去申请B锁同时还不解开A锁就满足了请求与保持条件。
A线程去申请B锁就必须在B解开锁后才能申请成功,没法直接强取豪夺了就满足了不剥夺条件。
A线程申请B锁B线程申请A锁导致A线程在等待B线程解开B锁而B线程又在等待A线程解开A锁从而满足循环等待条件。
那么想要避免死锁问题就只要破坏上面四个必要条件中的一个或者多个即可
如破坏互斥条件就只需要不用锁就行,破坏请求与保持条件就让一个线程在申请锁资源前把自己拥有的锁全部解开,破坏不剥夺条件也是类似请求与保持条件,破坏循环等待条件就是让一个线程按照顺序去申请全部的锁资源就可以。这也就是为什么多线程代码的编写难度要难的一个原因。
相关文章:
Linux之封装线程库和线程的互斥
Linux之封装线程库和线程的互斥与同步 一.封装线程库二.线程的互斥2.1互斥量的概念2.2初始化和销毁互斥量2.3加锁和解锁2.4互斥量的原理2.5可重入和线程安全2.6死锁 一.封装线程库 其实在我们C内部也有一个线程库而C中的线程库也是封装的原生线程库的函数,所以我们…...
PH热榜 | 2024-12-08
1. Reforged Labs 标语:轻松为手游工作室制作AI广告。 介绍:Reforged Labs 推出了一款前所未有的AI视频制作服务。我们自动化了以往昂贵且耗时的创意流程,取而代之的是能快速、低成本地为各个工作室量身定制视频广告。 产品网站࿱…...
LeetCode刷题day20——贪心
LeetCode刷题day20——贪心 435. 无重叠区间763. 划分字母区间分析: 56. 合并区间分析: 435. 无重叠区间 给定一个区间的集合 intervals ,其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 …...
CCF编程能力等级认证GESP—C++3级—20241207
CCF编程能力等级认证GESP—C3级—20241207 单选题(每题 2 分,共 30 分)判断题(每题 2 分,共 20 分)编程题 (每题 25 分,共 50 分)数字替换打印数字 单选题(每题 2 分,共 …...
Microi 吾码:大数据浪潮中的智能领航者
目录 一、大数据时代的挑战与机遇 二、Microi 吾码在大数据存储方面的应用 与分布式文件系统的集成 数据库存储优化 三、Microi 吾码在大数据处理与分析中的应用 数据清洗与转换 数据分析与挖掘 四、Microi 吾码在大数据可视化中的应用 五、Microi 吾码在大数据流式处…...
Lua语言入门 - Lua 数组
Lua 数组 数组,就是相同数据类型的元素按一定顺序排列的集合,可以是一维数组和多维数组。 在 Lua 中,数组不是一种特定的数据类型,而是一种用来存储一组值的数据结构。 实际上,Lua 中并没有专门的数组类型ÿ…...
gulp应该怎么用,前端批量自动化替换文件
背景 最近公司准备把所有项目中用到的国际化相关的key规范化,原因是: 一直以来公司的app和web端 在针对相同的需求以及相同的国际化语言,需要设置不同的两份国际化文件,难以维护旧版的国际化文件中,存在的大量值重复,…...
石岩湿地公园的停车场收费情况
周末石岩湿地公园停车场【967个】小车停车费封顶14元价格还行,我还记得2020年的时候湿地公园还是10元一天封顶。现在的收费情况也是可以的,尤其是周末停车比工作日停车便宜还是很得民心的哈。 车型 收费标准 小车 工作日 高峰时间8:00~20:00 首小时…...
A7157 基于Java+SSM+mysql+jsp的医院挂号系统的设计与实现 源码 文档 配置 全套资料
医院挂号系统 1.项目描述2. 绪论3.项目功能4.界面展示5.源码获取 1.项目描述 摘 要 随着计算机和网络技术的飞速发展,医院管理与互联网的结合也越来越紧密,享受便捷的医疗服务也变成了人民群众关注的重点。通过对医院就诊挂号情况的调查分析,…...
数据处理与统计分析——11-Pandas-Seaborn可视化
Seaborn 简介 Seaborn 是一个基于 Matplotlib 的图形可视化 Python 库,提供了高度交互式的接口,使用户能够轻松绘制各种吸引人的统计图表。Seaborn 可以直接使用 Pandas 的 DataFrame 和 Series 数据进行绘图。 1. Seaborn 绘制单变量图 (1) 直方图 h…...
【计算机网络】实验13:运输层端口
实验13 运输层端口 一、实验目的 本次实验旨在验证TCP和IP运输层端口号的作用,深入理解它们在网络通信中的重要性。通过实验,我将探讨端口号如何帮助区分不同的应用程序和服务,使得在同一台主机上能够同时运行多个网络服务而不发生冲突。此…...
STL之适配器(adapters)_下
STL之适配器adapters container adapersstackqueue iterator adaptgersinsert iteratorsreverse iteratorsstream iterators function adapters对返回值进行逻辑判断:not1,not2对参数进行绑定:bind1st, bind2nd用于函数合成:compose1,compose2用于函数指针 ptr_func…...
基于51单片机64位病床呼叫系统设计( proteus仿真+程序+设计报告+原理图+讲解视频)
基于51单片机病床呼叫系统设计( proteus仿真程序设计报告原理图讲解视频) 仿真图proteus7.8及以上 程序编译器:keil 4/keil 5 编程语言:C语言 设计编号:S0095 1. 主要功能: 基于51单片机的病床呼叫系统proteus仿…...
安装 Zookeeper 和 Kafka
注意:需要java环境 [roothcss-ecs-2a6a ~]# java -version java version "17.0.12" 2024-07-16 LTS Java(TM) SE Runtime Environment (build 17.0.128-LTS-286) Java HotSpot(TM) 64-Bit Server VM (build 17.0.128-LTS-286, mixed mode, sharing) [roo…...
操作系统输入输出系统知识点
I/O系统的功能、模型和接口 I/O系统的基本功能 隐藏物理设备的细节与设备的无关性提高处理机和I/O设备的利用率对1/0 设备进行控制确保对设备的正确共享 独占设备,进程应互斥地访问这些设备共享设备,在一段时间内允许多个进程同时访问的设备 错误处理 I…...
C语言控制语句与案例
控制语句与案例 1. 选择结构 1.1 if 语句 if 语句用于根据条件执行不同的代码块。最基本的语法形式如下: // 单分支 if (条件) {// 条件为真时执行的代码 }// 双分支 if (条件) {// 条件为真时执行的代码 } else {// 条件为假时执行的代码 }// 多分支 if (条件1…...
JVM的内存布局
Java虚拟机(JVM)的内存布局可以分为几个主要部分,每个部分都有特定的用途。以下是JVM内存布局的基本组成: 方法区(Method Area): 方法区是所有线程共享的内存区域,用于存储已被虚拟机…...
aws codepipeline + github + sonarqube + jenkins实践CI/CD
https://blog.csdn.net/u011564831/article/details/144007981文章浏览阅读1.2k次,点赞31次,收藏21次。本文使用 Jenkins 结合 CodeBuild, CodeDeploy 实现 Serverless 的 CI/CD 工作流,用于自动化发布已经部署 lambda 函数。在 AWS 海外区&a…...
mistralai 部署笔记
目录 mistralai 部署笔记 mistralai 部署笔记 #! /usr/bin/env python3 import os import sys import torch os.chdir(os.path.dirname(os.path.abspath(__file__)))current_dir = os.path.dirname(os.path.abspath(__file__))paths = [os.path.abspath(__file__).split(scri…...
Java——异常机制(上)
1 异常机制本质 (异常在Java里面是对象) (抛出异常:执行一个方法时,如果发生异常,则这个方法生成代表该异常的一个对象,停止当前执行路径,并把异常对象提交给JRE) 工作中,程序遇到的情况不可能完美。比如…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
C# 类和继承(抽象类)
抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...
用docker来安装部署freeswitch记录
今天刚才测试一个callcenter的项目,所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...
iview框架主题色的应用
1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题,无需引入,直接可…...
Web后端基础(基础知识)
BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。 优点:维护方便缺点:体验一般 CS架构:Client/Server,客户端/服务器架构模式。需要单独…...
Ubuntu系统复制(U盘-电脑硬盘)
所需环境 电脑自带硬盘:1块 (1T) U盘1:Ubuntu系统引导盘(用于“U盘2”复制到“电脑自带硬盘”) U盘2:Ubuntu系统盘(1T,用于被复制) !!!建议“电脑…...
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析 一、第一轮基础概念问题 1. Spring框架的核心容器是什么?它的作用是什么? Spring框架的核心容器是IoC(控制反转)容器。它的主要作用是管理对…...
