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下的多线程,后续会更新互斥和同步的文章,多多支持。

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

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

相关文章

AI大模型开发架构设计(9)——AI 编程架构刨析和业务应用实战案例

文章目录 AI 编程架构刨析和业务应用实战案例1 AI编程代码生成模型剖析编程方式的发展代码自动生成基于大模型的AI编程工具——Github Copilot以 CodeGeeX 为例-发展过程以 CodeGeeX 为例-训练过程以 CodeGeeX 为例-大规模代码数据处理以 CodeGeeX 为例-模型结构以 CodeGeeX 为…

【开源】基于JAVA+Vue+SpringBoot的新能源电池回收系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 用户档案模块2.2 电池品类模块2.3 回收机构模块2.4 电池订单模块2.5 客服咨询模块 三、系统设计3.1 用例设计3.2 业务流程设计3.3 E-R 图设计 四、系统展示五、核心代码5.1 增改电池类型5.2 查询电池品类5.3 查询电池回…

this指针剖析

this指针 this指针引出&#x1f4a6;特性&#x1f4a6;this指针存储位置 this指针 引出&#x1f4a6; 我们先来定义一个日期类 Date class Date { public: void Init(int year, int month, int day) { _year year; _month month; _day day; } void Print() { cout <<…

spring Bean 生命周期 源代码分析

文章目录 一、生命周期关键步骤1.1 前置条件1.2 创建bean 二、Bean生命周期、核心源码分析2.1 前置条件, 源代码2.2 创建bean, 源代码 一、生命周期关键步骤 1.1 前置条件 1.创建rootBean 生成RootBeanDefinition 2.对bean定义的方法&#xff0c;进行验证、重写 调用方法pre…

5G NR 频率计算

5G中引入了频率栅格的概念&#xff0c;也就是小区中心频点和SSB的频域位置不能随意配置&#xff0c;必须满足一定规律&#xff0c;主要目的是为了UE能快速的搜索小区&#xff1b;其中三个最重要的概念是Channel raster 、synchronization raster和pointA。 1、Channel raster …

用的到的linux-查找find-Day4

前言&#xff1a; 在上一节&#xff0c;我们了解到rm删除命令&#xff0c;一共拥有三种模式&#xff0c;即-i默认只能删除文件且会提示确认&#xff0c;其次是-r 遍历删除&#xff0c;用于删除目录及目录下的文件&#xff0c;同样需确认后才会删除&#xff0c;最后为-f为强制删…

猫头虎分享已解决Bug || TypeError: Cannot set property ‘innerHTML‘ of null

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

网络学习:数据链路层VLAN原理和配置

一、简介&#xff1a; VLAN又称为虚拟局域网&#xff0c;它是用来将使用路由器的网络分割成多个虚拟局域网&#xff0c;起到隔离广播域的作用&#xff0c;一个VLAN通常对应一个IP网段&#xff0c;不同VLAN通常规划到不同IP网段。划分VLAN可以提高网络的通讯质量和安全性。 二、…

探索C语言中的联合体与枚举:数据多面手的完美组合!

​ ✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;C语言学习 贝蒂的主页&#xff1a;Betty‘s blog 1. 联合体的定义 联合体又叫共用体&#xff0c;它是一种特殊的数据类型&…

leetCode二叉树的堂兄弟节点

题目描述 在二叉树中&#xff0c;根节点位于深度 0 处&#xff0c;每个深度为 k 的节点的子节点位于深度 k1 处。 如果二叉树的两个节点深度相同&#xff0c;但 父节点不同 &#xff0c;则它们是一对堂兄弟节点。 我们给出了具有唯一值的二叉树的根节点 root &#xff0c;以…

从概念到实践:数字孪生在智慧城市建设中的应用与挑战

一、引言 随着科技的飞速发展&#xff0c;数字孪生这一概念逐渐受到广泛关注。数字孪生通过建立物理世界的数字模型&#xff0c;为城市的管理和规划提供了前所未有的可能性。智慧城市作为现代城市发展的重要趋势&#xff0c;正逐渐成为人们关注的焦点。本文将探讨数字孪生在智…

编译原理实验3——自下而上的SLR1语法分析实现(包含画DFA转换图、建表、查表)

文章目录 实验目的实现流程定义DFA状态实现代码运行结果测试1测试2测试3 总结 实验目的 实现自下而上的SLR1语法分析&#xff0c;画出DFA图 实现流程 定义DFA状态 class DFA:def __init__(self, id_, item_, next_ids_):self.id_ id_ # 编号self.item_ item_ # productio…