Linux系统编程——线程

server/2024/12/29 6:58:10/

目录

一、前言

二、线程

1、线程的理解

三、线程相关的接口

1、线程的创建

2、线程的等待

3、实验

四、总结

1、线程优点

2、线程缺点

3、线程异常

4、Linux下的进程与线程对比


一、前言

       之前的文章中我们已经对进程相关的概念做了认识,从创建进程、子进程,进程回收、进程替换等,由于我们之前对于知识框架的不熟悉,为了方便我们的理解学习,我们在之前的学习中没有做到深挖细节和深入理解,对之前的进程内容的有些部分做了简单的抽象,接下来我们学习的是比进程粒度更细的一个概念——线程

       在这部分内容中,我们除了学习主线的内容外,还会对之前的內容稍作补充和修改,这都是会在文章中提到的。

二、线程

线程和进程都是操作系统中的两种基本的执行单位,概念上有本质的区别,但在不同平台和操作系统的实现中,它们之间的界限可能并不那么明显。

  • 在某些操作系统(尤其是传统的Unix类系统,如Linux、macOS等)中,线程的管理是由内核直接负责的,线程和进程的区别非常清晰。操作系统会为每个线程提供独立的调度和执行机制。
  • 在一些早期的操作系统或特定的嵌入式平台上,线程的管理可能是由用户空间的库来完成的,操作系统本身并不直接感知线程的存在。在这种情况下,线程和进程的区分可能显得不那么重要,因为操作系统本质上只是管理进程。

1、线程的理解

  1. 线程是在进程内部的执行流
  2. 线程相比进程,粒度更细,调用的成本更低
  3. 线程是CPU调度的基本单位

我们先通过具体的例子简单认识一下线程

        在我们讲进程的那一篇文章中,我们提到过  进程=PCB+被加载到内存中的数据 对于单个进程来讲进程的PCB和程序中的数据是通过进程地址空间和页表之间的映射才能获取的。CPU实际上是通过访问进程的PCB来实现对进程的调度的

        在我们创建子进程的时候,我们知道创建的子进程在未对父进程的数据进行修改时,为了节省存储空间,创建出的子进程的程序地址空间和页表会指向和父进程一样的数据和代码,而且,我们可以 通过fork()接口的返回值的判断,让父子进程执行不同的代码所以不同的执行流,可以做到执行不同的资源,即可以做到对特定资源的划分

所以在下次创建进程的时候,操作系统并不创建有关进程的所有结构,而是 只创建PCB,将新的PCB指向已经存在的进程。如下

接着以子进程划分程序资源类似的手段将进程的代码区划分为不同的区域,并将不同的PCB设置为实际分别负责执行不同的区域的代码。

最终,不同的PCB可以访问进程地址空间内代码区的不同区域,并通过相应的页表来访问到实际的物理内存。

所以就这样在进程内部创建了多个PCB执行流,而每个PCB执行流都只能访问一小部分代码和对应的一小部分页表,那么在Linux操作系统中,我们就将这样的PCB执行流称为“线程”

上述只是介绍了以下Linux操作系统中,线程的 粗粒度 的原理。 


       基于上述的介绍,我们知道了实际上在Linux操作系统中,进程和线程是一对多的关系即 进程:线程=1:N

       我们知道操作系统对于进程的管理是通过维护各自的 PCB 将对应进程的所有属性描述组织起来,那么对于在操作系统中存在的更多的线程,毫无疑问,操作系统也需要用一个结构体将他们描述、管理、组织起来。在大多数的操作系统中,描述线程的结构体被称为 TCB

       如果一个操作系统,为了描述管理进程和线程,在内核中分别实现了不同的PCB和TCB,那么PCB和TCB之间一定存在着非常复杂的耦合关系,因为PCB描述的是一个进程,而TCB描述的是进程内部更细小的线程,所以说 这两部分之间一定存在着相当一部分重叠的属性。

       所以说在维护一个进程与其内部的线程之间的关系时,一定是一个非常复杂的过程。


        在文章刚开始我们就提到过了,不同的操作系统对于进程和线程的区分的界限是不同的,我们上面所介绍的例子都是在Linux操作系统下,不同的操作系统其线程的实现方式也是不同的。

        我们在上面说到过操作系统会为线程维护一个结构体,但是实际情况是并不是所有的操作系统都会这么做,这会使得TCB和PCB之间的关系非常复杂。

        在Linux操作系统中就没有另外实现一个描述线程的结构体,而是使用了 task_struct(进程的PCB)模拟了线程,即在Linux中描述进程和线程的实际上是一个结构体: task_struct

我们使用的Windows操作系统才是真正的将进程和线程分开,分别实现了PCB和TCB以分别用来维护线程和进程,这样的被称为 真线程操作系统

但是为什么不同的操作系统会对进程和线程之间的关系, 设计出这样的差别呢?其实是开发者对 进程和线程在执行流层面的理解不同.

  1. 以 Windows 来说, Win为了维护线程真正实现了一个不同于PCB的TCB. 也就是说, Win的开发者认为进程和线程在执行流层面是不同的东西. 进程有自己的执行流, 线程在进程内部也有自己的执行流
  2. 而 Linux 则认为 进程和线程在概念上不做区分, 都是执行流. PCB要不要被CPU调度?TCB要不要被CPU调度?PCB调度要不要优先级?TCB要不要?要不要通过PCB找到代码和数据?要不要通过TCB找到代码和数据?进程切换要不要保护进程的上下文数据?线程切换要不要保护上下文数据?……在Linux看来, 种种迹象表明 PCB和TCB的功能 不从更细节来细分的话, 其实是大致相同的. 无非就是PCB和TCB中描述的代码量和数据量的不同, 所以 进程和线程都只看成一个执行流.

这么做的好处是:

用进程PCB模拟实现线程, 对线程 可以复用操作系统中已经针对进程实现的各种调度算法, 因为进程和线程的描述结构是相同的.

也不用维护进程和线程之间的关系.

也就是说, Linux操作系统中 线程TCB底层就可以看作进程PCB

TipsLinux复用PCB实现TCB, 那么从CPU的角度看待线程, 其实与进程没有区别. CPU调度线程实际上看到的还是PCB(task_strcut)

虽说Linux操作系统中的线程使用了进程的PCB模拟实现的,但是其在设计时已经考虑到了线程,也就是说在PCB内部其实是有用来表示线程的结构的

thread_struct{}结构体内部存储的大部分都是寄存器相关信息. 与维护不同线程的上下文数据有关系 


既然上面已经提到了Linux操作系统的线程也是用task_struct来实现的,那么我们现在该如何理解进程呢?

此时我们就需要改变一下我们之前对进程的认识了不能单纯只认为进程就是一个PCB和代码数据的和,而是如下图中所包含的所有结构合起来才能称为进程。

        在我们知道线程之前,我们所理解的进程就是只有一个执行流,即只有一个task_struct,但是现在我们将只包含一个执行流的进程称为 单执行流进程,称 内部存在多个执行流的进程为 多执行流进程

        所以现在我们返回来再次理解task_struct时,我们会知道我们当前知道的task_struct比之前所认识的task_struct体量要小,因为我们理解现在CPU所看到的task_struct可能是线程,即 轻量化的进程

 学到了这里我们可以说:

  1. 进程是承担操作系统 资源分配 的基本实体,即进程是向系统申请资源的基本单位。
  2. CPU调度是通过PCB(task_struct)调度的,所以线程是CPU调度的基本单位

总结:

  • 线程是进程内部运行的执行流,只访问执行进程的一部分代码和数据。
  • 线程和进程相比粒度更细、调用成本更低,进程切换调度需要切换PCB、进程地址空间、页表等。而线程切换调度,只需要切换TCB(在Linux下还是PCB)
  • 线程是CPU调度的基本单位 

三、线程相关的接口

1、线程的创建

线程的创建和查看也有系统的调用接口:pthread_create

该接口的作用是创建一个新线程

  • 第一个参数是此类型的指针,是一个输出型参数,用于获取创建的线程的id
  • 第二个参数是线程属性结构体的指针,现在我们只需要知道传入的是nullptr
  • 第三个参数是一个函数指针,参数和返回值都是空指针,用于传入此线程需要执行的函数
  • 第四个参数是一个空指针,其实就是第三个参数(函数指针)所指向的函数的参数

2、线程的等待

与子进程一样,线程也需要等待,pthread_join

该接口的作用是 join一个终止的进程 ,即等待指定的终止的线程。

  • 第一个参数需要传入的是 需要等待的线程的id
  • 第二个参数接收线程退出的结果,暂时不关心它,我们先看实验现象

3、实验

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>using std::cout;
using std::endl;
using std::cerr;
using std::string;void* pthreadFun1(void* argc){//定义线1执行的函数string str=(char*)argc;while(true){cout<<str<<"My pid::"<<getpid()<<endl;sleep(1);}}
void* pthreadFun2(void* argc){//定义线程2执行的函数string str=(char*)argc;while(true){cout<<str<<"My pid::"<<getpid()<<endl;sleep(1);}
}int main()
{pthread_t pth1,pth2;pthread_create(&pth1,nullptr,pthreadFun1,(void*)"I am pthread1!");pthread_create(&pth2,nullptr,pthreadFun2,(void*)"I am pthread2!");sleep(1);while(true){cout<<"Main pthread has gone!"<<"My pid::"<<getpid()<<endl;sleep(1);}pthread_join(pth1,nullptr);pthread_join(pth2,nullptr);return 0;
}

在编译生成可执行函数的时候特别要注意的是  我们需要手动链接 pthread库 ,因为其是第三方库

我们可以在进程运行的时候查看系统的进程表,如下 

 可以看到相关的进程只有一个,当然除了查看进程,线程也有查看的方法 ps -aL(a:all,L:轻量级进程)

可以看到线程列表中存在着三个相同的pid的线程,这三个线程同时属于一个 PID31660, 同时他们还有着自己的 LWP 轻量级进程编号

有一个的PID和LWP是相同的,这个线程是主线程。

四、总结

1、线程优点

  • 资源共享:同一个进程中的所有线程共享该进程的资源,包括全局变量、堆栈和文件描述符。这使得线程之间的通信和数据交换更加高效。

  • 创建和切换开销小:相比于进程,线程的创建和销毁成本更低,因为不需要像进程那样分配独立的地址空间和其他资源。同样,线程之间的上下文切换也比进程间的切换更快。

  • 简化编程模型:对于某些类型的应用程序,尤其是那些需要并行执行多个任务的应用程序,使用线程可以简化编程逻辑,使代码更易于理解和维护。

  • 充分利用多核处理器:现代计算机通常具有多核CPU,线程可以通过并行运行来充分利用这些硬件资源,从而提高应用程序的性能。比如一个程序运行时, 需要等待操作系统和网卡之间的I/O操作, 又要等待操作系统和磁盘之间的I/O操作.如果单线程的话, 这两个I/O操作只能一个一个等, 不过, 如果是多线程的话就可以同时等待不用排队.

  • 细粒度控制:线程允许对每个任务进行更细粒度的控制,例如可以单独设置优先级或调度策略。

  • 对于计算密集型应用, 为了能在多处理器系统上运行, 会将计算分解到多线程去实现。

2、线程缺点

  1. 线程安全问题:由于线程共享相同的地址空间,如果一个线程修改了共享的数据结构,而没有适当的同步机制,可能会导致其他线程看到不一致的状态,产生竞态条件(race condition)。

  2. 调试困难:线程错误往往难以重现和诊断,因为它们依赖于特定的执行顺序和时间点。此外,调试工具可能无法很好地支持多线程环境下的调试。

  3. 死锁风险:当两个或更多的线程相互等待对方持有的资源时,会发生死锁。设计良好的同步机制和避免循环等待是防止死锁的关键。

  4. 增加复杂性:虽然线程可以简化一些编程场景,但引入多线程也会增加程序的复杂性,特别是在处理同步和通信方面。

  5. 受限于单进程限制:线程属于同一个进程,因此受到操作系统对该进程施加的任何限制的影响,比如打开文件的数量或者可用内存大小。

  6. 非阻塞操作的需求:在一个线程中执行长时间运行的操作(如I/O操作)会阻塞整个线程,影响其他线程的执行效率。为了解决这个问题,通常需要采用异步I/O或者其他非阻塞技术。

3、线程异常

一个多线程进程中, 虽然一般每个线程访问执行的代码和数据不同, 但这些代码和数据都是属于整个进程的, 只有一份.

如果线程出现了异常, 那就说明什么?就说明是进程某处代码出现了异常.

也就是所, 线程出现异常是会影响整个进程 的. 线程出现异常其实就是进程出现了异常.

线程出现异常, 操作系统就会像线程发送信号, 然后会将整个进程终止. 整个进程终止, 进程中的其他所有线程也会退出.

4、Linux下的进程与线程对比

  • 进程是系统资源分配的基本单位
  • 线程是调度的基本单位
  • 进程和线程共享的资源
  1. 内存地址空间:进程中的所有线程共享同一块虚拟内存地址空间。这意味着它们可以访问相同的全局变量、静态数据以及动态分配的内存(如通过malloc()new操作符分配的堆内存)。这也使得线程间的通信变得简单高效。
  2. 文件描述符:包括打开的文件、套接字、管道等在内的所有文件描述符都是由进程创建并管理的,因此所有线程都可以读写这些文件描述符。这包括标准输入输出流(stdin, stdout, stderr)以及其他任何已打开的文件或网络连接。
  3. 打开的设备:如果进程打开了某些硬件设备(如打印机、串行端口等),那么所有线程都能够与这些设备进行交互。
  4. 信号处理函数:所有线程共享同一组信号处理器。当一个信号被递送给进程时,它会被传递给其中一个没有屏蔽该信号的线程执行相应的信号处理程序。
  5. 环境变量
  6. 当前工作目录
  7. 用户ID和组ID
  • 多线程共享进程数据,不过不同的线程也有着自己的一份数据
  1. 线程id:在Linux系统中,每个线程确实有其自己的标识符。这个标识符被称为轻量级进程(Light Weight Process, LWP)ID,有时也被称作线程ID(TID, Thread ID)。LWP和传统的“进程”概念不同,它是操作系统内核用来管理线程的一种方式。
  2. 一组寄存器:每个线程都有自己的一组寄存器用来维护线程的上下文数据。为了确保当操作系统在多个线程之间切换时,每个线程都能保持其执行状态,并且可以在被重新调度时从上次中断的地方继续执行。
  3. 线程栈:每个进程在运行时都会有自己的栈结构,用于管理函数调用、局部变量、参数传递和返回地址等。
  4. 信号屏蔽字:线程异常 就是 进程异常. 线程异常操作系统会向线程发送信号,虽然线程和进程在信号处理上有共享的部分,但每个线程都有自己独立的信号屏蔽机制,这使得它们可以在一定程度上控制信号的接收行为。这种设计既保持了进程级别的统一性,又给予了线程一定的灵活性。
  5. 调度优先级


http://www.ppmy.cn/server/154125.html

相关文章

DevOps工程技术价值流:Ansible自动化与Semaphore集成

在DevOps的浪潮中&#xff0c;自动化运维工具扮演着举足轻重的角色。Ansible&#xff0c;作为一款新兴的自动化运维工具&#xff0c;凭借其强大的功能和灵活性&#xff0c;在运维领域迅速崭露头角。本文将深入探讨Ansible的特点、架构、工作原理&#xff0c;以及其应用场景&…

SDK 指南

在前端开发中&#xff0c;SDK&#xff08;Software Development Kit&#xff0c;软件开发工具包&#xff09;是一个用于帮助开发者在特定平台、框架或技术栈中实现某些功能的工具集。 1. SDK 是什么&#xff1f; SDK 是一种开发工具包&#xff0c;它提供了开发人员实现某些功…

破解 JVM 上的第三方 API

JVM 生态系统已经成熟&#xff0c;提供了很多库&#xff0c;因此您无需重新发明轮子。基本功能&#xff08;以及不那么基本的功能&#xff09;只需依赖即可实现。然而&#xff0c;有时依赖项和您的用例略有不一致。 解决此问题的正确方法是创建一个 Pull 请求。但您的截止日期…

PyTorch transpose、permute、view和einops.rearrange

einops.rearrange 和 PyTorch 中的 transpose、permute 以及 view 都用于张量的操作,但它们的功能侧重和用法各有不同。以下是它们的详细比较和区别: 1. 功能和用途对比 功能/操作einops.rearrangetorch.transposetorch.permutetorch.view维度交换支持直接通过模式字符串交换…

AndroidKMP跨平台开发基础1-编译发布

目录 跨平台发布Androidwindows desktop发布流程发布参数修改 跨平台发布 Android 和以往打包JetpackCompose项目一样&#xff0c; 找到并点击菜单栏的 build->build app bundle/apk->build apk 编译完毕后会自动右下角弹出小窗&#xff0c;点击locate即可打开apk文件…

3D坐标下,一点在某一线段上的左右方向的判定

3D坐标下&#xff0c;一点在某一线段上的左右方向的判定 代码 代码 #include <iostream> #include <Eigen/Dense>#define M_PI 3.1415926// 计算三点组成平面的参数和变换到XOY平面的变换矩阵 void computePlaneAndTransform(const Eigen::Vector3d& P1, cons…

Visual Studio 2017 配置 OpenCV 4.5.5 及二次配置的导入

重点参考&#xff1a; Visual Studio 2017 OpenCV_4.5.0安装_opencv4.5.0下载-CSDN博客 VS2017配置OpenCV4.5_vs2017 opencv4.5.4-CSDN博客 下载准备工作就不说了&#xff0c;直接从官网下载就行了。 关键就两步&#xff1a; 1&#xff09;将OpenCV的bin目录添加到环境变量…

leetcode热题100(240. 搜索二维矩阵 II)c++

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。每列的元素从上到下升序排列。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,2…