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.rhkb.cn/news/257344.html

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

相关文章

C#调用WechatOCR.exe实现本地OCR文字识别

最近遇到一个需求&#xff1a;有大量的扫描件需要还原为可编辑的文本&#xff0c;很显然需要用到图片OCR识别为文字技术。本来以为这个技术很普遍的&#xff0c;结果用了几个开源库&#xff0c;效果不理想。后来&#xff0c;用了取巧的方法&#xff0c;直接使用了WX的OCR识别模…

算法与数据结构

算法与数据结构 前言 什么是算法和数据结构&#xff1f; 你可能会在一些教材上看到这句话&#xff1a; 程序 算法 数据结构 算法&#xff08;Algorithm&#xff09;&#xff1a;是指解题方案的准确而完整的描述&#xff0c;是一系列解决问题的清晰指令&#xff0c;算法代…

【Tomcat】:One or more listeners failed to start.报错解决方案

报错信息:One or more listeners failed to start. Full details will be found in the appropriate container log file. 具体就是web.xml此配置报错: 服务器启动错误Tomcat:One or more listeners failed to start.报错解决方案 IDEA:在使用IDEA运行SSM项目的时候 , Tomcat运…

ChatGpt报错:We ran into an issue while authenticating you解决办法

在登录ChatGpt时报错&#xff1a;Oops&#xff01;,We ran into an issue while authenticating you.(我们在验证您时遇到问题)&#xff0c;记录一下解决过程。 完整报错&#xff1a; We ran into an issue while authenticating you. If this issue persists, please contact…

怎么使用ChatGPT提高工作效率?

怎么使用ChatGPT提高工作效率&#xff0c;这是一个有趣的话题。 相信不同的人有不同的观点&#xff0c;大家的知识背景和从事的工作都不完全相同&#xff0c;所以最终ChatGPT能起到的作用也不一样。 在编程过程中&#xff0c;如果我们要找一个库&#xff0c;我们最先做的肯定…

[缓存] - 2.分布式缓存重磅中间件 Redis

1. 高性能 尽量使用短key 不要存过大的数据 避免使用keys *&#xff1a;使用SCAN,来代替 在存到Redis之前压缩数据 设置 key 有效期 选择回收策略(maxmemory-policy) 减少不必要的连接 限制redis的内存大小&#xff08;防止swap&#xff0c;OOM&#xff09; slowLog …

C语言第二十三弹---指针(七)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 指针 1、sizeof和strlen的对比 1.1、sizeof 1.2、strlen 1.3、sizeof 和 strlen的对比 2、数组和指针笔试题解析 2.1、⼀维数组 2.2、二维数组 总结 1、si…

每日五道java面试题之java基础篇(八)

第一题.CopyOnWriteArrayList的底层原理是怎样的 ⾸先CopyOnWriteArrayList内部也是⽤过数组来实现的&#xff0c;在向CopyOnWriteArrayList添加元素时&#xff0c;会复制⼀个新的数组&#xff0c;写操作在新数组上进⾏&#xff0c;读操作在原数组上进⾏并且&#xff0c;写操作…

OpenGL-ES 学习(1)---- AlphaBlend

AlphaBlend OpenGL-ES 混合本质上是将 2 个片元的颜色进行调和(一般是求和操作)&#xff0c;产生一个新的颜色 OpenGL ES 混合发生在片元通过各项测试之后&#xff0c;准备进入帧缓冲区的片元和原有的片元按照特定比例加权计算出最终片元的颜色值&#xff0c;不再是新&#xf…

Prometheus服务器、Prometheus被监控端、Grafana、监控MySQL数据库、自动发现概述、配置自动发现、Alertmanager

目录 Prometheus概述 部署Prometheus服务器 环境说明&#xff1a; 配置时间 安装Prometheus服务器 添加被监控端 部署通用的监控exporter Grafana 概述 部署Grafana 展示node1的监控信息 监控MySQL数据库 配置MySQL 配置mysql exporter 配置mysql exporter 配置…

算法沉淀——字符串(leetcode真题剖析)

算法沉淀——字符串 01.最长公共前缀02.最长回文子串03.二进制求和04.字符串相乘 01.最长公共前缀 题目链接&#xff1a;https://leetcode.cn/problems/longest-common-prefix/ 编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀&#xff0c;返回空字符串…

VS Code之Java代码重构和源代码操作

文章目录 支持的代码操作列表调用重构分配变量字段和局部变量的差别Assign statement to new local variable在有参构造函数中将参数指定成一个新的字段 将匿名类转换为嵌套类什么是匿名类&#xff1f;匿名类转换为嵌套类的完整演示 转换为Lambda表达式Lambda 表达式是什么?转…

【51单片机】串口(江科大)

8.1串口通信 1.串口介绍 2.硬件电路 3.电平标准 电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种: 电平标准是数据1和数据O的表达方式,是传输线缆中人为规定的电 压与数据的对应关系,串口常用的电平标准有如下…

C++类和对象-C++运算符重载->加号运算符重载、左移运算符重载、递增运算符重载、赋值运算符重载、关系运算符重载、函数调用运算符重载

#include<iostream> using namespace std; //加号运算符重载 class Person { public: Person() {}; Person(int a, int b) { this->m_A a; this->m_B b; } //1.成员函数实现 号运算符重载 Person operator(const Per…

Redis 单线程

文章目录 Redis单线程架构Redis 单线程访问速度IO多路复用原理 Redis单线程架构 Redis的单线程架构的效果为&#xff1a;Redis的单线程是对于服务端而言的&#xff0c;Redis允许多个Redis用户端同时在线操作&#xff0c;但同时只有一个用户端在和服务端交互。多个用户同时发送…

VueCLI核心知识综合案例TodoList

目录 1 拿到一个功能模块首先需要拆分组件&#xff1a; 2 使用组件实现静态页面的效果 3 分析数据保存在哪个组件 4 实现添加数据 5 实现复选框勾选 6 实现数据的删除 7 实现底部组件中数据的统计 8 实现勾选全部的小复选框来实现大复选框的勾选 9 实现勾选大复选框来…

有趣儿的组件(HTML/CSS)

分享几个炫酷的组件&#xff0c;起飞~~ 评论区留爪&#xff0c;继续分享哦~ 文章目录 1. 按钮2. 输入3. 工具提示4. 单选按钮5. 加载中 1. 按钮 HTML&#xff1a; <button id"btn">Button</button>CSS&#xff1a; button {padding: 10px 20px;text-tr…

【ArcGIS Pro二次开发】(79):符号系统_CIMUniqueValueRenderer

CIMUniqueValueRenderer是ArcGIS Pro SDK中的一个类&#xff0c;用于创建唯一值渲染器&#xff08;Unique Value Renderer&#xff09;。 在ArcGIS Pro中长这样&#xff1a; 通过对CIMUniqueValueRenderer的操作&#xff0c;可以对符号系统进行更改&#xff0c;实现很多功能。…

从零开始手写mmo游戏从框架到爆炸(十)— 集成springboot-jpa与用户表

导航&#xff1a;从零开始手写mmo游戏从框架到爆炸&#xff08;零&#xff09;—— 导航-CSDN博客 集成springboot-jpa&#xff0c;不用mybatis框架一个是方便对接不同的数据源。第二个目前规划的游戏内容可能对数据库的依赖不是很大&#xff0c;jpa应该肯定能满足要求了…

单片机的认识

单片机的定义 先简单理解为&#xff1a; 在一片集成电路芯片上集成了微处理器&#xff08;CPU &#xff09;存储器&#xff08;ROM和RAM&#xff09;、I/O 接口电路&#xff0c;构成单芯片微型计算机&#xff0c;即为单片机。 把组成微型计算机的控制器、运算器、存储器、输…