Linux下的多线程

前面学习了进程、文件等概念,接下里为大家引入线程的概念

多线程

  • 线程是什么?
  • 为什么要有线程?
  • 线程的优缺点
  • Linux线程操作
    • 线程创建
    • 线程等待
    • 线程终止
    • 线程分离
  • 线程间的私有和共享数据
  • 理解线程库和线程id
  • 深刻理解Linux多线程(重点)

线程是什么?

  • 线程是一个执行分支,执行粒度比进程更细,调度成本更低。
  • 线程是进程内部的一个执行流
  • 线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体

为什么要有线程?

多线程编程在现代计算中具有显著的优势,主要体现在以下几个方面:

  1. 发挥多核CPU优势:
    现代处理器普遍拥有多个核心,多线程能够确保这些核心得到充分利用。通过创建多个线程,程序可以同时在不同核心上执行不同的任务部分,从而提升并行处理能力,整体提高程序的运行效率。
  2. 防止阻塞和提高响应性:
    当一个线程在等待IO操作(如磁盘读写、网络通信)或执行耗时计算时,操作系统可以调度其他线程继续执行,避免了整个进程停滞,提高了系统的响应速度。
  3. 并发处理能力增强:
    多线程使得系统能够并发地处理多个用户请求或执行多个独立的任务。这对于高并发场景下的服务器应用、实时数据处理、大规模并行计算等尤为关键,可以显著提升系统吞吐量和服务质量。
  4. 模块化与简化设计:
    在复杂的软件架构中,多线程有助于将大的任务分解成若干个可管理的子任务,并分配给不同的线程来处理。这不仅使得代码逻辑更加清晰,也更容易进行模块化开发和维护。
  5. 改善用户体验:
    用户界面应用程序中,主线程负责处理UI事件,后台线程则可以处理长时间运行的操作(例如加载数据、文件压缩解压)。这样可以在不影响用户界面交互的同时完成大量工作,提升了用户体验。
  6. 资源利用率:
    相对于为每个并发任务创建新的进程,线程间的切换开销较小,因为它们共享同一进程的地址空间和其他资源。这降低了系统资源消耗,尤其是在内存有限的情况下。

然而,多线程编程也带来了一系列挑战,包括但不限于:资源共享引发的数据竞争问题、死锁、优先级反转等并发控制难题。因此,在享受多线程带来的性能提升时,开发者需要精心设计和实现线程间同步机制,以保证程序的正确性和稳定性。

线程的优缺点

优点:

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

缺点:

  • 性能损失
    一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的 同步和调度开销,而可用的资源不变。
  • 健壮性降低
    编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。(一个线程崩溃了,系统发送信号是以进程为单位的,所以整个进程都会崩溃。)
  • 缺乏访问控制
    进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响(多线程共享地址空间)。

Linux线程操作

使用Linux线程接口的时候,我们需要导入线程库。例如:

g++ -o main main.cpp -std=c++11 -lpthread

这个pthread就是Linux自带的线程库,但是我们上面线程概念提到了Linux没有真正的线程,而是用进程模拟的线程(LWP),所以Linux也没有真正的线程接口,Linux提供的是轻量级进程的系统接口,然后对这个接口进行封装,封装成线程库,在用户的角度看这就是线程控制的接口,从而完成对线程的控制。(任何操作系统都会自带线程库)

线程创建

线程创建使用pthread_create这个函数接口,需要包含头文件pthread.h
在这里插入图片描述
参数解析:

  • pthread_t *thread //线程id
  • const pthread_attr_t *attr //设置线程属性,一般为nullptr
  • void *(start_routine) (void ) //回调函数,会执行传入的函数指针(返回值void,参数void
  • void *arg //可以作为函数参数

线程等待

在主线程调用这个函数会让主线程阻塞等待线程结束,并不是所有的情况都必须调用这个函数。
在这里插入图片描述
传入对应的线程id,并且阻塞等待。第二个参数是一个输出型参数,用于接收线程返回值。

线程终止

线程终止可能由以下几种情况造成:

  1. 一个线程正常情况下结束有可能是线程函数执行结束了,return void*。
  2. 还有一种就是调用这个函数,线程调用这个函数让自己退出。
    在这里插入图片描述
  3. 主线程调用这个函数就可以取消正在执行的线程。返回值为-1。
    在这里插入图片描述

线程分离

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

分离线程的接口,只需要传入线程id就可以了。
在这里插入图片描述
线程分离之后是不可以join的,join会返回报错码,线程分离是一种属性状态,如果主线程join等待某个线程,会查看这个线程的状态是否是分离的。如果是分离状态就会直接报错。如果先join线程,再分离线程,会检测不到。

例如这样就会报错

void *run_thread(void *args){
	//pthread_detach(pthread_t pthread_self()); //可以自己分离自己
    int cnt = 5;
    while(cnt)
    {
        cout<<"我是线程:"<<cnt--<<endl;
        sleep(1);
    }
}
int main(){
    pthread_t t1;
    pthread_create(&t1,nullptr,run_thread,nullptr);
    pthread_detach(t1);//可以再主线程里面分离

    int n = pthread_join(t1,nullptr);
    if(n!=0)//如果n!=0表示阻塞等待失败,打印返回的错误码
    {
        cout<<"error:"<<n<<":"<<strerror(n)<<endl;
    }
    return 0;
 }

在这里插入图片描述

线程间的私有和共享数据

线程共享进程数据,但是也拥有自己的一部分数据:

  • 线程ID
  • 一组寄存器
  • errno
  • 信号屏蔽字
  • 调度优先级

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

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

理解线程库和线程id

我们学习的Linux里面其实是没有真正意义上的线程的,Linux用进程模拟的线程。所谓的线程就是一个轻量级的进程,所以Linux提供了对轻量级进程操作的接口,而我们用的线程库就是对这些接口进行了封装。从用户的角度是对线程进行操作,但是从OS的角度是对轻量级进程的操作。

这个线程库是一个用户级的动态库,所以进程想要用这个库,必须先加载到内存,然后映射到进程地址空间的共享区中。

这个线程库里面可能会创建很多个线程,所以需要对线程进行管理,所以先描述,再组织。每个线程都有一个结构体来对这些线程进行统一管理。
在这里插入图片描述

线程库中有很多个线程,每个线程都是向数组一样排列,每个线程都有一个起始地址,这个起始地址就是线程id,把这个线程id传给其他线程,就可以获取该线程的属性信息。

每个线程都拥有自己的栈结构,主线程用的是地址空间的栈。

深刻理解Linux多线程(重点)

当一个进程创建子进程时,需要创建PCB、进程地址空间、页表等。是非常独立的。一个进程内可以有多个线程,那么线程是怎么创建的呢?

  • 给一个进程创建线程时,只会创建一个PCB,这个PCB还是会指向这个进程,进程地址空间内部有代码区,每个线程指向代码区中不同的代码区域,一个线程对应一个函数代码,这样一个线程就是一个执行流。这就是为什么线程是进程内部的一个执行流。
  • 线程执行粒度更细因为可以执行进程中不同的代码,控制粒度更细。
  • 调度成本更低因为PCB中存放进程地址空间的地址,当CPU切换PCB时,发现不用切换加载进程地址空间以及页表等一系列操作。但是最重要还是不用切换cache(cache是一个集成在CPU里面的硬件,也叫做高速缓存器,当我们访问内存中的代码的时候,会预先加载一部分代码到cache中,减少IO,提升效率,这个也叫做局部性原理。CPU切换的线程如果是同一个进程,cache不用切换数据,使得切换成本更低)。
  • 之前谈到进程是CPU的基本调度单位,因为之前谈论的进程都是一个执行流,一个进程只有一个PCB,而多线程这里,一个进程有多个PCB,也就是多个执行流,对于CPU来说其实调度切换的是进程还是线程,CPU并不知道,也并不重要,CPU只需要可以通过PCB访问进程地址空间,通过页表映射到内存就可以了。
    但是并不是所有的操作系统都是这样设计多线程的,这种是Linux下的多线程,而windows的里面线程和进程是不同的,Windows下的线程叫做TCB(线程控制块),而线程是进程的一个执行流,必须遵守执行粒度更细,调度成本更低,所以Windows的设计是比较复杂的。Windows里面是有真线程的,Linux下则是用进程的方案去模拟线程,所以Linux没有真正意义上的线程,都叫做轻量级进程(LWP)。Linux对比Windows复用代码结构,更简单,好维护,效率更高。

一个进程里面有多个执行流,有一个执行流是主执行流。每个进程都有一个pid,一个进程里面有多个执行流,每个执行流的pid当然都是一样的,但是每个执行流都有一个LWP是不一样的,CPU根据LWP来进行基本的调度。切换执行流,如果pid不变证明还是一个进程,不需要切换地址空间、cache等操作,如果pid变了,证明不是同一个进程了。

代码证明多线程有多个LWP,有同一个pid

void *run_thread1(void *args){
    while(true)
    {
        cout<<"我是执行流1:"<<*((int*)args)<<endl;
        sleep(1);

    }
}
void *run_thread2(void *args){
    while(true)
    {
        cout<<"我是执行流2:"<<*((int*)args)<<endl;
        sleep(1);

    }
}
void *run_thread3(void *args){
    while(true)
    {
        cout<<"我是执行流3:"<<*((int*)args)<<endl;
        sleep(1);
    }
}
int main(){
    pthread_t t1,t2,t3;
    int th1=1;
    pthread_create(&t1,nullptr,run_thread1,&th1);
    int th2=2;
    pthread_create(&t2,nullptr,run_thread2,&th2);
    int th3=3;
    pthread_create(&t3,nullptr,run_thread3,&th3);

    while(true)
    {
        cout<<"我是主执行流"<<endl;
        sleep(1);
    }
    return 0;
}

执行结果
在这里插入图片描述
这段代码使用了一下多线程,证明一个线程有一个LWP。主执行流的LWP和进程的pid是相同的,所以之前学习进程说的CPU根据pid调度进程也是对的。

这就是Linux下的多线程,后续会更新互斥和同步的文章,多多支持。

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

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

相关文章

2023年全国职业院校技能大赛软件测试赛题第3套

2023年全国职业院校技能大赛 软件测试赛题第3套 赛项名称&#xff1a; 软件测试 英文名称&#xff1a; Software Testing 赛项编号&#xff1a; GZ034 归属产业&#xff1a; 电子与信息大类 …

软件价值10-数字时钟

这是一个数字时钟程序&#xff1a; # importing whole module from tkinter import * from tkinter.ttk import *# importing strftime function to # retrieve systems time from time import strftime# creating tkinter window root Tk() root.title(Clock)# This functio…

Docker Compose实例

目录 一、前提说明 二、简单的Docker容器部署案例 1. Dockerfile 配置 2. docker-compose.yml 配置 3. application.properties 配置 4. pom.xml 配置 5. 上传文件 6. 创建基础Docker镜像 7. docker-compose.yml编排 8. 停止并删除容器编排 一、前提说明 在配置好Do…

【博云2023】乘龙一跃腾云海,侧目抬手摘星河

癸卯渐远&#xff0c;甲辰渐至&#xff0c;预示着被汗水浇灌的种子&#xff0c;必将顶开冻土&#xff0c;迎接阳光。 每逢春节&#xff0c;当亲友彼此问候&#xff0c;博云人总能自豪地说&#xff0c;我们认真地、努力地奋斗&#xff0c;让我们能自信地踏上新的征程。 我们的…

使用Python进行数据的描述性分析,用少量的描述性指标来概括大量的原始数据

在进行数据分析时&#xff0c;当研究者得到的数据量很小时&#xff0c;可以通过直接观察原始数据来获得所有的信息。但是&#xff0c;当得到的数据量很大时&#xff0c;就必须借助各种描述性指标来完成对数据的描述工作。用少量的描述性指标来概括大量的原始数据&#xff0c;对…

用户和文件权限管理

一、用户管理 1、创建用户 [rootmaster ~]# useradd maple [rootmaster ~]# ll /home total 0 drwx------ 2 maple maple 62 Feb 7 20:47 maple drwx------ 2 www www 62 Jan 17 21:05 www [rootmaster ~]# passwd maple Changing password for user maple. New passwor…

Swift Combine 发布者publisher的生命周期 从入门到精通四

Combine 系列 Swift Combine 从入门到精通一Swift Combine 发布者订阅者操作者 从入门到精通二Swift Combine 管道 从入门到精通三 1. 发布者和订阅者的生命周期 订阅者和发布者以明确定义的顺序进行通信&#xff0c;因此使得它们具有从开始到结束的生命周期&#xff1a; …

深度学习系列56:使用whisper进行语音转文字

1. openai-whisper 这应该是最快的使用方式了。安装pip install -U openai-whisper&#xff0c;接着安装ffmpeg&#xff0c;随后就可以使用了。模型清单如下&#xff1a; 第一种方式&#xff0c;使用命令行&#xff1a; whisper japanese.wav --language Japanese --model…

12. BI - 可视化在项目蒸汽量预测的过程及应用

本文为 「茶桁的 AI 秘籍 - BI 篇 第 12 篇」 文章目录 工业蒸汽量预测 Hi, 你好。我是茶桁。 我们今天继续来看数据可视化做数据探索&#xff0c;今天我们还是来看相关项目。来看看可视化 EDA 在项目中的应用。 工业蒸汽量预测 接下来这个项目&#xff0c;是在阿里天池上的一…

c++之说_12|模板

关于模板&#xff0c;至少我们要先了解几个概念 一&#xff1a;函数模板 二&#xff1a;类模板 三&#xff1a;模板特化 四&#xff1a;形参参数包 模板覆盖的东西太多 我目前也不了解太多 函数模板 语法 template<typename 类型名,typename 类型名,typename ...多参…

006集——where语句进行属性筛选——arcgis

在arcgis中&#xff0c; dBASE 文件除了 WHERE 语句以外&#xff0c;不支持 其它 SQL 命令。选择窗口如下&#xff1a; 首先&#xff0c;我们了解下什么是where语句。 WHERE语句是SQL语言中使用频率很高的一种语句。它的作用是从数据库表中选择一些特定的记录行来进行操作。WHE…

uniapp小程序端使用计算属性动态绑定style样式踩坑

踩坑点: 使用uniapp编译小程序端动态绑定复杂style使用计算属性方式&#xff0c;return必须返回json字符串格式&#xff0c;不能返回object&#xff0c;否则会不起作用。 代码总览 视图层 逻辑层&#xff08;注意这里是使用的计算属性哈&#xff09; 这里我封装成了一个个性化…

安全名词解析-水坑攻击、鱼叉攻击、钓鲸攻击

为方便您的阅读&#xff0c;可点击下方蓝色字体&#xff0c;进行跳转↓↓↓ 01 水坑攻击02 鱼叉攻击03 钓鲸攻击 01 水坑攻击 水坑攻击&#xff08;Watering Hole Attack&#xff09;是一种针对特定目标的网络攻击方式。顾名思义&#xff0c;是在受害者必经之路设置了一个“水坑…

PyTorch 2.2大更新!集成FlashAttention-2,性能提升2倍

【新智元导读】新的一年&#xff0c;PyTorch也迎来了重大更新&#xff0c;PyTorch 2.2集成了FlashAttention-2和AOTInductor等新特性&#xff0c;计算性能翻倍。 新的一年&#xff0c;PyTorch也迎来了重大更新&#xff01; 继去年十月份的PyTorch大会发布了2.1版本之后&#…

Octave实现位置式PID算法

由于Matlab不让用&#xff0c;只能“你不让爷用&#xff0c;爷就用别的”&#xff0c;选择开源的Octave以及scilab进行相关领域的学习。Octave的代码和Matlab几乎是100%相同的&#xff0c;只有一些专用的包的函数&#xff0c;可能有些还没来得及写&#xff0c;或者有些差异。但…

NLP_语言模型的雏形 N-Gram 模型

文章目录 N-Gram 模型1.将给定的文本分割成连续的N个词的组合(N-Gram)2.统计每个N-Gram在文本中出现的次数&#xff0c;也就是词频3.为了得到一个词在给定上下文中出现的概率&#xff0c;我们可以利用条件概率公式计算。具体来讲&#xff0c;就是计算给定前N-1个词时&#xff0…

Redis篇之redis是单线程

一、redis是单线程 Redis是单线程的&#xff0c;但是为什么还那么快&#xff1f;主要原因有下面3点原因&#xff1a; 1. Redis是纯内存操作&#xff0c;执行速度非常快。 2. 采用单线程&#xff0c;避免不必要的上下文切换可竞争条件&#xff0c;多线程还要考虑线程安全问题。 …

硬件大熊原创合集(2024/01更新)

则2024-01月份更新篇章&#xff1a; NFC芯片WS1850的9个典型问题-篇章1 凡夫畏果&#xff0c;菩萨畏因 赶在春节前&#xff0c;做个红包封面&#xff0c;一轮操作下来&#xff0c;发现自己在美工设计这块实在是太欠缺&#xff0c;还好图像处理AI出来了&#xff0c;用Midijourne…

软考 系统分析师系列知识点之信息系统战略规划方法(3)

接前一篇文章&#xff1a;软考 系统分析师系列知识点之信息系统战略规划方法&#xff08;2&#xff09; 所属章节&#xff1a; 第7章. 企业信息化战略与实施 第4节. 信息系统战略规划方法 7.4.1 企业系统规划法 7. 确定管理部门对系统的要求 BSP的出发点是管理部门对系统的要…