Linux线程篇(中)

news2024/11/23 8:56:23

       有了之前对线程的初步了解我们学习了什么是线程,线程的原理及其控制。这篇文章将继续讲解关于线程的内容以及重要的知识点。

线程的优缺点

 线程的缺点

在这里我们来谈一谈线程健壮性

首先我们先思考一个问题,如果一个线程出现了问题,那么它会影响其他线程吗?

我们写代码验证一下:

#include <iostream>
#include <string>
#include <unistd.h>
using namespace std;


void* start_route(void* args)
{
    string name =static_cast<char*>(args);
    while(true)
    {
        cout<<"我是一个新线程,我的名字是:"<<name<<endl;
        sleep(1);

    }

}


int main()
{

    pthread_t id;
    pthread_create(&id,nullptr,start_route,(void*)"thread one");
    while(true)
    {
        cout<<"我是主线程!!!!"<<endl;
        sleep(1);

    }

    return 0;



}

代码运行的结果如下:

 我们用ps命令进行查看:

pid和lwp的值相同的是主线程 ,不一样的是创建出来的新线程。我们继续修改代码,使其中的一个线程崩溃,看看会不会影响另一个进程:

 结果如下:

当我们再用ps命令进行查找时发现两个线程不复存在,原因是:当一个线程对野指针进行访问时,操作系统会发送信号终止进程而两个线程的pid相同,同属于一个进程,因此两个线程同时崩溃!

 之前我们说过操作系统里面没有真正线程的概念,它只提供轻量级进程的使用接口,而创建进程或者线程底层调用的是clone

这里我们了解一下底层使用的接口就行,我们还是使用平常用的fork和pthread_create创建。

如何看待线程库?(语言版)

#include <iostream>
#include <string>
#include <unistd.h>
#include <thread>

void* thread_run()
{
    while(true)
    {
        std::cout<<"我是一个新线程!!"<<std::endl;
    }


}

int main()
{
    std::thread t1(thread_run);
    while(true)
    {
        std::cout<<"我是主线程!!!"<<std::endl;
    }
    t1.join();
}

当我们用c++11中的线程时,不引入线程库时,程序运行报错:

错误显示有未被定义的“pthread_create",由此可以看出在linux环境下,c++语言中的线程本质是对linux底下线程库的进一步封装

上面的代码也能在windows底下运行,因为语言帮我们解决了平台差异性问题,实现了跨平台!!而原生线程库的接口都是不可跨平台的,暴露和使用原生线程库都是自己决定的!

全局变量的安全性

现在我们写一个抢票的代码,抢票逻辑没有任何问题。但如果多个线程并发的执行就会出现bug:

#include <iostream>
#include <string>
#include <unistd.h>
#include <memory>
#include "Thread.hpp"

int ticket = 10000;
void *getTicket(void *args)
{
    string username = static_cast<const char *>(args);
    while (true)
    {
        if (ticket > 0)
        {
            usleep(1234);
            std::cout << username << "正在抢票:" << ticket << std::endl;
            --ticket;
        }
        else{
            break;
        }
    }
}

int main()
{
    std::unique_ptr<Thread> thread1(new Thread(getTicket, (void *)"user1", 1));
    std::unique_ptr<Thread> thread2(new Thread(getTicket, (void *)"user2", 2));
    std::unique_ptr<Thread> thread3(new Thread(getTicket, (void *)"user3", 3));
    std::unique_ptr<Thread> thread4(new Thread(getTicket, (void *)"user4", 4));

    thread1->start();
    thread2->start();
    thread3->start();
    thread4->start();

    thread1->join();
    thread2->join();
    thread3->join();
    thread4->join();
    return 0;
}

运行结果:

这时我们发现票数竟然变成了负数,原因就是当我们进行usleep操作时线程被不停的切换。例如线程a刚进入判断语句相对票数进行减减时,线程a被切换,保存在寄存器的上下文也相应地被切走。这时线程b又来了,它和线程a就一起进入了判断语句对票数进行删减操作。当b进行了20次循环操作(假设)票数减了20次,但这时线程a又被切换回来时,cpu先读取线程a中的上下文,在进行减减操作,最后再将结果写回到内存中。这样线程b再回来的时候结果就变得翻天覆地!!

发生以上问题的原因主要是++、--操作不是原子性的,在汇编语句上至少是三条语句:

在这里我先补充一些概念:

临界资源:多个执行流进行安全访问的共享资源

临界区:多个执行流中,访问临界资源的代码。--往往是代码的很小一部分

互斥:让多个执行流串行访问共享资源。

原子性:对一个资源进行访问时,要么不做,要么就一次性做完。换句话来说执行的语句用一条汇编就能完成。

解决以上问题的手段:加锁!!!!!

锁的常用接口:

锁的初始化:

 当你定义一个锁时,如果是全局锁就可以用以下方式定义:‘

 初始化以后就不需要对锁进行以下接口的调用:

 但如果是一个局部的锁,就需要对锁进行以上的初始化和销毁的操作。下面定义的一个全局锁:

lock和unlock之前的代码区域就是临界区,临界区中访问的ticket就是临界资源,访问它们的方式都是安全的!!

现在我们使用一个局部的锁(全局的锁太简单):

class ThreadData
{
public:
    ThreadData(const string&threadname,pthread_mutex_t* pmutex =nullptr)
    :_threadname(threadname)
    ,_pmutex(pmutex)
    {

    }

    ~ThreadData(){}

public:
    string _threadname;
    pthread_mutex_t* _pmutex;

};

int ticket = 10000;
void *getTicket(void *args)
{
    ThreadData* td =static_cast<ThreadData*>(args);
    while (true)
    {
        pthread_mutex_lock(td->_pmutex);
        if (ticket > 0)
        {
            usleep(1234);
            std::cout <<td->_threadname << "正在抢票:" << ticket << std::endl;
            --ticket;
            pthread_mutex_unlock(td->_pmutex);

        }
        else{
            pthread_mutex_unlock(td->_pmutex);
            break;
        }
        
    }
    return nullptr;
}

int main()
{
    #define NUM 4
    pthread_mutex_t lock;
    pthread_mutex_init(&lock,nullptr);
    vector<pthread_t> tids(NUM);
    for(int i=0;i<NUM;++i)
    {
        char buffer[64];
        snprintf(buffer,sizeof buffer,"thread %d",i+1);
        ThreadData* td =new ThreadData(buffer,&lock);
        pthread_create(&tids[i],nullptr,getTicket,td);
    }

    for(const auto& tid:tids)
    {
        pthread_join(tid,nullptr);
    }
    
    pthread_mutex_destroy(&lock);

 

    return 0;
}

当我们用了全局所以后发现几个现象:

1.抢票的速度变慢了!!!原因:就是加锁和解锁的过程是多个线程串行执行的。

2.抢票的时候基本上都是一个线程抢了好多票,其他线程没有机会抢票。原因锁只是规定互斥访问,锁是多个执行流竞争的结果。因为我们抢票的逻辑还不完整导致一个线程释放锁以后,这个线程再次进入循环申请锁。抢完票以后不应该立即再次进入循环,而是出现抢票结果的响应,但我们没有写,简单的用usleep(最后一行)代替一下:

现在我们来谈一谈对锁的认识

1.锁是用来保护共享资源时其变得安全,但多个执行流申请锁致使锁也是个共享资源。因此申请锁和释放锁也是原子性的。

2.使用锁来保护共享资源实际上是一个执行流串行运行的结果。因此为了提高效率和速度,用锁保护的代码区域的粒度越小越好

3.如果一个线程申请锁成功,即使线程被切换,它是抱着锁被切换走的!!因此当另一个线程来申请锁的时候就必须挂起等待。我们所学的锁也称为挂起等待锁。

4.谁持有锁谁就进入临界区!!!!!

锁的原子性实现原理:

在理解原理之前我们必须要有两个共识:

1.cpu只有一套寄存器被所有执行流共享。

2.cpu内寄存器的内容是每个执行流私有的,是执行流的上下文。

现在我为大家展示底层原理的代码实现:

 为了保证申请锁和释放锁的原子性,大多数体系结构都提供了swap或exchange指令,它们的作用就是将寄存器里的数据和内存里的数据进行交换,并且是一步到位。

保证申请锁为原子性的方式如下:

首先1代表有锁,0代表没有锁,先将0置于一个线程的寄存器中,使寄存器中的数值成为线程a的上下文:

 然后使用exchange指令将寄存器中的0和内存中的1进行交换,这样线程a就持有了锁:

由于交换数值在汇编上只有一条指令,因此保证了申请锁的原子性,那么锁被申请到了,线程a能被随意的切走吗??答案是:当然可以!!!!因为线程a被切走时,它的上下文也随之被切走,因此当别的进程来申请锁时就申请不到内存中的锁了(内存中数值为0表示没有锁),因此被挂起等待。

如果这是线程a又被切回来,它会带着它的1(它自己的上下文)回来。这时就保证了只有一个持有锁的进程能够访问临界资源!!!释放锁的原理和上面差不多这里就不多说了。这时有人会问:假如我让一个几个线程必须持有锁才能访问资源,让一个线程不需要锁进行访问,那不就不能保证只有一个线程访问了吗?? ---------------这里必须强调一下,加锁使程序员的工作,要访问就必须让所有线程持有锁访问,如果搞特殊的话这是你程序员自己代码上的失误喔!!!

锁的封装设计:(RAII)

首先我们来介绍一下什么是RAII:

 以下是代码的实现:

class Mutex
{
public:
    Mutex(pthread_mutex_t* pmutex =nullptr)
    :_pmutex(pmutex)
    {

    }
    void lock()
    {
        if(_pmutex) pthread_mutex_lock(_pmutex);

    }

    void unlock()
    {
        if(_pmutex) pthread_mutex_unlock(_pmutex);

    }

    ~Mutex()
    {

    }

    pthread_mutex_t* _pmutex;
};


class Guard_Mutex
{
public:
    Guard_Mutex(pthread_mutex_t* pmutex):_mutex(pmutex)
    {
        _mutex.lock();   //构造函数中进行加锁

    }

    ~Guard_Mutex()
    {
        _mutex.unlock(); //析构函数中进行解锁
    }

private:
Mutex _mutex;

};

可重入和线程安全:

重入的概念:同一个函数被不同的执行流调用。一个执行流还没有调用完,其他执行流就再次进入这个函数。

可重入的概念:一个函数在重入的前提结果不会出现任何的问题,则称为可重入函数。

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

 

 常见可重入的情况

 

前面讲了这么多相信大家听的云里雾里的,总结一句话就是:可重入是形容函数的,而线程安全表现的是整个程序运行的结果正不正确(例如有没有对全局变量的数据做保护、有没有对函数里的共享资源上锁等等)。线程安全可能调用函数,也可能不调用。因此可重入函数一定是线程安全的,而线程安全不一定是可重入的。

常见锁概念

死锁的概念:

多个线程在不释放自己锁资源的情况下,不断的请求对方的锁资源而导致永久等待的状态。

死锁产生的四个必要条件

1.互斥:这是锁的特性,每个资源每次只能被一个执行流使用。

2.请求与保持条件:对自己已经获得的资源不释放,并且不断请求对方的资源。

3.不剥夺:不强行获取对方的资源。

4.环路等待:若干执行流之间形成头尾相连的循环等待资源的关系。

破坏死锁的方法:

为了解决死锁问题,我们至少需要破坏死锁的必要条件中的一个。因为互斥是锁的基本特性,如果没有所那还谈什么死锁呢,所以互斥条件我们是没有办法解决的。因此我们根据后三条来解决:

不请求与保持:如果一个执行流申请锁失败时,可以先立即释放自己拥有的锁资源。

剥夺:提高某些执行流的优先级,我们当前执行流需要锁却申请不到,直接强行将锁给它。

破坏环路等待:如果有两把锁A、B,两个执行流依次以A、B的顺序申请和释放锁,而不是一个先申请A后申请B,一个先申请B再申请A。

到这里线程中篇就结束了,线程下篇持续更新,希望大家多多支持!

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

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

相关文章

先进API生产力工具eqable HTTP,一站式开发调试工具推荐

简介 Reqable是什么? Regable Fiddler/Charles Postman Reqable是HTTP一站式开发调试国产化解决方案&#xff0c;拥有更便捷的体验&#xff0c;更先进的协议&#xff0c;更高效的性能和更精致的界面。 Reqable是一款跨平台的专业HTTP开发和调试工具&#xff0c;在全平台支持…

JVM调优与参数设置

JVM调优 1、开始 JVM调优不是常规手段&#xff0c;性能问题一般第一选择是优化程序&#xff0c;最后的选择才是进行JVM调优。 JVM的自动内存管理本来就是为了将开发人员从内存管理的泥潭里拉出来。即使不得不进行JVM调优&#xff0c;也绝对不能拍脑门就去调整参数&#xff…

2023年高教社杯 国赛数学建模思路 - 复盘:校园消费行为分析

文章目录 0 赛题思路1 赛题背景2 分析目标3 数据说明4 数据预处理5 数据分析5.1 食堂就餐行为分析5.2 学生消费行为分析 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 赛题背景 校园一卡通是集…

Python 数据分析——matplotlib 快速绘图

matplotlib采用面向对象的技术来实现&#xff0c;因此组成图表的各个元素都是对象&#xff0c;在编写较大的应用程序时通过面向对象的方式使用matplotlib将更加有效。但是使用这种面向对象的调用接口进行绘图比较烦琐&#xff0c;因此matplotlib还提供了快速绘图的pyplot模块。…

iOS App签名与重签名:从开发者证书到重新安装运行

前文回顾&#xff1a; iOS脱壳技术&#xff08;二&#xff09;&#xff1a;深入探讨dumpdecrypted工具的高级使用方法 iOS逆向&#xff1a;越狱及相关概念的介绍 在本文中&#xff0c;我们将详细介绍iOS应用的签名过程&#xff0c;包括开发者证书的种类、证书与App ID、Provisi…

足球- EDA的历史数据分析并可视化

足球- EDA的历史数据分析并可视化 背景数据介绍探索数据时需要遵循的一些方向:数据处理导入库数据探索 数据可视化赛事分析主客场比分相关性分析时间序列分析 总结 背景 该数据集包括从1872年第一场正式比赛到2023年的44&#xff0c;341场国际足球比赛的结果。比赛范围从FIFA世…

GPIO输入-外电检测

前言 &#xff08;1&#xff09;本系列是基于STM32的项目笔记&#xff0c;内容涵盖了STM32各种外设的使用&#xff0c;由浅入深。 &#xff08;2&#xff09;小编使用的单片机是STM32F105RCT6&#xff0c;项目笔记基于小编的实际项目&#xff0c;但是博客中的内容适用于各种单片…

Property ‘sqlSessionFactory‘ or ‘sqlSessionTemplate‘ are required问题解决

运行程序的时候出现了如下的报错&#xff1a; 解决方法&#xff1a;去除EnableAutoConfiguration中的(exclude{DataSourceAutoConfiguration.class})

淘宝API技术解析,实现关键词搜索淘宝商品(商品详情接口等)

淘宝提供了开放平台接口&#xff08;API&#xff09;来实现按图搜索淘宝商品的功能。您可以通过以下步骤来实现&#xff1a; 获取开放平台的访问权限&#xff1a;首先&#xff0c;您需要在淘宝开放平台创建一个应用&#xff0c;获取访问淘宝API的权限。具体的申请步骤和要求可以…

行业追踪,2023-08-25

自动复盘 2023-08-25 凡所有相&#xff0c;皆是虚妄。若见诸相非相&#xff0c;即见如来。 k 线图是最好的老师&#xff0c;每天持续发布板块的rps排名&#xff0c;追踪板块&#xff0c;板块来开仓&#xff0c;板块去清仓&#xff0c;丢弃自以为是的想法&#xff0c;板块去留让…

Flink流批一体计算(18):PyFlink DataStream API之计算和Sink

目录 1. 在上节数据流上执行转换操作&#xff0c;或者使用 sink 将数据写入外部系统。 2. File Sink File Sink Format Types Row-encoded Formats Bulk-encoded Formats 桶分配 滚动策略 3. 如何输出结果 Print 集合数据到客户端&#xff0c;execute_and_collect…

Python爬虫分布式架构问题汇总

在使用Python爬虫分布式架构中可能出现以下的问题&#xff0c;我们针对这些问题&#xff0c;列出相应解决方案&#xff1a; 1、任务重复执行 在分布式环境下&#xff0c;多个爬虫节点同时从消息队列中获取任务&#xff0c;可能导致任务重复执行的问题。 解决方案&#xff1a;…

十三、pikachu之暴力破解

文章目录 1、暴力破解概述2、基于表单的暴力破解3、验证码的绕过3.1 验证码的认证流程3.2 验证码绕过&#xff08;on client&#xff09;3.3 验证码绕过&#xff08;on server&#xff09;3.4 token防爆破&#xff1f; 1、暴力破解概述 “暴力破解”是一攻击具手段&#xff0c;…

L1-035 情人节(Python实现) 测试点全过

题目 以上是朋友圈中一奇葩贴&#xff1a;“2月14情人节了&#xff0c;我决定造福大家。第2个赞和第14个赞的&#xff0c;我介绍你俩认识…………咱三吃饭…你俩请…”。现给出此贴下点赞的朋友名单&#xff0c;请你找出那两位要请客的倒霉蛋。 输入格式 输入按照点赞的先后顺…

Python数据分析 | 各种图表对比总结

本期将带领大家一起对在数据可视化的过程中常用的一些图表进行下总结&#xff1a; 条形图 【适用场景】 适用场合是二维数据集&#xff08;每个数据点包括两个值x和y&#xff09;&#xff0c;但只有一个维度需要比较&#xff0c;用于显示一段时间内的数据变化或显示各项之间的…

thinkphp6 入门(1)--安装、路由规则、多应用模式

一、安装thinkphp6 具体参考官方文档 安装 ThinkPHP6.0完全开发手册 看云 下面仅列举重要步骤 ThinkPHP6.0的环境要求如下&#xff1a; PHP > 7.2.5 1. 安装Composer 2. 安装稳定版thinkphp 如果你是第一次安装的话&#xff0c;在命令行下面&#xff0c;切换到你的WE…

“R语言+遥感“水环境综合评价方法

详情点击链接&#xff1a;"R语言遥感"水环境综合评价方法 一&#xff1a;R语言 1.1 R语言特点&#xff08;R语言&#xff09; 1.2 安装R&#xff08;R语言&#xff09; 1.3 安装RStudio&#xff08;R语言&#xff09; &#xff08;1&#xff09;下载地址 &…

语言模型(language model)

文章目录 引言1. 什么是语言模型2. 语言模型的主要用途2.1 言模型-语音识别2.2 语言模型-手写识别2.3 语言模型-输入法 3. 语言模型的分类4. N-gram语言模型4.1 N-gram语言模型-平滑方法4.2 ngram代码4.3 语言模型的评价指标4.4 两类语言模型的对比 5. 神经网络语言模型6. 语言…

开发一款AR导览导航小程序多少钱?ar地图微信小程序 ar导航 源码

随着科技的不断发展&#xff0c;增强现实&#xff08;AR&#xff09;技术在不同领域展现出了巨大的潜力。AR导览小程序作为其中的一种应用形式&#xff0c;为用户提供了全新的观赏和学习体验。然而&#xff0c;开发一款高质量的AR导览小程序需要投入大量的时间、人力和技术资源…

C语言 数字在升序数组中出现的次数

目录 1.题目描述 2.题目分析 2.1遍历数组方法 2.2二分查找方法 2.3代码示例 数字在升序数组中出现的次数 这道题可以用遍历数组和二分查找来处理 1.题目描述 2.题目分析 题目中有一个关键信息&#xff0c;非降序数组&#xff0c;我们可以使用if语句来处理这个问题 if(…