【1++的Linux】之线程(二)

news2024/11/25 7:04:00

👍作者主页:进击的1++
🤩 专栏链接:【1++的Linux】

文章目录

  • 一,对上一篇内容的补充
  • 二,Linux线程互斥
    • 1. 互斥的引出
    • 2. 互斥量
    • 3. 剖析锁的原理

一,对上一篇内容的补充

线程创建: pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID

**线程终止:**需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

线程等待: 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间。(线程等待的原因)

int pthread_join(pthread_t thread, void **value_ptr);

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数
    PTHREAD_ CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参
    数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

线程分离: 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

二,Linux线程互斥

1. 互斥的引出

我们再来回顾一下我们以前曾提到过的临界资源,临界区和原子性。

临界资源:被多个执行流所共享的资源叫做临界资源。
临界区:每个线程内部执行访问临界资源的代码叫做临界区。
原子性:在别人看来只有两种状态,做一件事情,要么没做,要么做完。

首先我们来回答为什么要有线程互斥。

我们来看一段代码:

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

int tickets=1000;
void* get_tickets(void* argv)
{
    while(true)
    {
        if(tickets>0)
        {
            cout<<"我是:"<<pthread_self()<<" "<<"我抢的第"<<tickets<<"票"<<endl;
            tickets--;
        }
        else
        {
            break;
        }
    }

    cout<<"票完了"<<endl;
    return nullptr;
}

int main()
{
    pthread_t tid[3];
    for(int i=0;i<3;i++)
    {
        pthread_create(tid+i,nullptr,get_tickets,(void*)"thread");//创建多个线程
    }

    //等待线程
    for(int i=0;i<3;i++)
    {
        pthread_join(tid[i],nullptr);
    }
    return 0;
}

在这里插入图片描述
我们看到,最终的结果竟然有两个线程抢到了同一个编号的票,这样岂不是一个作为我们卖出去了两张甚至更多的票。

这是为什么呢?
我们用下面这张剖析图来理解:
在这里插入图片描述

有如下场景:线程A先执行,票数减减的步骤在我们看到就只有一行,实际转换为汇编代码后是有三条语句,图中我已经写出,当A执行完前两步,要将数据写入内存中去时,因为时间片等某种原因,其被切换了下来,换进程B去执行,此时在内存中票的数仍然为1000,所以B拿到的仍然是编号为1000的票,因此就发生了上述结果。
因此就要有互斥的存在了!!!

多线程是共享地址空间的,所以有很多资源都是共享的。
这种方式带来的优势:方便了线程间的通信
缺点:并发访问一些共享的数据时,回由于时序问题而导致数据不一致的问题。

那么什么是互斥呢?
我们先来看其概念:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
以上述代码为例,那么我们的临界资源就是票的数量,临界区就为抢票过程的一段代码

在这里插入图片描述
互斥就是对临界区的保护的一种方式,其本质就是保护临界资源

2. 互斥量

要解决上述数据不一致的问题,需要做到三点:

  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
    要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量

锁的初始化:
在这里插入图片描述

锁的初始化有两种,一种是用函数做初始化,将你的锁的地址传入进去,属性可以设置为nullptr,另一种是对于全局的锁或者是static修饰的锁,可以直接用宏PTHREAD_MUTEX_INITIALIZER 进行初始化。

锁的销毁:

pthread_mutex_destroy是销毁锁。
销毁互斥量需要注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁

加锁和解锁:
在这里插入图片描述

int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);//加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁
返回值:成功返回0,失败返回错误号。
调用 pthread_ lock 时:

  1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
  2. 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
    trylock函数,:
    这个函数是非阻塞调用模式, 也就是说, 如果互斥量没被锁住, trylock函数将把互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了, trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态。

有了锁之后我们对抢票系统做出改进:

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

pthread_mutex_t mt=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;//创建锁并初始化
int tickets=1000;
void* get_tickets(void* argv)
{
    (void*)argv;
    while(true)
    {
        pthread_mutex_lock(&mt);//加锁
        if(tickets>0)
        {
            cout<<"我是:"<<pthread_self()<<" "<<"我抢的第"<<tickets<<"票"<<endl;
            tickets--;
            pthread_mutex_unlock(&mt);
        }
        else
        {
             pthread_mutex_unlock(&mt);
            break;

        }
    }

    cout<<"票完了"<<endl;
    return nullptr;
}

int main()
{
    pthread_t tid[3];
    for(int i=0;i<3;i++)
    {
        pthread_create(tid+i,nullptr,get_tickets,(void*)"thread");//创建多个线程
    }

    //等待线程
    for(int i=0;i<3;i++)
    {
        pthread_join(tid[i],nullptr);
    }

    return 0;
}

在这里插入图片描述
此时我们可以看到运行结果就达到了我们所期望的。

下面我们换一种锁的初始化方式来进行验证:

#define TH_NUM 5

class Get_tickets
{
    public:
    Get_tickets(string& name,pthread_mutex_t* mut)
        :_name(name)
        ,_mut(mut)
        {}
    
    public:
    string _name;
    pthread_mutex_t* _mut;

};

void* get_tickets(void* argv)
{
    Get_tickets* Lock=(Get_tickets*)argv;
    while(true)
    {
        pthread_mutex_lock(Lock->_mut);
        if(tickets>0)
        {
            cout<<"I am "<<Lock->_name<<": "<<tickets<<endl;
            tickets--;
            pthread_mutex_unlock(Lock->_mut);
            usleep(100000);
        }
        else
        {
            pthread_mutex_unlock(Lock->_mut);
            break;
        }
    }

    cout<<"票完了"<<endl;
}
int main()
{
    pthread_t tid[TH_NUM];
    pthread_mutex_t mut;
    pthread_mutex_init(&mut,nullptr);
    string name="thread";
    for(int i=0;i<TH_NUM;i++)
    { 
        Get_tickets* pLock=new Get_tickets(name,&mut);
        pLock->_name+=to_string(i+1);
        pthread_create(tid+i,nullptr,get_tickets,(void*)pLock);
    }

    for(int i=0;i<TH_NUM;i++)
    {
        pthread_join(tid[i],nullptr);
    }

    return 0;
}

在这里插入图片描述
我们发现结果符合我们的预期。

3. 剖析锁的原理

由于我们对临界区加了锁,因此多个执行流在访问临界值的时候都是串行的,也就是说每次只让一个执行流区访问临界资源直到出了临界区,也就是解锁后才回再让这些执行流去竞争进入临界区。(对于临界区,我们的临界区要尽量短小精悍,因为锁是回影响执行效率的,这违背了我们创建线程的初衷,因此非必要不适用锁) 我们的临界资源加了锁后我们就可以说它是原子的。我们在访问临界资源时,先访问的是锁,先会去判断是否已经加锁了,并且会有多个执行流看到它,那么锁是不是临界资源呢?或者说是互斥锁是不是原子的呢?锁自己都保护不好自己怎么去保护别人,那么该如何去保证锁的安全呢?

我们先抛出答案,锁是具有原子性的。

我们来看看关于加锁和解锁的两段伪代码:
在这里插入图片描述
我们再次以一张图来理解这段伪代码:
在这里插入图片描述
我们有如下场景:
A线程正在进行加锁的过程,我们可以把申请的这把锁中的内容 “1” 看作一个令牌(一个锁只有一个)先是将0写入特定的寄存器当中,接着将锁的内容和寄存器中的0进行交换(这一步在汇编中只有一行代码,因此这一操作也是原子的) 若,这是,A线程被切换掉,B线程执行,此时会在从第一步开始执行,将0写入,然后交换,但这是交换到寄存器中的值是A进行交换时交换过去的0,因此在判断是,其会被挂起等待,此时A线程被换上去继续执行,恢复其上下文数据后,(这段数据中,也会记录A上次执行到了那一步),此时寄存器中的值就为恢复上来的1,进行判断后,加锁成功。
这就好比上面所提到的只有一个令牌,只要执行完交换语句后,A就拿到了这个令牌,成为它上下文中的一部分,哪怕被切下去,也没有关系,因为,寄存器只有一份,但寄存器中的数据可以有很多分。寄存器中的内容,是每一个执行流私有的。
此时B虽然被调度执行,但令牌已经没了,所以B只能等待。

对于解锁,就是将令牌归还与锁,这一动作也是有原子的。归还后,等待的线程会再次重复上述争令牌的过程。

交换的现象:内存<---->寄存器
交换的本质:原本锁中的数据:共享---->私有

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

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

相关文章

人工智能AI 全栈体系(十二)

第二章 计算机是如何学会下棋的 下棋一直被认为是人类的高智商游戏&#xff0c;从人工智能诞生的那一天开始&#xff0c;研究者就开始研究计算机如何下棋。著名人工智能学者、图灵奖获得者约翰麦卡锡在 50 年代就开始从事计算机下棋方面的研究工作&#xff0c;并提出了著名的 …

关键字驱动自动化测试框架搭建详解

前言 那么这篇文章我们将了解关键字驱动测试又是如何驱动自动化测试完成整个测试过程的。关键字驱动框架是一种功能自动化测试框架&#xff0c;它也被称为表格驱动测试或者基于动作字的测试。关键字驱动的框架的基本工作是将测试用例分成四个不同的部分。首先是测试步骤&#…

用HTML + javaScript快速完成excel表格信息除重并合并

今天突然接到一个工作&#xff0c;要把两个存储在.xls的主体信息表&#xff0c;除重后合并成一个主体信息表&#xff0c;并且补充主体类型和所在县区这两列信息。 完成这项工作的方法有很多&#xff0c;如果信息表中的信息量不大的话&#xff0c;手工处理一下也行&#xff0c;如…

MYSQL运维篇(已完结)

一、日志 1. 错误日志 2. 二进制日志 &#x1f60e; 介绍 &#x1f60e; 日志格式 &#x1f60e; 日志查看 &#x1f60e; 日志删除 3. 查询日志 4. 慢查询日志 二、主从复制 1. 概述 2. 原理 3. 搭建 4. 总结 三、分库分表 1. 介绍 &#x1f364; 问题分析 &#x1f364;…

WPF布局与控件分类

Refer&#xff1a;WPF从假入门到真的入门 - 知乎 (zhihu.com) Refer&#xff1a;WPF从假入门到真的入门 - 知乎 (zhihu.com) https://www.zhihu.com/column/c_1397867519101755392 https://blog.csdn.net/qq_44034384/article/details/106154954 https://www.cnblogs.com/mq0…

报错“this.bookDao“ is null

这是我的报错&#xff1a; 原因是我的BookServiceImpl方法中的对象没有装配&#xff1a; 添加上自动装配注释即可实现自动装配&#xff1a;

Python---字符串的修改方法---replace()替换

修改字符串&#xff0c;指的就是通过函数&#xff08;方法&#xff09;的形式修改字符串中的数据。 编号函数作用1replace()返回替换后的字符串2split()返回切割后的列表序列3capitalize()首字母大写4title()所有单词首字母大写5upper()与lower()返回全部大写或小写的字符串6l…

文件夹批量改名:轻松实现文件夹随机重命名

无论是在我们的日常生活还是工作中&#xff0c;批量重命名文件夹是一项非常常见的任务。当我们需要整理或分类大量的文件时&#xff0c;往往需要对相应的文件夹进行重命名。然而&#xff0c;手动一个接一个地完成这个任务不仅会消耗大量的时间&#xff0c;还容易在重命名过程中…

Apache Doris (五十二): Doris Join类型 - Broadcast Join

🏡 个人主页:IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 🚩 私聊博主:加入大数据技术讨论群聊,获取更多大数据资料。 🔔 博主个人B栈地址:豹哥教你大数据的个人空间-豹哥教你大数据个人主页-哔哩哔哩视频 目录 1. Broadcast Join原理

AD7792/AD7793 备忘

AD7792/AD7793 是一款 ∑-Δ ADC&#xff0c;3 通道、低噪声&#xff0c;内部集成仪表放大器和参考源。AD7792 为 16 位&#xff0c;AD7793为 24 位。 供电电压&#xff1a;2.7 ~ 5.25 V&#xff0c;并不支持负电压。转换速率&#xff1a;4.17 Hz ~ 470 Hz内置参考基准&#x…

图片怎么转换成pdf?

图片怎么转换成pdf&#xff1f;图片可以转换成PDF格式文档吗&#xff1f;当然是可以的呀&#xff0c;当图片转换成PDF文件类型时&#xff0c;我们就会发现图片更加方便的打开分享和传播&#xff0c;而且还可以更加安全的保证我们的图片所有性。我们知道PDF文档是可以加密的&…

mac系统快速切换不同版本JDK

1.安装所需jdk版本 下载地址&#xff1a;http://www.codebaoku.com/jdk/jdk-index.html 本示例安装了jdk8和jdk19两个版本 2.查看对应安装路径 安装好后&#xff0c;通过终端输入以下命令查看相关路径&#xff08;后续需在.bash_profile中配置&#xff09; /usr/libexec/ja…

技术分享 | 抓包分析 TCP 协议

TCP 协议是在传输层中&#xff0c;一种面向连接的、可靠的、基于字节流的传输层通信协议。 环境准备 对接口测试工具进行分类&#xff0c;可以如下几类&#xff1a; 网络嗅探工具&#xff1a;tcpdump&#xff0c;wireshark代理工具&#xff1a;fiddler&#xff0c;charles&a…

OmniPlan Pro 4:一站式项目流程管理神器

&#x1f916; OmniPlan Pro 4 for Mac 是一款强大的项目管理软件&#xff0c;具备许多功能。以下是它的一些主要功能介绍&#xff1a; &#x1f5d3;️ 强大的项目计划&#xff1a; OmniPlan Pro 4 可以帮助您创建详细的项目计划。您可以创建任务、设置任务之间的依赖关系、分…

软件测试/测试开发丨Python安装指南(Windows版)

点此获取更多相关资料 下载 Python 解释器 下载地址: https://www.Python.org/downloads/ 通过下载页面&#xff0c;可以在该页面上看到下载链接。 在下载列表中以“(64-bit)”结尾的链接是 64 位的 Python 安装程序&#xff0c;以“(32-bit)”开头的链接是 32 位的 Python 安…

Chromebook文件夹应用新功能

种种迹象表明 Google 旗下的 Chromebooks 近期要有大动作了。根据 Google 团队成员透露&#xff0c;公司计划在 Chrome OS 的资源管理器中新增“Recents”&#xff08;最近使用&#xff09;文件&#xff0c;以便于用户更快找到所需要的文件。 种种迹象表明 Google 旗下的 Chro…

一方的系统架构师认证考试之路

为什么参加 因为人民币。听说过医生、律师等职业有评职称的考试&#xff0c;程序员也能评职称&#xff1f;评个职称有啥用&#xff1f;我等体系外的一线搬砖仔&#xff0c;考个软考高级证有啥用&#xff1f;答&#xff1a;拿到证原地加薪 500/月&#xff0c;有图为证&#xff…

三:ffmpeg命令帮助文档

目录 一&#xff1a;帮助文档的命令格式 二&#xff1a;将帮助文档输出到文件 一&#xff1a;帮助文档的命令格式 ffmpeg -h帮助的基本信息ffmpeg -h long帮助的高级信息ffmpeg -h full帮助的全部信息 ffmpeg的命令使用方式&#xff1a;ffmpeg [options] [[infile options] …

作用域,基本数据类型(常量const),转义字符,运算符

1.作用域 全局作用域&#xff1a;定义在所有花括号外的名字具有“全局作用域” 块作用域&#xff1a;在某个花括号内定义的名字具有“块作用域” 一般把具有全局作用域的变量叫做“全局变量”&#xff0c;具有块作用域的变量叫做“局部变量” 如果在嵌套作用域里出现重名&a…

如何优化服务器负载均衡策略?一文讲解

在现代大规模、高流量的网络使用场景中&#xff0c;对于企业来说&#xff0c;仅凭单机提供业务已不能给用户带来最佳体验&#xff0c;应用的可靠性和速度也会受到影响。为了应对高并发和海量数据的挑战&#xff0c;必须提升系统性能&#xff0c;服务器负载均衡技术应运而生。那…