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.mzph.cn/news/682970.shtml

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

相关文章

嵌入式软件设计入门:从零开始学习嵌入式软件设计

&#xff08;本文为简单介绍&#xff0c;个人观点仅供参考&#xff09; 首先,让我们了解一下嵌入式软件的定义。嵌入式软件是指运行在嵌入式系统中的特定用途软件,它通常被用来控制硬件设备、处理实时数据和实现特定功能。与桌面应用程序相比,嵌入式软件需要具备更高的实时性、…

反无人机系统技术分析,无人机反制技术理论基础,无人机技术详解

近年来&#xff0c;经过大疆、parrot、3d robotics等公司不断的努力&#xff0c;具有强大功能的消费级无人机价格不断降低&#xff0c;操作简便性不断提高&#xff0c;无人机正快速地从尖端的军用设备转入大众市场&#xff0c;成为普通民众手中的玩具。 然而&#xff0c;随着消…

Python算法题集_翻转二叉树

Python算法题集_翻转二叉树 题226&#xff1a;翻转二叉树1. 示例说明2. 题目解析- 题意分解- 优化思路- 测量工具 3. 代码展开1) 标准求解【DFS递归】2) 改进版一【BFS迭代&#xff0c;节点循环】3) 改进版二【BFS迭代&#xff0c;列表循环】 4. 最优算法 本文为Python算法题集…

Spring Boot 笔记 019 创建接口_文件上传

1.1 创建阿里OSS bucket OSS Java SDK 兼容性和示例代码_对象存储(OSS)-阿里云帮助中心 (aliyun.com) 1.2 编写工具类 package com.geji.utils;import com.aliyun.oss.ClientException; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun…

加速创新如何先从创意管理开始?

文章详细介绍了什么是创意管理以及它在组织中的重要性和最佳实践。创意管理是指在组织内捕捉、组织、评估和实施创意的过程。它通过建立一个结构化的系统&#xff0c;从员工、客户或其他利益相关者那里收集创意&#xff0c;并系统地审查和选择最有前景的创意进行进一步的开发或…

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

算法学习——LeetCode力扣回溯篇3 491. 非递减子序列 491. 非递减子序列 - 力扣&#xff08;LeetCode&#xff09; 描述 给你一个整数数组 nums &#xff0c;找出并返回所有该数组中不同的递增子序列&#xff0c;递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。…

2024龙年特别篇 -- 魔法指针 之 指针变量的意义 指针运算

学习完指针变量&#xff1a;链接后&#xff0c; 我们继续学习指针变量的应用 目录 程序展示 原始方式 指针变量方式 代码对比 指针运算 指针-整数 用指针打印数组内容 使用指针打印1-10中的奇数 指针-指针 指针的关系运算 程序展示 打印一个有10个元素的数组&am…

语言与科技创新(大语言模型对科技创新的影响)

1.科技创新中的语言因素 科技创新中的语言因素至关重要&#xff0c;具体体现在以下几个方面&#xff1a; 科技文献交流&#xff1a; 英语作为全球科学研究的通用语言&#xff0c;极大地推动了科技成果的国际传播与合作。科学家们在发表论文、报告研究成果时&#xff0c;大多选…

ChatGPT高效提问—prompt实践(教师助手)

ChatGPT高效提问—prompt实践&#xff08;教师助手&#xff09; 下面来看看ChatGPT在教育领域有什么用途。 首先设定ChatGPT的角色为高中教师助手。 输入prompt: ChatGPT输出&#xff1a; ​ 教师助手的角色已经设置完成。下面通过几种不同的情景演示如何使用。 1.1.1 制定…

2001-2022年368个地级市平均气温数据

2001-2022年368个地级市平均气温数据 1、时间:2001-2022年 2、范围&#xff1a;368个地级市 3、来源&#xff1a;基于NOAA下属NCEI提供的原始数据编制而成的。 4、指标&#xff1a;年份、省份、省份代码、城市、城市代码、平均气温 5、指标解释&#xff1a;平均气温指某一…

【JavaEE】_JavaScript(Web API)

目录 1. DOM 1.1 DOM基本概念 1.2 DOM树 2. 选中页面元素 2.1 querySelector 2.2 querySelectorAll 3. 事件 3.1 基本概念 3.2 事件的三要素 3.3 示例 4.操作元素 4.1 获取/修改元素内容 4.2 获取/修改元素属性 4.3 获取/修改表单元素属性 4.3.1 value&#xf…

机器学习、深度学习、强化学习、迁移学习的关联与区别

Hi&#xff0c;大家好&#xff0c;我是半亩花海。本文主要了解并初步探究机器学习、深度学习、强化学习、迁移学习的关系与区别&#xff0c;通过清晰直观的关系图展现出四种“学习”之间的关系。虽然这四种“学习”方法在理论和应用上存在着一定的区别&#xff0c;但它们之间也…

FreeRTOS 队列管理

概览 基于 FreeRTOS 的应用程序由一组独立的任务构成——每个任务都是具有独立权 限的小程序。这些独立的任务之间很可能会通过相互通信以提供有用的系统功能。 FreeRTOS 中所有的通信与同步机制都是基于队列实现的。 本章期望让读者了解以下事情   如何创建一个队列   …

有限合伙协议书(模板)下

第六章 合伙事务的执行 第十七条 有限合伙人不执行合伙事务&#xff0c;对外不具有代表权。有限合伙企业由普通合伙人执行合伙事务。 第十八条 经全体合伙人一致同意可以委托一个普通合伙人&#xff08;也可以委托数个普通合伙人&#xff09;对外代表合伙企业&#xff0c;执…

算法学习——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; 输…

【机器学习案例4】为机器学习算法编码分类数据【含源码】

目录 编码分类数据 序数编码 标签编码 一次性编码 目标编码 目标编码的优点 目标编码的缺点 在现实生活中,收集的原始数据很少采用我们可以直接用于机器学习模型的格式,即数值型数据。因此,需要进行一些预处理,以便以正确的格式呈现数据、选择信息丰富的数据或降低其…

【C++函数探幽】内联函数inline

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 目录 1. 前言2.概念3.特性…

GPT-4带来的思想火花

GPT-4能够以其强大的生成能力和广泛的知识储备激发出众多思想火花。它能够在不同的情境下生成新颖的观点、独特的见解和富有创意的解决方案&#xff0c;这不仅有助于用户突破思维定势&#xff0c;还能促进知识与信息在不同领域的交叉融合。 对于研究者而言&#xff0c;GPT-4可能…

浅谈业务场景中缓存的使用

业务场景中缓存的使用 一、背景二、缓存分类1.本地缓存2.分布式缓存 三、缓存读写模式1.读请求2.写请求 四、缓存穿透1.缓存空对象2.请求校验3.请求来源限制4.布隆过滤器 五、缓存击穿1.改变过期时间2.串行访问数据库 六、缓存雪崩1.避免集中过期2.提前更新缓存 七、缓存与数据…

【MATLAB】鲸鱼算法优化混合核极限学习机(WOA-HKELM)回归预测算法

有意向获取代码&#xff0c;请转文末观看代码获取方式~也可转原文链接获取~ 1 基本定义 鲸鱼算法优化混合核极限学习机&#xff08;WOA-HKELM&#xff09;回归预测算法是一种结合鲸鱼优化算法和混合核极限学习机的混合算法。其原理主要包含以下几个步骤&#xff1a; 初始化&am…