Linux线程篇(中)

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

线程的优缺点

 线程的缺点

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

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

我们写代码验证一下:

#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 4pthread_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.mzph.cn/news/56258.shtml

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

相关文章

【官方中文文档】Mybatis-Spring #目录

目录 此页面用于在GitHub上呈现索引。 NOTE: 由于链接目标是在使用maven-site-plugin转换为html的假设下指定的&#xff0c;因此在GitHub上的呈现中有一个锚点已损坏。 简介入门SqlSessionFactoryBean事务使用 SqlSession注入映射器Spring Boot使用 MyBatis APISpring Batch示…

Ubuntu22.04安装中文输入法►由踩坑到上岸版◄

Ubuntu22.04安装中文输入法►由踩坑到上岸版◄ 了解入坑上岸 更新一发&#xff1a;Gedit中文乱码问题的解决 为了方便回忆和记录甚至后面继续重装系统&#xff0c;我还是写一下以便将来用到或参考&#xff5e; 了解 安装Ubuntu22.04&#xff08;截至2023年08月26日11&#xff…

基于JavaFX的贪吃蛇小游戏

游戏背景介绍 贪吃蛇游戏是一款经典的小游戏&#xff0c;它的玩法很简单&#xff0c;就是控制蛇吃食物&#xff0c;每吃一个食物蛇的长度就会加一&#xff0c;直到蛇撞到墙壁或者撞到自己时游戏结束&#xff0c;最终的得分是蛇的长度减一。 JavaFX 用Java开发桌面端首选就是J…

shell脚本——循环语句、sed、函数、数组、免交互expect

目录 循环语句 for while 与 until sed 基本用法 sed脚本格式 函数 注意事项 定义函数和调用函数 脚本中函数的位置 查看函数 删除函数 函数返回值 函数的传参操作 使用函数文件 递归函数 数组 声明数组 数组切片 免交互expect 定义 基本命令 循环语句 …

python3/pip3 SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed

环境&#xff1a; mac os 背景&#xff1a; 电脑之前安装的是python3.9 &#xff0c; 现在升级到python3.10。 从python官网下载macos版本的python3.10 pkg。 双击安装。 程序使用aiohttp访问ebay 。 出错&#xff1a; aiohttp.client_exceptions.ClientConnectorCertifi…

MySql015——使用子查询

一、创建customers表 ######################## # Create customers table ######################## use study;CREATE TABLE customers (cust_id int NOT NULL AUTO_INCREMENT,cust_name char(50) NOT NULL ,cust_address char(50) NULL ,cust_city char…

QuickLook概述和使用以及常用插件

1、QuickLook概述 QuickLook&#xff1a; 是可以快速预览的工具&#xff0c;开源、免费。通过空格键即可快速查看文件内容。 文件无需打开就可以用QuickLook一键快速预览。 说明文档&#xff1a;https://en.wikipedia.org/wiki/Quick_Look github地址&#xff1a;https://git…

基础shell小技巧002

1. 使用if-then语句 如果之前用过其他编程语言的if-then语句&#xff0c;那么这种形式可能会让你有点儿困惑。在其他编程语言中&#xff0c;if语句之后的对象是一个等式&#xff0c;其求值结果为TRUE或FALSE。但bash shell的if语句并非如此。 重点&#xff1a;bash shell的if…

hive 动态分区-动态分区数量太多也会导致效率下降只设置非严格模式也能执行动态分区

hive 动态分区-动态分区数量太多也会导致效率下降&只设置非严格模式也能执行动态分区 结论 在非严格模式下不开启动态分区的功能的参数&#xff08;配置如下&#xff09;&#xff0c;同样也能进行动态分区数据写入&#xff0c;目测原因是不严格检查SQL中是否指定分区或者…

如何让qt tableView每个item中个别字用不同颜色显示?

如何让qt tableView每个item中个别字用不同颜色显示&#xff1f; 从上面图片可以看到&#xff0c;Item为红色&#xff0c;数字5为黑色。 要实现在一个控件实现不同颜色&#xff0c;目前想到的只有QTextEdit 、QLabel。有两种方法&#xff0c;第一种是代理&#xff0c;第二种是…

yolov5添加SimAM注意力机制(yolov7同理)

SimAM注意力机制简介 关于SIMAM注意力机制的原理这里不再详细解释,这篇发在Proceeddings of the 38th Internation Conference on Machine Learning.论文参考如下论文链接here   yolov5中添加SimAM注意力机制 注意力机制分为接收通道数和不接受通道数两种。这次属于不接受通…

数据库相关知识2

数据库知识2 关系完整性 数据完整性 指的是数据库中的数据的准确性和可靠性 实体完整性约束&#xff1a; 目的&#xff1a; 在表中至少有一个唯一的 标识&#xff0c;主属性字段中&#xff0c;不为空&#xff0c;不重复 主键约束&#xff1a;唯一 不重复 不为空 primary k…

c语言实现堆

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、树1、树的概念2、树的相关概念3、树的表示 二、二叉树1、二叉树概念2、特殊的二叉树3、二叉树的性质4、二叉树的顺序结构5、二叉树的链式结构 三、堆(二叉树…

C# textBox1.Text=““与textBox1.Clear()的区别

一、区别 textbox.Text "" 和 textbox.Clear() 都可以用于清空文本框的内容&#xff0c;但它们之间有一些细微的区别。 textbox.Text "": 这种方式会将文本框的 Text 属性直接设置为空字符串。这样会立即清除文本框的内容&#xff0c;并将文本框显示为空…

【leetcode 力扣刷题】双指针///原地扩充线性表

双指针///原地扩充线性表 剑指 Offer 05. 替换空格定义一个新字符串扩充字符串&#xff0c;原地替换思考 剑指 Offer 05. 替换空格 题目链接&#xff1a;剑指 Offer 05. 替换空格 题目内容&#xff1a; 这是一道简单题&#xff0c;理解题意&#xff0c;就是将字符串s中的空格…

unity 提取 字符串中 数字 修改后返回 字符串

参考博主&#xff1a;unity 提取字符串数字修改后返回字符串_unity string提取数字_lvcoc的博客-CSDN博客 正数和浮点数的 正则表达式 //正则表达式//const string pattern "\d";//表达1位或多位的整数数字 const string pattern "\d\.\d";//表达1位或…

拼多多开放平台的API接口可以获取拼多多电商数据。以下是API接口流程

使用拼多多开放平台的API接口可以获取拼多多电商数据。以下是一般的API接口流程&#xff1a; 1. 注册开发者账号&#xff1a;首先&#xff0c;您需要在拼多多开放平台注册一个开发者账号。通过开发者账号&#xff0c;您可以获得API密钥和其他必要的信息。 2. 鉴权与认证&…

数据结构1

数据结构是计算机科学中存储和组织数据的一种方式&#xff0c;它定义了数据的表示方式和对数据进行操作的方法&#xff0c;常见的数据结构包括数组、栈、链表、队列、树、图等。 目录 一、常见的数据结构 1.数组 2.栈 3.队列 4.链表 5.树 6.图 一、常见的数据结构 1.数…

自动设置服务器全教程

亲爱的爬虫探险家&#xff01;在网络爬虫的世界里&#xff0c;自动设置代理服务器是一个非常有用的技巧。今天&#xff0c;作为一家代理服务器供应商&#xff0c;我将为你呈上一份轻松实用的教程&#xff0c;帮助你轻松搞定爬虫自动设置代理服务器。 一、为什么需要自动设置代…

【MongoDB】Springboot中MongoDB简单使用

1. docker安装MongoDB 拉取镜像 docker pull mongo创建容器 docker run -di --name mongo-service --restartalways -p 27017:27017 -v ~/data/mongodata:/data mongo2. 导入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactI…