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。
到这里线程中篇就结束了,线程下篇持续更新,希望大家多多支持!
相关文章:

Linux线程篇(中)
有了之前对线程的初步了解我们学习了什么是线程,线程的原理及其控制。这篇文章将继续讲解关于线程的内容以及重要的知识点。 线程的优缺点: 线程的缺点 在这里我们来谈一谈线程健壮性: 首先我们先思考一个问题,如果一个线程出现…...

深度学习优化入门:Momentum、RMSProp 和 Adam
目录 深度学习优化入门:Momentum、RMSProp 和 Adam 病态曲率 1牛顿法 2 Momentum:动量 3Adam 深度学习优化入门:Momentum、RMSProp 和 Adam 本文,我们讨论一个困扰神经网络训练的问题,病态曲率。 虽然局部极小值和鞍点会阻碍…...
LeetCode 面试题 01.09. 字符串轮转
文章目录 一、题目二、C# 题解 一、题目 字符串轮转。给定两个字符串 s1 和 s2,请编写代码检查 s2 是否为 s1 旋转而成(比如,waterbottle 是 erbottlewat 旋转后的字符串)。 点击此处跳转题目。 示例1: 输入:s1 “wa…...

系统上线安全测评需要做哪些内容?
电力信息系统、航空航天、交通运输、银行金融、地图绘画、政府官网等系统再正式上线前需要做安全测试。避免造成数据泄露从而引起的各种严重问题。 那么系统上线前需要做哪些测试内容呢?下面由我给大家介绍 1、安全机制检测-应用安全 身份鉴别 登录控制模块 应提供…...

vue 中 axios 的安装及使用
vue 中 axios 的安装及使用 1. axios 安装2. axios使用 1. axios 安装 首先,打开当前的项目终端,输入 npm install axios --save-dev验证是否安装成功,检查项目根目录下的 package.json,其中的 devDependencies 里面会多出一个axios及其版本…...

数据结构——线性数据结构(数组,链表,栈,队列)
文章目录 1. 数组2. 链表2.1. 链表简介2.2. 链表分类2.2.1. 单链表2.2.2. 循环链表2.2.3. 双向链表2.2.4. 双向循环链表 2.3. 应用场景2.4. 数组 vs 链表 3. 栈3.1. 栈简介3.2. 栈的常见应用常见应用场景3.2.1. 实现浏览器的回退和前进功能3.2.2. 检查符号是否成对出现3.2.3. 反…...

多态(C++)
多态 一、初识多态概念“登场”1>. 多态的构成条件2>. 虚函数3>. 虚函数重写(覆盖)4>. 虚函数重写的两个例外1. 协变 一 基类和派生类虚函数返回值类型不同2. 析构函数重写(基类和派生类析构函数名不同) 小结 二、延伸…...

算法leetcode|73. 矩阵置零(rust重拳出击)
文章目录 73. 矩阵置零:样例 1:样例 2:提示:进阶: 分析:题解:rust:go:c:python:java: 73. 矩阵置零: 给定一个 m x n 的矩…...
axios 二次封装
axios 二次封装 基本上每一个项目开发,都必须要二次封装 axios。主要是为了减少重复性工作,不可能每一次发起新请求时,都要重新配置请求域名、请求头 Content-Type、Token 等信息。所以需要把公用的部分都封装成一个函数,每次调用…...
Rust安全之数值
文章目录 数值溢出 数值溢出 编译通过,运行失败 cargo run 1 fn main() {let mut arg std::env::args().skip(1).map(|x| x.parse::<i32>().unwrap()).next().unwrap();let m_i i32::MAX - 1;let a m_i arg;println!("{:?}", a); }thread main panicked…...
4种方法实现html 页面内锚点定位及跳转
使用scrollIntoView进行锚点定位效果 不知道你有没有遇到这样的需求:锚点定位?进入页面某个元素需要出现在可视区?…这一类的需求归根结底就是处理元素与可视区域的关系。我接触了很多前端小伙伴,实现的方式有各种各样的ÿ…...
gitlab配置备忘
版本 gitlab 14.6.2 gitlab备份上传到阿里云oss ### Backup Settings ###! Docs: https://docs.gitlab.com/omnibus/settings/backups.html# gitlab_rails[manage_backup_path] true # gitlab_rails[backup_path] "/var/opt/gitlab/backups"###! Docs: https://…...
基于Centos搭建k8s仓库
系统环境: Red Hat Enterprise Linux 9.1 (Plow) Kernel: Linux 5.14.0-162.6.1.el9_1.x86_64 主机名地址master192.168.19.128node01192.168.19.129node02192.168.19.130 目录 1、关闭防火墙,关闭SElinxu ,开启时间同步服务 2、关…...

浅谈泛在电力物联网发展形态与技术挑战
安科瑞 华楠 摘 要:泛在电力物联网是当前智能电网发展的一个方向。首先,总结了泛在电力物联网的主要作用和价值体现;其次,从智能电网各个环节概述了物联网技术在电力领域的已有研究和应用基础;进而,构思并…...
git reset --soft 用法
git reset --soft 是 Git 命令中的一个选项,它用于取消之前的提交,并将取消的更改保留在暂存区。这允许您重新组织提交历史或将更改合并到一个新的提交中,而不影响暂存区和工作目录中的更改。 这个命令的语法是: git reset --so…...

哪些测试仪器可以用于检测静电中和设备的性能
静电设备性能测试通常需要使用一些专门的仪器来进行。以下是一些常见的静电设备性能测试仪器: 1. 静电电压测试仪:用于测量物体表面的静电电压。它通常可以测量正负电压,并具有高精度和快速响应的特点。 2. 静电电荷仪:用于测量物…...

浅析 GlusterFS 与 JuiceFS 的架构异同
在进行分布式文件存储解决方案的选型时,GlusterFS 无疑是一个不可忽视的考虑对象。作为一款开源的软件定义分布式存储解决方案,GlusterFS 能够在单个集群中支持高达 PiB 级别的数据存储。自从首次发布以来,已经有超过十年的发展历程。目前&am…...

ARM开发,stm32mp157a-A7核PWM实验(驱动蜂鸣器,风扇,马达工作)
1.分析框图; 2.比较捕获寄存器(产生PWM方波); 工作原理: 1、系统提供一个时钟源209MHZ,需要通过分频器进行分频,设置分频器值为209分频; 2、当定时器启动之后,自动重载…...
群狼调研(长沙眼镜店神秘顾客)|消费者需求研究方案
本文由群狼调研(长沙品牌调研)出品,欢迎转载,请注明出处。消费者需求研究方案是在开展研究之前制定的计划,用于指导研究的设计、实施和分析。以下是一个可能的消费者需求研究方案的大致框架: 1. 研究目标和问题: • …...
电脑入门:宽带路由器常见故障排除技巧
宽带路由器在企业网络中的应用是相当广泛的,在运行的过程中出现故障是在所难免的,虽然故障现象多种多样,引起故障发生的原因也不尽相同,但从大体上可以把这些故障分为硬件故障和软件故障,具体来说就是一些网络连接性问题、配置文件选项问题以及网络协议问题等。 由于路由器…...

【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...

排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...

GO协程(Goroutine)问题总结
在使用Go语言来编写代码时,遇到的一些问题总结一下 [参考文档]:https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现: 今天在看到这个教程的时候,在自己的电…...

【LeetCode】算法详解#6 ---除自身以外数组的乘积
1.题目介绍 给定一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O…...

Neko虚拟浏览器远程协作方案:Docker+内网穿透技术部署实践
前言:本文将向开发者介绍一款创新性协作工具——Neko虚拟浏览器。在数字化协作场景中,跨地域的团队常需面对实时共享屏幕、协同编辑文档等需求。通过本指南,你将掌握在Ubuntu系统中使用容器化技术部署该工具的具体方案,并结合内网…...

jdbc查询mysql数据库时,出现id顺序错误的情况
我在repository中的查询语句如下所示,即传入一个List<intager>的数据,返回这些id的问题列表。但是由于数据库查询时ID列表的顺序与预期不一致,会导致返回的id是从小到大排列的,但我不希望这样。 Query("SELECT NEW com…...
深度解析:etcd 在 Milvus 向量数据库中的关键作用
目录 🚀 深度解析:etcd 在 Milvus 向量数据库中的关键作用 💡 什么是 etcd? 🧠 Milvus 架构简介 📦 etcd 在 Milvus 中的核心作用 🔧 实际工作流程示意 ⚠️ 如果 etcd 出现问题会怎样&am…...