Linux多线程[二]

引入知识

进程在线程内部执行是OS的系统调度单位。

内核中针对地址空间,有一种特殊的结构,VM_area_struct。这个用来控制虚拟内存中每个malloc等申请的空间,来区别每个malloc的是对应的堆区哪一段。OS可以做到资源的精细度划分。

对于磁盘上的exe本质上是一个文件,我们的可执行程序本来就是按照地址空间来划分的,可执行程序其实也按照了区域,被划分为以4kb为单位部分。物理内存也按照是4kb划分(软件层面的划分)。对于这么4kb的块我们也要管理起来(先描述在组织)。每个4kb我们把它叫做页帧。物理内存的4kb大小一端我们叫做页框。每次Io的时候我们就把页帧装到页框里面。虚拟内存有多少个地址(32位)2^32个。映射一定有key和value

物理内存和虚拟内存的映射关系因为物理内存是按照4字节划分的。如果每个4自己进行映射直接保存,页表压根保存不下,所以必须进行特殊的保存。

关于页表,有32位,前10位是一级页表,2^10=1024个映射关系,首先拿前10位对一级页表进行索引。找到的也不是真实物理地址,而是二级页表,11-20个比特位。对二级页表进行索引,找到数据在物理内存所在页的起始位置。然后通过起始位置进行便宜找到要访问的内容。最后12位保存的就是偏移量。这样子就可以把虚拟地址转化为物理地址。这样子就很好解决了空间不够的问题,通过一二级页表可以很好的找到对应的物理内存文件。

如何理解线程

每个进程都有自己的虚拟内存和页表,如果他创建子进程,子进程的的PCB test_struct也指向父进程的struct mm_struct.也就是说子进程有自己的pcb结构体,但是子进程公用父进程的struct mm_struct。创建的每个task_strcut就叫做线程。对于cpu来说只关心pcb一个pcb就是一个线程,cpu压根不管是线程还是进程。(linux特有的)为了管理线程也需要先描述再组织。

对于Linux上的线程和进程的区别,进程有自己的mm_struct。线程没有,线程是复用的。

既然这样子,那么我们之前的进程也需要重新理解一下,用户视角:进程=进程对应的代码和数据+内核数据结构(task_struct。。)这是我们之前理解的。内核视角:承担分配系统资源的基本实体。只有伸手向系统要资源的就被叫做进程。资源角度:之前,内部只有一个执行流的进程。现在:内部有多个执行流。这种情况叫单进程多线程。pcb我们按照现在的视角重新看下:task_strcut是进程内部的一个执行流。cpu在执行的时候压根不关心进程和线程只关心pcb结构体。进行和线程无所谓。那么既然这个是linux下的特殊处理方法。linux没有真正意义上的线程,他是和进程共用一套。linux不会提供进程接口,只提供了轻量级系统接口。于是在用户层实现了一层轻量级多线程方案,以库的形式提供给用户——pthread,原生线程库。

使用

功能:创建一个新的线程

#include<pthread.h>

原型 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void*), void *arg);

        参数 thread:返回线程ID

        attr:设置线程的属性,attr为NULL表示使用默认属性

        start_routine:是个函数地址,线程启动后要执行的函数

        arg:传给线程启动函数的参数 返回值:成功返回0;失败返回错误码(将arg传递给void *(*start_routine) (void*)

 示例代码

#include<iostream>
#include<pthread.h>
#include<string>
#include<sys/types.h>
#include<unistd.h>
#include<cstdio>
using namespace std;
void *threadrun(void *args)
{const string name=(char*)args;while(1){cout<<"name:"<<name<<"-----pid :"<<getpid()<<"\n"<<endl;sleep(1);}
}int main()
{pthread_t tid[5];char name[64];for(int i;i<5;i++){   snprintf(name,sizeof name,"%s-%d","thread",i);pthread_create(tid+i,nullptr,threadrun,(void*)name);sleep(1);//缓解传参的bug}//主线程 main中的是主进程while(1){cout<<"main thread  pid::"<<getpid()<<endl;sleep(1);}}

查看是否调用线程库

运行结果

 线程如何看待内部资源

 操作系统给线程分配资源,线程向进程申请资源,进程挂掉线程都挂掉。线程使用进程资源,很多东西他们都是共享的。

文件描述符表共享:一个线程打开文件fd=3那么下一个线程就是fd=4了

每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)

函数处理方法,初始化,未初始化

当前工作目录

用户id和组id 

但是也有不共享的

线程ID

一组寄存器(进程上下文)

errno

信号屏蔽字

调度优先级 

进程VS线程

线程切换成本更低,在进程内调度线程,地址空间不需要切换,页表不需要切换 。同时进程加载的时候有3级缓存。所以进程内部代码不需要重新加载。而切换cou需要重新加载。

线程不是越多越好,线程数量一般等于cpu的核心数,因为如果线程过多线程之间切换也需要时间。造成性能损失。

单进程类似于vfork

线程控制:

假设线程中有一个线程发送除0错误呢?导致进程整体退出。

进程等待

线程在运行的时候需要等待,会导致类似于僵尸进程的问题,造成内存泄漏。

功能:等待线程结束
原型int pthread_join(pthread_t thread, void **value_ptr);
参数thread:线程IDvalue_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cassert>
using namespace std;void *threadRun(void *args)
{int i=0;while(1){cout<<"args:"<<(char*)args<<"------runing"<<endl;sleep(1);if(i++==4){break;}}cout<<"子线程退出。。。。。"<<endl;
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadRun,(void*)"thread 1");int n=pthread_join(tid,nullptr);//默认会等待阻塞新线程退出assert(n==0);cout<<"子线程等待成功"<<endl;while(1){cout<<"main:"<<"------runing"<<endl;sleep(1);}
}

运行结果

因为进程等待的问题,所以只能子进程结束后父进程再继续传参。

线程创建回调函数返回值可以返回自己想要的值强转就可以

    return (void*)10;

但是线程返回值压根返回给谁?谁等给谁——给主线程一般。那么主线程一般如何获取到。join函数的第二个参数。

    void *ret =nullptr;//linux环境下开辟8个字节。int n=pthread_join(tid,(void**)&ret);//默认会等待阻塞新线程退出cout<<"返回值---:"<<(int)ret<<endl;
线程异常 

如何知道线程异常呢?

线程一旦异常就会全部崩溃,所以线程异常就没有什么意义了。不需要关系退出是否异常。

线程终止 

 exit??

我们发现子进程执行之后,父进程剩下的代码都不执行了。整个个进程直接终止。 所以需要专门的函数。

功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
void *threadRun(void *args)
{int i=0;while(1){cout<<"args:"<<(char*)args<<"------runing"<<endl;sleep(1);if(i++==4){break;}}pthread_exit((void*)13);//exit(2);cout<<"子线程退出。。。。。"<<endl;return (void*)10;
}

 此外还有一种线程取消的方式

功能:取消一个执行中的线程
原型int pthread_cancel(pthread_t thread);
参数thread:线程ID
返回值:成功返回0;失败返回错误码
pthread_t

线程id我们一般会想到LWP,一个整数。那么这个数我们可能有点好奇线程ID为什么这么大呢?那是因为,他表示线程的地址。因为我们用的不少linux自己创建的接口而是pthread的库。

在之前的内容中我们知道线程的栈是独立的,那么栈是在用户层还是内核层呢?用户层,操作系统执行线程的时候多个进程入栈出栈,很容易相互覆盖栈的数据,我们只能在用户层提供,进行管理区分。

 库不仅仅可以提供操作方法,也可以做数据维护。所以线程库内还维护了每个线程的私有数据。其中就包括线程ID 局部存储,以及线程对应的栈结构。库映射到内存中是线性的,为了更快的找到对应的线程资源,就使用起始地址来当线程id。主线程使用内核区栈结构,其他线程使用共享区的栈结构。同时pthread_t pthread_self(void);函数可以获取线程id。

线程全局变量是共有的但是前面加入__thread 就每个thread线程都具有一个变量,不共享。这个就叫线程的局部存储。

进程分离

我们不想等待线程,想要线程执行完自动结束就需要分离线程

线程分离之后不能join,join之后会报错。 

线程安全

线程互斥

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个 线程,其他线程无法获得这种变量。 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之 间的交互。 多个线程并发的操作共享变量,会带来一些问题。

多线程函数调度的时候很容易多个线程都调度同一个函数,很容易造成一个线程执行到一半准备返回结果,但是另一个线程开始执行对结果进行了处理之后,之前的线程返回结果覆盖率最新的结果。在并发访问的时候很容易导致时序不一致的问题。 

cpu ticket判断的时候极有可能别的线程也ticket判断,会导致多个执行流进入执行代码。同时计算机支持多个线程并行,多个线程同时跑,多个执行流同时执行一段代码。这么都会导致结果错误。

在ticket>0和ticket--的时候都很大概率发生这样的问题,那么如何避免这样的问题产生呢?加锁保护mutex。

示例代码:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cassert>
#include<cstdio>
using namespace std;
//  pthread_mutex_t mu2x;//定义一把锁
pthread_mutex_t mu2x = PTHREAD_MUTEX_INITIALIZER; //完成锁的初始化  //静态全局变量int ticker =1000;void*threadrun(void* args)
{while(1){pthread_mutex_lock(&mu2x);//对线程完成枷锁if(ticker>0)//判断本质也是计算{usleep(1000);printf("%p : %s ----%d\n",pthread_self(),(char*)args,ticker);ticker--;pthread_mutex_unlock(&mu2x);//解锁}//解锁//在加锁和解锁之间的代码是临界区else{pthread_mutex_unlock(&mu2x);//解锁break;//如果这里break的话就会一直不释放锁}}
}//锁的初始化有2中方式
//方法一: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER  int main()
{pthread_t tid,tid2,tid3;pthread_create(&tid,nullptr,threadrun,(void*)"thread 1");pthread_create(&tid2,nullptr,threadrun,(void*)"thread 2");pthread_create(&tid3,nullptr,threadrun,(void*)"thread 3");pthread_join(tid,nullptr);pthread_join(tid2,nullptr);pthread_join(tid3,nullptr);}

但是即便是加了锁也会出现一直情况,一个线程始终能抢到资源。

锁的初始化

静态初始化

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER  

 这种必须锁在全局。

动态初始化——可以在任意位置设置锁,但是不用必须释放

 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t 
*restrict attr);参数:mutex:要初始化的互斥量attr:NULL
int pthread_mutex_destroy(pthread_mutex_t *mutex);
    //锁的动态分布pthread_mutex_t mux1;pthread_mutex_init(&mux1,nullptr);//动态分配初始化;/*************************/pthread_t tid,tid2,tid3;pthread_create(&tid,nullptr,threadrun,(void*)"thread 1");pthread_create(&tid2,nullptr,threadrun,(void*)"thread 2");pthread_create(&tid3,nullptr,threadrun,(void*)"thread 3");pthread_join(tid,nullptr);pthread_join(tid2,nullptr);pthread_join(tid3,nullptr);//释放锁pthread_mutex_destroy(&mux1);/***********************************************/

动态分配一般卸载局部,那么如何将锁传递给回调函数呢? 通过定义结构体来传递结构体

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cassert>
#include<cstdio>
#include<string>
using namespace std;
#define Thread_NUM 5
//  pthread_mutex_t mu2x;//定义一把锁
//pthread_mutex_t mu2x = PTHREAD_MUTEX_INITIALIZER; //完成锁的初始化  //静态全局变量int ticker =1000;class Thread_date
{
public:Thread_date(const string&n,pthread_mutex_t *mux):tname(n),ptmax(mux){}
public:string tname;pthread_mutex_t* ptmax;};void*threadrun(void* args)
{Thread_date* td=(Thread_date*)args;while(1){pthread_mutex_lock(td->ptmax);//对线程完成枷锁if(ticker>0)//判断本质也是计算{
usleep(rand()%1500);printf("%p : %s ----%d\n",pthread_self(),td->tname.c_str(),ticker);ticker--;pthread_mutex_unlock(td->ptmax);//解锁}//解锁//在加锁和解锁之间的代码是临界区else{pthread_mutex_unlock(td->ptmax);//解锁break;//如果这里break的话就会一直不释放锁}usleep(rand()%1500);}
}//锁的初始化有2中方式
//方法一: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER  int main()
{//锁的动态分布pthread_mutex_t mux1;pthread_mutex_init(&mux1,nullptr);//动态分配初始化;/*************************/pthread_t tid[Thread_NUM];for(int i=0;i<Thread_NUM;i++){ string name="thread";name+=to_string(i+1);Thread_date *td =new Thread_date(name,&mux1);pthread_create(tid+i,nullptr,threadrun,(void*)td);}for(int i=0;i<Thread_NUM;i++){pthread_join(tid[i],nullptr);}//释放锁pthread_mutex_destroy(&mux1);/***********************************************/cout<<"总线程结束"<<endl;return 0;}

那么加锁之后就是串行了吗?加锁之后在临界区是否会切换呢?以及原子性的体现。不会,就算被切换也是,你把锁带走了,其他的线程也无法申请锁进入临界区。保证了临界资源的一致性,,假设线程不申请锁直接访问临界区,这就编码错误了。对于没有锁的线程只关心2种情况:1.其他的线程 也没有持有锁。2.其他的线程也没有释放锁。

那么加锁就算串行执行了吗?

是的,执行临界区代码一定是串行的。要访问呢临界资源,每一个线程都必须申请锁,每一个线程都必须看到同一个锁&&访问锁,锁本身就算一种共享资源。那么锁怎么保证他的安全呢?必须保证锁是原子的。那么锁是如何实现的,原子性如何让保证?

站在汇编的角度,如果只有一条汇编指令,我们就认为是原子的。swap和exchange指令是以一条指令将内存和cpu数据进行交换。cpu内部有寄存器,cpu内部寄存器本质上是当前执行流的山下文,寄存器的空间是共享的,但是寄存器的内容是私有的,逻辑如下。

 

函数重入 

一个函数被多个执行流同时进入,没有问题就是可重入函数,出问题的就算不可重入函数。之前的回调函数,加入锁之后就算可重入函数。

死锁

 在用锁的时候不一定用了一把锁,使用了好几把锁,因为锁申请次序导致必须的线程互相申请对方锁的现象叫死锁。

死锁的必要条件

互斥条件:一个资源每次只能被一个执行流使用

请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放

不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺

循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

 避免死锁

破坏死锁的四个必要条件

加锁顺序一致

避免锁未释放的场景

资源一次性分配

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

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

相关文章

视觉slam十四讲学习笔记(四)相机与图像

理解理解针孔相机的模型、内参与径向畸变参数。理解一个空间点是如何投影到相机成像平面的。掌握OpenCV的图像存储与表达方式。学会基本的摄像头标定方法。 目录 前言 一、相机模型 1 针孔相机模型 2 畸变 单目相机的成像过程 3 双目相机模型 4 RGB-D 相机模型 二、图像…

算法学习——LeetCode力扣回溯篇1

算法学习——LeetCode力扣回溯篇1 77. 组合 77. 组合 - 力扣&#xff08;LeetCode&#xff09; 描述 任何顺序 返回答案。 示例 示例 1&#xff1a; 输入&#xff1a;n 4, k 2 输出&#xff1a; [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ] 示例 2&#xff1a; 输…

串行通信的艺术:深入解析UART与奇偶校验

发送数据位是电流传输吗&#xff1f; 在UART&#xff08;Universal Asynchronous Receiver/Transmitter&#xff09;通信中&#xff0c;发送数据位不直接以电流的形式传输。而是通过改变电压水平或者光信号&#xff08;在光纤通信中&#xff09;来表示不同的数据位&#xff08…

UART通信中的奇偶校验

UART通信中的奇偶校验&#xff1a;提升数据传输可靠性的简单方法 在微控制器&#xff08;MCU&#xff09;和各种电子设备之间的数据通信领域&#xff0c;UART&#xff08;Universal Asynchronous Receiver/Transmitter&#xff0c;通用异步收发传输器&#xff09;协议是一种广泛…

qt“五彩斑斓“ opengl

本篇文章我们来描述一下opengl相关知识 我们先看一下opengl渲染的效果 很漂亮&#xff1f; 那下面就来介绍一下这么漂亮的opengl OpenGL&#xff08;Open Graphics Library&#xff09;是一个跨平台的图形编程接口&#xff0c;用于渲染2D和3D图形。它提供了一系列函数和数据结…

【AI视野·今日Robot 机器人论文速览 第七十八期】Wed, 17 Jan 2024

AI视野今日CS.Robotics 机器人学论文速览 Wed, 17 Jan 2024 Totally 49 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers Safe Mission-Level Path Planning for Exploration of Lunar Shadowed Regions by a Solar-Powered Rover Authors Olivier L…

宋小黑的生财知识资料库第1012期

大家好&#xff0c;我是小黑&#xff0c;在当前经济和职场环境中&#xff0c;拥有一份副业已经成为很多人增加财务安全感和提升个人能力的重要手段。通过投身于副业&#xff0c;你不仅可以为自己创造一个额外的收入来源&#xff0c;减少对主职工作的经济依赖&#xff0c;还可以…

心法利器[107] onnx和tensorRT的bert加速方案记录

心法利器 本栏目主要和大家一起讨论近期自己学习的心得和体会&#xff0c;与大家一起成长。具体介绍&#xff1a;仓颉专项&#xff1a;飞机大炮我都会&#xff0c;利器心法我还有。 2023年新一版的文章合集已经发布&#xff0c;获取方式看这里&#xff1a;又添十万字-CS的陋室2…

山西电力市场日前价格预测【2024-02-10】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2024-02-10&#xff09;山西电力市场全天平均日前电价为126.73元/MWh。其中&#xff0c;最高日前电价为302.95元/MWh&#xff0c;预计出现在08:15。最低日前电价为0.00元/MWh&#xff0c;预计出…

jmeter遇到连接数据库的问题

jmeter连接mysql或者oracle简单&#xff0c;但是连接过inceptor吗&#xff1f; 上货 1、下载驱动inceptor 5.1.2.jar包 2、在添加驱动那里导入 3、在JBC request中的写法 PS:没什么可说的

ZISUOJ 2022年算法基础公选课练习四(Map)

说明&#xff1a; 博主为了提早预习数据结构和C的一些知识&#xff0c;自己琢磨外加查阅资料所写的代码&#xff0c;题目来源于22年初的学院老师组织的算法基础公选课的练习。我的代码甚至思路肯定存在许多不足和错误&#xff0c;欢迎大家批评指正。 题目列表&#xff1a; 问题…

【C++】类和对象(四)

前言&#xff1a;在类和对象中&#xff0c;我们走过了十分漫长的道路&#xff0c;今天我们将进一步学习类和对象&#xff0c;类和对象这块荆棘地很长&#xff0c;各位一起加油呀。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; 专栏分类:高质量&a…