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初始化和销毁互斥量

初始化互斥量有两种方法分为静态初始化互斥量和动态初始化互斥量

  1. 静态初始化互斥量
    在这里插入图片描述
    非常的简单,我们可以在任意地方静态初始化互斥量并且静态的互斥量不需要销毁
  2. 动态初始化和销毁互斥量
    在这里插入图片描述

动态初始化互斥量中第一个参数是一个互斥量变量第二个参数是互斥量的属性通常我们设为NULL即可。
销毁互斥量只需要传入一个互斥量变量即可。

注意:不要销毁一个被加锁的互斥量并且已经被销毁的互斥量之后要确保没有线程再对其进行加锁

2.3加锁和解锁


第一个函数就是加锁,第二个函数是检测是否能加锁,第三个函数是解锁
在加锁的时候可能会遇到两种情况:

  1. 这个互斥量没有被加锁,那么调用lock函数就会加锁成功并且返回0
  2. 这个互斥量已经被加锁或者和其他线程同时申请这个互斥量时没有竞争过其他线程,那么调用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锁从而满足循环等待条件。

那么想要避免死锁问题就只要破坏上面四个必要条件中的一个或者多个即可
如破坏互斥条件就只需要不用锁就行,破坏请求与保持条件就让一个线程在申请锁资源前把自己拥有的锁全部解开,破坏不剥夺条件也是类似请求与保持条件,破坏循环等待条件就是让一个线程按照顺序去申请全部的锁资源就可以。这也就是为什么多线程代码的编写难度要难的一个原因。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PHP语法学习(第九天)—PHP连接mysql详解(下)

首先&#xff0c;温馨提示&#xff0c;该部分内容跟昨天“PHP语法学习(第八天)—PHP连接mysql详解(上)”一起食用更佳噢&#xff01;&#xff01; 学习本篇内容必须掌握数据库基础命令点击“MYSQL 数据库”~~ 本文是接着PHP连接mysql的知识点接着讲&#xff0c;今天主要讲述PHP…

qt基本部分控件用法(一)

前言: 以前 windows下做工具主要是MFC&#xff0c;趁有点空时间&#xff0c;研究了QT&#xff0c;感觉跟MFC 差不多&#xff0c;VS 比 QT CREATOR 还是强大&#xff0c;不过QT可以跨平台&#xff0c;功能更强大&#xff0c;MFC 只能在win平台下.&#xff1b; 1&#xff1a;环境…

Mysql索引,聚簇索引,非聚簇索引,回表查询

什么是索引 数据库索引是为了实现高效数据查询的一种有序的数据数据结构&#xff0c;类似于书的目录&#xff0c;通过目录可以快速的定位到想要的数据&#xff0c;因为一张表中的数据会有很多&#xff0c;如果直接去表中检索数据效率会很低&#xff0c;所以需要为表中的数据建立…

以MP6924A为核心的LLC拓扑学习【一】

PFCLLC: 在PFC&#xff08;功率因数校正&#xff09;和LLC&#xff08;谐振变换器&#xff09;组成的电源系统中&#xff0c;各个电路有特定的作用&#xff0c;它们协同工作以实现高效率和高功率因数的电能转换。 1. PFC&#xff08;功率因数校正&#xff09;电路的作用 PFC电…

️ 在 Windows WSL 上部署 Ollama 和大语言模型的完整指南20241206

&#x1f6e0;️ 在 Windows WSL 上部署 Ollama 和大语言模型的完整指南 &#x1f4dd; 引言 随着大语言模型&#xff08;LLM&#xff09;和人工智能的飞速发展&#xff0c;越来越多的开发者尝试在本地环境中部署大模型进行实验。然而&#xff0c;由于资源需求高、网络限制多…

1-1 ESP32开发环境配置

前言&#xff1a; 基于Arduio配置ESP32开发环境... 目录 前言&#xff1a; 1.0 安装Python 2.0 安装VSCode 3.0 VSCode实用插件 4.0 替换VSCode配置&#xff08;可选&#xff09; 后记 1.0 安装Python 在windows操作系统的搜索框中搜索Microsoft Store 点击获取 安装完成…

【k8s 深入学习之 event 聚合】event count累记聚合(采用 Patch),Message 聚合形成聚合 event(采用Create)

参考 15.深入k8s:Event事件处理及其源码分析 - luozhiyun - 博客园event 模块总览 EventRecorder:是事件生成者,k8s组件通过调用它的方法来生成事件;EventBroadcaster:事件广播器,负责消费EventRecorder产生的事件,然后分发给broadcasterWatcher;broadcasterWatcher:用…

AURIX TC3xx学习笔记2 GTM模块

文章目录 引言功能改进一些缩写 功能细节GTM Clock and Time Base Management (CTBM)Clock Management Unit (CMU)External Generation Unit (EGU)Configurable Clock Generation sub-unit (CFGU)Fixed Clock Generation (FXU) Time Base Unit (TBU) Cluster Configuration Mod…

在CentOS上无Parallel时并发上传.wav文件的Shell脚本解决方案

在CentOS上无Parallel时并发上传.wav文件的Shell脚本解决方案 背景概述解决方案脚本实现脚本说明使用指南注意事项在CentOS操作系统环境中,若需并发上传特定目录下的.wav文件至HTTP服务器,而系统未安装GNU parallel工具,我们可通过其他方法实现此需求。本文将介绍一种利用Sh…

QT通过在线安装器安装【详细】

在线安装器地址&#xff1a; 官方在线安装器&#xff1a;Index of /official_releases/online_installers (qt.io) 通过命令行启动安装页面 直接双击qt安装程序&#xff0c;在线安装会非常慢&#xff0c;甚至安装失败&#xff0c;所以通过命令行页面启动安装页面。点击wind…

保姆级教学 uniapp绘制二维码海报并保存至相册,真机正常展示图片二维码

一、获取二维码 uni.request({url: https://api.weixin.qq.com/wxa/getwxacode?access_token${getStorage("token")},responseType: "arraybuffer",method: "POST",data: {path: "/pages/index/index"},success(res) {// 转换为 Uint…

Unity类银河战士恶魔城学习总结(P166 Ailments FX 异常状态伤害粒子特效)

【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili 教程源地址&#xff1a;https://www.udemy.com/course/2d-rpg-alexdev/ 本章节创建了三种粒子特效&#xff0c;火焰&#xff0c;寒冰&#xff0c;雷电 主场景创建/特效/粒子 初始的例子特效 火焰 寒冰 雷电 En…

Java基于SpringBoot的网上订餐系统,附源码

博主介绍&#xff1a;✌Java老徐、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&…

[笔记] Windows 上 Git 安装详细教程:从零开始,附带每个选项解析

Git 是目前最流行的分布式版本控制系统之一&#xff0c;广泛应用于软件开发和项目管理中。对于 Windows 用户来说&#xff0c;正确安装和配置 Git 是开始使用 Git 的第一步。本文提供一份详细的指南&#xff0c;帮助你在 Windows 系统上顺利安装 Git&#xff0c;并解释每个安装…

JavaScript编写css自定义属性

一、自定义属性 是在 CSS 中定义的变量&#xff0c;以 --开头。它们可以存储颜色、尺寸、字体等任何 CSS 值&#xff0c;并且可以在整个文档中重复使用。 :root {--primary-color: #3498db;--font-size: 16px; }body {color: var(--primary-color);font-size: var(--font-siz…

项目开发之Jenkins

文章目录 思考基础概述JenkinsMavenGit集成开发部署GitLab服务安装 实战1 新建任务需要的配置pipeline最后 思考 jenkis怎么连接github仓库&#xff1f; jenkis的作用是什么&#xff1f;基础 概述 定义&#xff1a;Jenkins是一款开源的持续集成(Continuous Integration&…

core Webapi jwt 认证

core cookie 验证 Web API Jwt 》》》》用户信息 namespace WebAPI001.Coms {public class Account{public string UserName { get; set; }public string UserPassword { get; set; }public string UserRole { get; set; }} }》》》获取jwt类 using Microsoft.AspNetCore.Mvc…

TCP/IP协议详解(小白)

TCP/IP协议详解 TCP/IP协议包含了一系列的协议&#xff0c;也叫TCP/IP协议族&#xff08;TCP/IP Protocol Suite&#xff0c;或TCP/IP Protocols&#xff09;&#xff0c;简称TCP/IP。TCP/IP协议族提供了点对点的连结机制&#xff0c;并且将传输数据帧的封装、寻址、传输、路由…

Java项目实战II基于微信小程序的旅游社交平台(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、核心代码 五、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。 一、前言 随着移动互联网的迅猛发展&#xff0c;旅游已经成为人…

jmeter配置

单接口运行没问题&#xff0c;但是批量执行100个线程数发现总是提示请求不合法 最后发现 需要将配置改成 正好回归一下这个配置&#xff1a; Ramp-Up时间&#xff08;秒&#xff09;的定义&#xff1a; Ramp-Up时间是指在JMeter测试中&#xff0c;所有指定的线程&#xff08…