Linux-多线程

 线程的概念

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
  • 进程是承担资源分配的基本实体,线程是cpu调度的基本单位

        在操作系统的学习中,我们可能在课本上看到过进程是被PCB描述的,而线程是被TCB描述的,在Linux中对于线程的设计有所不同,并没有设计出struct tcb的结构体,Linux实现线程是通过复用原有结构PCB,而windows操作系统就依据操作系统书籍上的规定设计出了专门管理线程的TCB结构体,相比于windows的做法Linux是更具有优势的,因为线程是在进程内部运行的,它的结构和数据和进程是比较相似的,采用复用PCB的方式,可以省去为线程创建初始化TCB结构体、构建地址空间等工作,只需要创建一个PCB然后与进程复用同一份资源就可以了,可以节省空间,并且如果将线程单独设计出来,还要新设计出线程的调度算法,成本比较高,此外,Windows对于线程的设计更为复杂,程序出问题后的调试工作相比于Linux也会更加困难。

问题:已经有多进程了为什么还要有多线程?

【解释】:

        在启动方面,创建一个进程需要构建进程PCB,地址空间、页表并将与物理内存构建映射关系,再将内存的数据导入,而创建一个线程只需要创建一个PCB,将进程的资源导入即可,因为其与进程共用一个地址空间和页表,内存的映射关系也是一样的,相比之下创建一个线程的代价是很小的

        在运行方面,调度线程也是比调度进程速成本低的。在CPU内部存在一个cache缓冲区,假设进程A要访问第五行代码,根据局部性原理,OS可能认为下一步可能会访问第六行、第七行代码,OS就会直接将周围的50行代码,或者一个代码块预先加载到cache中,cache中的数据称为热数据,如果下一次访问数据会先在cache中查找,如果命中的话就可以直接使用了,如果没有命中则重新加载数据,当进程A访问完,轮到进程B调度了,此时cache中的热数据进程B一点都用不到,还需要重新从内存中加载数据到cache中,效率比较低。而由于线程执行的是进程中的代码,当一个进程的线程进行调度时,cache中的代码不同的线程可能都用的到,就不需要重新加载了。

        在删除方面,删除一个进程需要删除进程PCB、地址空间、页表等,而删除一个线程只需要删除对应的PCB即可。综上所述,多线程相比于多进程存在许多优点

线程的优点

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

 线程的缺点

1. 性能损失

        一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的 同步和调度开销,而可用的资源不变。例如一个单核的CPU我们硬要将一个计算分给10个线程进行,此时的效率可能没有让一个线程直接计算的效率高,因为此时有线程调度切换的成本,所以线程并不是创建越多越好

2. 缺乏访问控制

      在一个多线程的程序中,由于线程是共用进程的地址空间的,大多数数据其实是被共享的,如果一个线程无意改变了另一个线程的数据,可能会造成不良影响

3. 健壮性差

        单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该 进程内的所有线程也就随即退出

线程的用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是 多线程运行的一种表现)

进程与线程对比

  1. 进程是资源分配的基本单位
  2. 线程是调度的基本单位
  3. 线程共享进程数据,但也拥有自己的一部分数据:
  • 线程ID
  • 一组寄存器
  • errno
  • 信号屏蔽字
  • 调度优先级

其中比较重要的是寄存器和栈

线程拥有自己独立的寄存器,主要是出于以下几个方面的考虑:

  1. 独立计算:每个线程在执行时都需要一个独立的上下文环境,以便进行独立的计算。拥有自己的寄存器可以确保线程在执行过程中不会受到其他线程的影响,从而保持计算的准确性和独立性。
  2. 避免冲突:在多线程环境中,如果多个线程共享同一个寄存器,那么它们之间的操作可能会相互干扰,导致数据错乱或执行错误。拥有独立的寄存器可以避免这种冲突,确保每个线程都能在自己的环境中安全地执行。
  3. 减少上下文切换开销:当CPU在多个线程之间切换时,需要保存和恢复每个线程的上下文环境。如果线程拥有独立的寄存器,那么在切换时只需要保存和恢复这些寄存器的值,而不需要处理共享资源可能带来的复杂同步问题,从而减少了上下文切换的开销。
  4. 快速恢复执行状态:由于每个线程都拥有自己的寄存器,因此在被调度执行时,可以迅速地从寄存器中恢复执行状态,而不需要重新加载或计算大量的数据,这有助于提高线程的执行效率和响应速度。
  5. 并发控制:在多线程并发执行时,每个线程都需要独立地控制自己的执行流程和状态。拥有独立的寄存器可以使得线程在并发执行时能够更好地控制自己的执行流程,避免因为共享资源而导致的并发控制问题。
  6. 并行加速:在并行处理中,多个线程可以同时执行不同的任务。如果每个线程都拥有自己的寄存器,那么它们就可以并行地处理数据,从而提高整个系统的处理能力和加速比。

线程也要拥有自己独立的栈,因为如果多进程公用进程地址空间中的栈的话,线程之间进行入栈出栈操作可能会互相造成影响


进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

进程和线程的关系如下图:

进程控制

创建线程

功能:创建一个新的线程
原型
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;失败返回错误码

错误检查:

  • 传统的一些函数是,成功返回 0,失败返回-1,并且对全局变量 errno 赋值以 指示错误。 
  • pthreads 函数出错时不会设置全局变量 errno(而大部分其他 POSIX 函数会这 样做)。而是将错误代码通过返回值返回
  • pthreads 同样也提供了线程内的 errno 变量,以支持其它使用 errno 的代码。 对于 pthreads 函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程 内的 errno 变量的开销更小

线程 ID 及进程地址空间布局

  • pthread_ create 函数会产生一个线程 ID,存放在第一个参数指向的地址中。 该线程 ID 和前面说的线程 ID不是一回事。
  • 前面讲的线程 ID 属于进程调度的范畴。因为线程是轻量级进程,是操作系统 调度器的最小单位,所以需要一个数值来唯一表示该线程。
  •  pthread_ create 函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程 ID,属于 NPTL 线程库的范畴。线程库的后续操作,就是根据该线程 ID 来操作线程的。
  •  线程库 NPTL 提供了 pthread_ self 函数,可以获得线程自身的 ID:
pthread_t pthread_self(void);

pthread_t 到底是什么类型呢?取决于实现。对于 Linux 目前实现的 NPTL 实现而 言,pthread_t 类型的线程 ID,本质就是一个进程地址空间上的一个地址。

由于我们使用的线程接口都是在线程库中维护的,所以我们想创建一个线程的前提是让线程库加载到内存并映射到地址空间,当我们创建一个线程时,pthread库就会在其内部创建一个结构用来管理这个线程,就像我们使用的文件操作,调用fopen函数会返回一个FILE*的结构体,而FILE结构体就是维护在c标准库中的,而tid就是线程库维护的,用于标识每个线程的唯一性,tid是进程级的内核看不到,是用户级的用来让用户操作。

我们通过ps -al可以查看当前进程的所有线程,其中的LWP(轻量级进程)也是用来唯一标识线程的,Linux下的tid和LWP在本质上是一致的,都是用来唯一标识一个线程。tid是系统调用层面使用的术语,而LWP则是从内核视角看到的线程实现方式。在用户程序中,我们可能更习惯于使用tid来标识线程;而在查看系统线程信息时,LWP则是一个常见的显示方式。两者之间的关系可以理解为内核态与用户态对线程概念的不同表述方式。

线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit,主线程退出该进程的所有线程都会退出
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit函数
功能:线程终止
原型void pthread_exit(void* value_ptr);
参数value_ptr:value_ptr不要指向一个局部变量,用于后续接收从线程函数的返回值
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

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

线程等待

  • 已经退出的线程(执行完函数),其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。
  • 如果不进行线程等待会造成类似于僵尸进程的后果
功能:等待线程结束
原型:int pthread_join(pthread_t thread, void **value_ptr);
参数: thread:线程 ID value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回 0;失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。在函数内部调用pthread_exit()和return的效果是一样的 
  3. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED,在标准输出中打印出来为-1
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

分离线程

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 一个线程在join其他线程时会被阻塞等待,不能做自己的事情,如果不关心线程的返回值,join是一种负担,这个时候我们可以告诉系统,当线程退出时,自动释放线程资源

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

int pthread_detach(pthread_t thread)int pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable又是分离的,当一个线程被分离后还被join,此时不会阻塞等待线程终止,join会直接出错返回

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

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

相关文章

Labview_压缩文件

调用顺序 源文件 生成后的文件 1.新建ZIP文件 生成ZIP文件的路径:为最终生成ZIP文件的路径,需要提供ZIP文件的名称和类型 2.添加文件到压缩文件 源文件路径:为需要压缩的文件路径,非文件夹路径 生成ZIP文件时的路径&#x…

Elasticsearch 缓存策略详解:优化你的搜索体验

目录 1. Elasticsearch缓存机制概述 2. 缓存机制的详细解析 2.1 节点查询缓存 2.2 分片请求缓存 2.3 字段数据缓存 2.4 分片查询缓存 3. 合理缓存比例设置 3.1 节点查询缓存 3.2 分片请求缓存 3.3 字段数据缓存 3.4 分片查询缓存 4. 缓存监控与调优 5.实战调优 5…

区域特征检测工具的使用

区域特征检测工具的使用 选择区域-》右键-》工具->特征检测

实践致知第12享:如何新建一个Word并设置格式

一、背景需求 小姑电话说:要新建一个Word文档,并将每段的首行设置空2格。 二、解决方案 1、在电脑桌面上空白地方,点击鼠标右键,在下拉的功能框中选择“DOC文档”或“DOCX文档”都可以,如下图所示。 之后&#xff0…

Js- Math对象

1.对应的方法 Math.abs(x):返回 x 的绝对值。Math.ceil(x):返回大于或等于 x 的最小整数(向上取整)。Math.floor(x):返回小于或等于 x 的最大整数(向下取整)。Math.round(x):返回 x…

(图文详解)小程序AppID申请以及在Hbuilderx中运行

今天小编给大家带来了如何去申请APPID,如果你是小程序的开发者,就必须要这个id。 申请步骤 到小程序注册页面,注册一个小程序账号 微信公众平台 填完信息后提交注册 会在邮箱收到 链接激活账号 确认。邮箱打开链接后,会输入实…

一、openGauss详细安装教程

一、openGauss详细安装教程 一、安装环境二、下载三、安装1.创建omm用户2.授权omm安装目录3.安装4.验证是否安装成功5.配置gc_ctl命令 四、配置远程访问1.配置pg_hba.conf2.配置postgresql.conf3.重启 五、创建用户及数据库 一、安装环境 Centos7.9 x86openGauss 5.0.1 企业版…

nvm下载

nvm下载 1.下载nvm安装包2.安装nvm3.修改settings.txt4.安装成功5.继续配置 下载nvm之前,你最好将你电脑上的node卸载掉,直接在winx中卸载就行 1.下载nvm安装包 https://github.com/coreybutler/nvm-windows/releases 2.安装nvm 3.修改settings.txt root: E:\nvm\install\nv…

Golang | Leetcode Golang题解之第225题用队列实现栈

题目: 题解: type MyStack struct {queue []int }/** Initialize your data structure here. */ func Constructor() (s MyStack) {return }/** Push element x onto stack. */ func (s *MyStack) Push(x int) {n : len(s.queue)s.queue append(s.queu…

Elasticsearch 8 支持别名查询

在 Elasticsearch 8 中,使用 Java 高级 REST 客户端进行别名管理的过程与之前的版本类似,但有一些API细节上的变化。以下是如何使用 Java 和 Elasticsearch 8 进行别名操作的例子: 引入依赖 确保你的项目中包含了 Elasticsearch 的高级 RES…

关于利用C/C++ 利用编译器RAII机制,在多种编译器及跨平台下得兼容性问题。

在C/C 之中,我们常常利用RAII机制,来处理某个临时块得初始、及利用编译器自动析构,但这可能存在一定的致命性风险,如果你没有遇到,只是你没有过多的进行了解,挨得毒打太小,导致的。 举几个小例子…

08.C2W3.Auto-complete and Language Models

往期文章请点这里 目录 N-Grams: OverviewN-grams and ProbabilitiesN-gramsSequence notationUnigram probabilityBigram probabilityTrigram ProbabilityN -gram probabilityQuiz Sequence ProbabilitiesProbability of a sequenceSequence probability shortcomingsApproxi…

基数排序算法Python实现

1. 基数排序原理和步骤 基数排序是一种非比较型的排序算法,特别适用于处理整数或者字符串等可以分解为多个部分的数据。其基本思想是按位(或字符)进行排序,从最低有效位到最高有效位逐次排序。基数排序常分为LSD(Leas…

字节码编程javassist之生成带有注解的类

写在前面 本文看下如何使用javassist生成带有注解的类。 1:程序 测试类 package com.dahuyou.javassist.huohuo.cc;import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import ja…

MyBatisPlus实现增删改查

文章目录 MyBatisPlus实现增删改查基本操作分页查询配置分页插件 MyBatisPlus实现增删改查 实体类GkUser package com.geekmice.springbootselfexercise.entity;import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField;…

保姆级教程:Linux (Ubuntu) 部署流光卡片开源 API

流光卡片 API 开源地址 Github:https://github.com/ygh3279799773/streamer-card 流光卡片 API 开源地址 Gitee:https://gitee.com/y-gh/streamer-card 流光卡片在线使用地址:https://fireflycard.shushiai.com/ 等等,你说你不…

0基础学会在亚马逊云科技AWS上搭建生成式AI云原生Serverless问答QA机器人(含代码和步骤)

小李哥今天带大家继续学习在国际主流云计算平台亚马逊云科技AWS上开发生成式AI软件应用方案。上一篇文章我们为大家介绍了,如何在亚马逊云科技上利用Amazon SageMaker搭建、部署和测试开源模型Llama 7B。下面我将会带大家探索如何搭建高扩展性、高可用的完全托管云原…

C++线程安全队列

在 C 中,多线程队列(queue)的实现通常需要考虑线程安全问题,特别是在多个线程需要同时对队列进行操作时。C 标准库中的 std::queue 并不是线程安全的,因此我们需要引入额外的机制来确保线程安全。常用的方法是使用互斥…

FullCalendar的使用,react日历组件

1.下载 yarn add fullcalendar/core fullcalendar/react fullcalendar/daygrid 2.运行 import React from react; import FullCalendar from "fullcalendar/react"; import dayGridPlugin from "fullcalendar/daygrid";const ExperimentalSchedule () …

2024百度之星第三场第一题 数星星

天上有 n 颗星星,每颗星星自第 bi​ 秒开始(包含第 bi​ 秒),每 ai​ 秒便会闪烁一次,小度 今晚有一点失眠,所以他想来数星星,天上的星星每闪烁一次,小度便会在心中记一次数&#xf…