Linux——多线程(一)

news/2024/7/21 23:28:15/文章来源:https://blog.csdn.net/m0_74515566/article/details/139234526

一、线程的概念

1.1线程概念

教材中的概念:                                                                   (有问题?)        

线程是进程内部的一个执行分支,线程是CPU调度的基本单位

之前我们讲的进程:

加载到内存中的程序,叫做进程---(修正)---->进程=内核数据结构+进程代码和数据

 1.2线程的理解(以Linux系统为例)

正文:代码段(区),我们的代码在进程中,全部都是串行(单进程)调用的 

进程新建,(时空)成本较高(一堆数据结构,映射...)

地址空间和地址空间上的虚拟地址,本质其实是一种"资源"(进程的大部分资源需要通过地址空间来访问)

多执行流(并行)  :正文代码分好,已、未初始化区公开,创建执行流就多加一个task_struct,指向同一块地址空间

我们把在进程地址空间中这样创建的"进程",称为线程

 

理解概念:线程是进程内部(进程地址空间)的一个执行分支,线程是CPU调度的基本单位,CPU调度只关注进程PCB,也就是task_struct

Linux为什么要这么设计"线程"?

如果我们要设计线程,OS也要对线程做管理。

怎么管理呢?依旧是那六个字,先描述,再组织

如果像上面这样设计的话,OS会变得非常的复杂 

Linux的设计者认为:进程和线程都是执行流,具有极度的相似性,没必要单独设计数据结构和代码,直接复用就行 

1.2.1线程与进程  

进程 vs 线程

什么是进程?

中间那个圈里面的叫进程 

task_struct 是进程中的执行流,这里我们暂时叫它线程

以前我们说的进程:一个内部只有一个线程的进程;

今天我们讲的进程:一个内部至少有一个线程的进程

CPU不用区分task_struct(进、线程都是执行流)

在内核的角度: 进程是承担分配系统资源的基本实体

关于调度的问题:线程<=执行流(轻量级进程)<=进程

Linux所有的调度执行流都叫做:轻量级进程

Linux内核中并没有线程的概念,线程是用进程PCB来模拟的

那么多个执行流如何进行代码划分?如何理解?

OS要不要管理内存呢?自然是要的

大部分OS的内存都是以4KB为大小的内存块组成

 在OS的术语里:把4KB的空间or内容叫 页框/页帧

在物理内存中,每一块被分割成4KB大小的数据块,被叫做页框

以上还不是管理,OS要先描述,再组织----------->定义一个结构体

先描述:

#define Kerucl 0x1
#define User 0x2
#define USR 0x4
#define NOUSR 0x8struct Page
{int flag;//其他属性...
}

再组织

struct Page mem[1048576];

对内存的管理,变成对此数组的增删查改(操作系统对内存管理的基本单位是4KB)

可执行文件:写好的代码会被编译器处理形成二进制可执行文件放在磁盘中,在运行的时候加载到内存中

编译器在处理源文件生成二进制可执行文件,同样是以4KB为单位的,这4KB的数据块被叫做页帧

1.2.2页表(页目录+页表项)

再来讲讲页表

每个进程的虚拟地址空间都是4GB,也就是2^32个地址,如果每个虚拟地址都在页表中存一个物理地址,那样占的空间也太大了,这还没算映射在物理内存的地址,再加上其它属性和另外一些杂七杂八的,假设每一行就占10B的空间,那么光是页表就占10*2^32 = 40G 的空间,怎么可能一个进程的页表比物理内存都大。

真正的页表是什么呢?

在32位机器上,地址的大小是4G字节,也就是有32个比特位

将32个比特位分为10个比特位,10个比特位,12个比特位,共3组:

32个比特位的最高10位,代表的是页目录的下标;[0,1023]的下标代表了页目录总共有2^10 = 1024个

页目录中存放的是页表项的地址,可以通过下标找到对应的页表项。

32个比特位的中间10位代表的是页表项的下标,最多能够有1024个页表项

页表项里面存的是物理内存中每个4KB内存块(页框)的起始地址

 这最后的12位是什么呢?

0x1234+虚拟地址的后12位为对应的数据(页内偏移刚好4KB)

2^12 = 4KB = 1024 * 2 * 2  

1.3Linux线程的系统调用

在上面中我们讲过,在Linux内核中是不存在线程这一个概念的,没有TCB数据结构以及管理算法,而我们所说的线程,在Linux系统都是轻量级进程

Linux操作系统中也没有提供创建线程的系统调用。

只提供了轻量级进程的系统调用 

无论是宏观操作系统,还是用户(程序员)都只认线程的概念,但Linux内核中没有线程的概念。

那么Linux是怎么做到当用户在创建线程时,就在内核中创建轻量级进程的呢?

用户创建线程时会调用一个线程库,这个线程库里封装了Linux的轻量级进程的系统调用接口,这样一来就能在Linux中创建轻量级进程了。 而且这个线程库时所有Linux操作系统都一定自带的一个库,所以也叫原生线程库

1.4pthread_create(线程创建)

给不同的线程分配不同的区域,本质就是让不同的线程,各自看到全部页表的子集

man pthread_create

参数:

pthread_t *thread:                                线程标识符线程tid

const pthread_attr_t *attr:                    线程属性,这个阶段我们一般设为nullptr

void *(*start_routine) (void *):              函数指针,创建出的新线程执行的就是此函数的代码

void *arg :                                            传给线程函数的参数,是上个参数的指向函数的形参

返回值:创建成功返回0,创建失败返回错误码

用pthread_create创建一个新线程 

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;void* handler(void *args)
{while(true){cout<<"i am newthread"<<endl;sleep(1);}} int main()
{pthread_t tid;int n = pthread_create(&tid,nullptr,handler,nullptr);while(true){cout<<"i am main thread,running..."<<endl;sleep(1);}return 0;
}

 makefile

 

mytest:thread.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f mytest

此时编译报错:链接错误,编译器不认识pthread_create函数。我们创建新线程需要通过原生线程库去创建,此时编译器找不到原生线程库。我们需要使用-l去选项指定链接原生线程库pthread

这时候就编译成功了

 

  用命令查了一下只查到了一个进程

用另外的命令查到两个执行流(-L查看轻量级进程)

PID: 这个我们之前讲过进程pid(进程标识符)

LWP:light weight process 轻量级进程

通过上图我们发现,两个执行流他们的PID是相同的,而LWP是不同的

这也证明线程是进程的一部分(其中的一个执行流)

可以看到第一个线程的LWP和PID是一样的,这个线程被叫做主线程,其它不一样的叫做新线程

 

新创建出来的线程称为新线程

main所对应的执行流会自动向下运行,称为主线程 

那么OS在进行调度时,用哪个id进行调度呢?LWP

那以前我们讲的单进程、多进程的调度呢?

其实就是每个进程内部只有一个执行流,LWP==PID

二、线程的共有资源、私有资源

再了解一个函数

man pthread_self

无参数,获取线程id(tid) 

#include<iostream>
#include<unistd.h>
#include<string>
#include<pthread.h>
using namespace std;
std::string ToHex(pthread_t tid)
{char id[64];snprintf(id,sizeof(id),"0x%lx",tid);return id;
}
int g_val = 2;
void* threadrun(void *args)
{int cnt = 5;std::string threadname = (char*)args;while(cnt){cout<<threadname<<"is running:"<<cnt<<",pid:"<<getpid()<<",mythread id:"<<ToHex(pthread_self())<<",g_val:"<<g_val<<",&g_val"<<&g_val<<endl;g_val++;sleep(1);cnt--;}
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");int cnt = 10;while(cnt){cout<<"main thread is running...,cnt:"<<cnt<<",pid:"<<getpid()<<",newthread tid"<<ToHex(tid)<<",main thread id:"<<ToHex(pthread_self())<<",g_val:"<<g_val<<",&g_val"<<&g_val<<endl;sleep(1);cnt--;}return 0;
}

 注意不同线程中g_val的值以及地址:新线程、主线程看到的全局变量是同一个;当任何一个线程修改这个全局变量的值时,都会影响另外一个线程使用这个值

在新线程、主线程中,这个全局变量的地址相同,说明他们使用的是同一个全局变量

上面代码说明进程的数据段资源也被线程共享

进程中的绝大部分资源都是被所有线程共享的(代码和全局数据、进程文件描述符表)

那么线程自己有没有私有的资源呢?

PCB属性私有:所有线程都有各自的PCB,PCB中的属性肯定是私有的,属于各自线程

线程的硬件上下文数据(CPU寄存器的值)私有:CPU在调度PCB的时候,采用轮转时间片的方式,当一个线程被换下时,该线程的上下文一定是私有的,防止被其他线程修改而导致恢复上下文的时候出现错误。

线程的独立栈结构

不同线程各自的临时变量一定是私有的,而临时变量存放在栈结构中,所有栈也是私有的

共享的都是同一块虚拟地址空间,为什么非是栈结构私有呢?还有一个原因是原生线程库的实现。

man clone 

系统调用clone是用来创建子进程(轻量级线程)的,也就是没有独立的虚拟地址空间,clone中有一个参数:void *stack,这个参数就是用来开这个子进程的栈空间的

所以 我们在调用pthread_create创建新线程的时候底层会调用clone去指定该线程的私有栈结构

三、线程特点

3.1线程优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程切换需要保存的上下文数据,只比进程少了一点,为什么说线程之间的切换需要操作系统做的工作要少很多? 

cache:集成在CPU内的离CPU寄存器远而容量比寄存器大得多的缓存

 一行代码执行完,大概率会执行下一行,可能将相关代码搬到cache中缓存起来,后面读代码直接从cache中读取;如果是两个进程之间的切换的话,那么cache缓存的内容会失效,切换B进程之后要清空cache,然后把B进程的代码和数据放里面,线程的切换中cache不用清空

3.2线程缺点 

线程的健壮性较差:

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的

缺乏访问控制:
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高:
编写与调试一个多线程程序比单线程程序困难得多

性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

 主线程退出 == 进程退出 == 所有线程都要退出

所以

  1. 往往我们需要main thread最后结束
  2. 线程也要被"wait",要不然会产生类似进程那里的内存泄露的问题

四、线程控制

4.1pthread_create(线程创建)

在上面的1.4中我们已经提到了pthread_create的使用方法,这里做几点小补充

线程退出无非3中结果:

  1. 代码跑完,结果对
  2. 代码跑完,结果不对
  3. 出异常——重点

 

#include<iostream>
#include<unistd.h>
#include<string>
#include<pthread.h>
using namespace std;
int g_val = 2;
void *threadrun(void *args)
{int cnt = 5;while(cnt){printf("new thread,g_val:%d,&g_val:%p\n",g_val,&g_val);g_val++;sleep(1);int *p = nullptr;*p = 100;//故意野指针cnt--;}return (void*)123;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");void *ret = nullptr;while(true){printf("main thread is running,g_val:%d,&g_val:%p\n",g_val,&g_val);sleep(1);}int n = pthread_join(tid,&ret);cout << "main thread quit, n=" << n << " main thread get a ret: " << (long long)ret << std::endl;
}

 

我们怎么没有像进程一样获取线程的退出信号呢?只有自己手动写的退出码。

因为一旦有一个线程出异常,整个进程就都寄了,不考虑线程异常的情况 

4.2pthread_join(线程等待)

如何进行线程等待呢?man pthread_join

函数参数:

pthread_t thread : 线程标识符(tid)

void **retval:输出型参数,主线程等待新线程的返回值,&(void)------>void**

 返回值:等待成功返回0,等待失败返回错误码

#include<iostream>
#include<unistd.h>
#include<string>
#include<pthread.h>
using namespace std;void* threadrun(void *args)
{int cnt = 5;std::string threadname = (char*)args;while(cnt){cout<<threadname<<" is running..."<<endl;sleep(1);cnt--;}return (void*)123;
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");void *ret = nullptr;int cnt = 10;while(cnt){cout<<"main thread is running..."<<endl;sleep(1);cnt--;}int n = pthread_join(tid,&ret);cout << "main thread quit, n=" << n << std::endl;return 0;
}

 

 

 4.3pthread_exit(线程结束)

线程结束

1.线程函数结束

2.pthread_exit()

不能直接exit线程,因为它是终止进程的 

 

 参数:void* retval:返回线程结束信息,当前阶段设置成nullptr即可。

void* threadrun(void *args)
{int cnt = 5;std::string threadname = (char*)args;while(cnt){cout<<threadname<<" is running..."<<endl;sleep(1);cnt--;if(cnt == 2){pthread_exit(nullptr);}}return (void*)123;
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");void *ret = nullptr;int cnt = 10;while(cnt){cout<<"main thread is running..."<<endl;sleep(1);cnt--;}int n = pthread_join(tid,&ret);cout << "main thread quit, n=" << n << std::endl;return 0;
}

新线程退出,只剩下主线程

3.int pthread_cancel(tid);//线程取消

 

参数:要取消线程的tid

返回值:取消成功返回0,取消失败返回错误码

 

void* threadrun(void *args)
{int cnt = 5;std::string threadname = (char*)args;while(cnt){cout<<threadname<<" is running..."<<endl;sleep(1);cnt--;}return (void*)123;
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");void *ret = nullptr;int cnt = 10;while(cnt){cout<<"main thread is running..."<<endl;sleep(1);cnt--;if(cnt == 7){break;}}pthread_cancel(tid);cout<<"pthread_cancel:"<<tid<<endl;int n = pthread_join(tid,&ret);cout << "main thread quit, n=" << n << " main thread get a ret: " << (long long)ret << std::endl;return 0;
}

 

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

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

相关文章

重学java 49 List接口

但逢良辰&#xff0c;顺颂时宜 —— 24.5.28 一、List接口 1.概述: 是collection接口的子接口 2.常见的实现类: ArrayList LinkedList Vector 二、List集合下的实现类 1.ArrayList集合的使用及源码分析 1.概述 ArrayList是List接口的实现类 2.特点 a.元素有序 —> 按照什么顺…

简单随机数据算法

文章目录 一&#xff0c;需求概述二&#xff0c;实现代码三、测试代码四、测试结果五、源码传送六、效果演示 一&#xff0c;需求概述 系统启动时&#xff0c;读取一组图片数据&#xff0c;通过接口返回给前台&#xff0c;要求&#xff1a; 图片随机相邻图片不重复 二&#…

Node.js和npm常用命令

一、Node.js简介 Node.js是一个免费、开源、跨平台的JavaScript运行时环境&#xff0c;允许开发人员创建服务器、web应用程序、命令行工具和脚本。 点击查看node.js中文官网 点击查看node.js英文官网 二、npm简介 npm(Node Package Manager)是Node.js的软件包管理器&#xff0…

三层交换机基本配置,动态路由链接

<Huawei>system-view //进入系统视图[Huawei]undo info-center enable //关日志[Huawei]vlan batch 2 3 //创建vlan2与3[Huawei]display vlan //检查[Huawei]interface GigabitEthernet 0/0/2 //进2口[Huawei-GigabitEthernet0/0/2]port link-type access //配置…

安全态势管理的六大挑战:态势感知

德迅云安全鉴于如今的安全威胁不断变幻&#xff0c;企业对实施态势管理策略至关重要&#xff0c;可以让安全团队根据需要进行安全策略的动态调整。如果企业在研究构建态势感知管理&#xff0c;需要特别关注以下六个方面的挑战。 如果企业正在使用一个或多个平台&#xff0c;那么…

如何用分立器件设计一个过流保护电路

过流保护电路是指当后级负载电流过大或者发生短路时&#xff0c;自动切断电源与后级负载&#xff0c;从而防止大电流损害电源电路&#xff0c;过流保护又叫OCP。 常见的过流保护电路有保险丝过流保护&#xff0c;集成的过流保护芯片&#xff0c;还有这种分立器件搭建的过流保护…

【vue-cli搭建vue项目的过程2.x】

vue-cli搭建vue项目 vue-cli搭建vue项目安装node安装vue-cli脚手架并创建项目安装 Ant Design Vue或element-ui(笔者使用Ant-design-vue组件&#xff0c;并全局引入)开发安装三方库包1、Package.json文件---引入如下package.json文件执行npm i或npm install命令即可下载如下依赖…

java多线程创建方式

1. 继承Thread类 这种方式是通过创建一个新的类继承自Thread类&#xff0c;并覆盖run()方法来创建线程。然后通过创建这个类的对象并调用其start()方法来启动线程。 public class MyThread extends Thread { public void run() { // 在这里定义线程的执行逻辑 …

基于小波分析和机器学习(SVM,KNN,NB,MLP)的癫痫脑电图检测(MATLAB环境)

癫痫是一种由大脑神经元突发性异常放电导致的大脑功能性障碍疾病。据世界卫生组织统计&#xff0c;全球约有7000万人患有癫痫。癫痫患者在发病时呈现肌肉抽搐、呼吸困难、意识丧失等症状。由于癫痫发作的偶然性&#xff0c;患者极有可能在高空、驾驶、游泳等危险情况下发病并丧…

SCI一区 | Matlab实现PSO-TCN-LSTM-Attention粒子群算法优化时间卷积长短期记忆神经网络融合注意力机制多变量时间序列预测

SCI一区 | Matlab实现PSO-TCN-LSTM-Attention粒子群算法优化时间卷积长短期记忆神经网络融合注意力机制多变量时间序列预测 目录 SCI一区 | Matlab实现PSO-TCN-LSTM-Attention粒子群算法优化时间卷积长短期记忆神经网络融合注意力机制多变量时间序列预测预测效果基本介绍程序设…

Python条件分支与循环

大家好&#xff0c;当涉及到编写高效和灵活的程序时&#xff0c;条件分支和循环是 Python 中至关重要的概念。它们允许我们根据不同的条件执行不同的代码块&#xff0c;或者重复执行一组语句。条件分支和循环是测试开发工程师在日常工作中经常使用的工具&#xff0c;无论是编写…

kind: Telemetry

访问日志 访问日志提供了一种从单个工作负载实例的角度监控和理解行为的方法。 Istio 能够以一组可配置的格式为服务流量生成访问日志&#xff0c; 使操作员可以完全控制日志记录的方式、内容、时间和地点。 有关更多信息&#xff0c;请参阅获取 Envoy 的访问日志。 https:/…

二叉树(概念详解)

目录 树树的概念完全二叉树和满二叉树完全二叉树存储完全二叉树用数组存储堆 树 树是一种非线性的数据结构 树是根朝上的&#xff0c;叶子是朝下的 树是递归定义的【根N棵子树】 递归定义&#xff1a;大事化小&#xff0c;可以理解为套娃 树的子树之间是独立的&#xff0c;所…

conda 环境找不到 libnsl.so.1

安装prokka后运行报错 perl: error while loading shared libraries: libnsl.so.1: cannot open shared object file: No such file or directory 通过conda list 可以看到 有libsnl 2.00版本&#xff0c;通过修改软链接方式进行欺骗

面试题:字符串“1024“不强转怎么变成数字1024(ASCII应用)

面试题&#xff1a;就是面试官很秀的场合。怎么把字符串"1024"转成1024 1.ASCII码表是什么&#xff1f; ASCII(American Standard Code for Information Interchange)码表使用用于将字符转换成对应数字的编码规范。它由美国国家标准协会(ANSI)于1963年制定&#xf…

Llama模型家族训练奖励模型Reward Model技术及代码实战(二)从用户反馈构建比较数据集

LlaMA 3 系列博客 基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;一&#xff09; 基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;二&#xff09; 基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;三&#xff09; 基于 LlaMA…

AAAI2024 基于扩散模型 多类别 工业异常检测 DiAD

前言 本文分享一个基于扩散模型的多类别异常检测框架&#xff0c;用于检测工业场景的缺陷检测或异常检测。 设计SG语义引导网络&#xff0c;在重建过程中有效保持输入图像的语义信息&#xff0c;解决了LDM在多类别异常检测中的语义信息丢失问题。高效重建&#xff0c;通过在潜…

rust语言初识

程序设计实践课上水一篇ing 来源&#xff1a;rust基础入门-1.初识rust-酷程网 (kucoding.com) rust作为一名新兴语言&#xff0c;与go又有些许不同&#xff0c;因为它的目标是对标系统级开发&#xff0c;也就是C、C这两位在编程界的位置。比如我们最常用的windows系统&#x…

【漏洞复现】用友NC registerServlet JNDI 远程代码执行漏洞(XVE-2024-10248)

0x01 产品简介 用友NC是 用友软件股份有限公司开发的一套企业级管理软件系统。它是一个基于互联网的多层应用系统&#xff0c;旨在为中大型企业提供全面、集成的管理解决方案。是一种商业级的企业资源规划云平台&#xff0c;为企业提供全面的管理解决方案&#xff0c;包括财务…

2024年蓝桥杯Web开发【大赛大纲】15届

一、 组别 Web应用开发分为&#xff1a;大学组和职业院校组。 每位选手只能申请参加其中一个组别的竞赛。各个组别单独评奖。 研究生和本科生只能报大学组。 其它高职高专院校可自行选择报任意组别。 二. 竞赛赛程 省赛时长&#xff1a;4小时。 决赛时长&#xff1a;4小…