Linux线程详解(概念、原理、实现方法、优缺点)

文章目录

  • 一、Linux线程基本概念
  • 二、Linux内核线程实现原理
  • 三、创建线程
  • 四、线程的优缺点

一、Linux线程基本概念

linux中,线程又叫做轻量级进程(light-weight process LWP),也有PCB,创建线程使用的底层函数和进程底层一样,都是clone,但没有独立的地址空间;而进程有独立地址空间,拥有PCB。
Linux下:线程是最小的执行单位,调度的基本单位。进程是最小分配资源单位,可看成是只有一个线程的进程。
线程是一个进程内部的控制序列。控制序列可以理解为一个执行流。进程内部是指虚拟地址空间。

1、线程特点:
(1)线程是资源竞争的基本单位。
操作系统有很多资源。进程与进程之间要竞争操作系统资源,当一个进程申请得到一大堆资源。而这些资源又会分配给线程。一个进程内部有多个线程,去竞争进程所获得的资源。所以说线程是资源竞争的基本单位。
(2)线程是程序执行的最小单位
当用户让进程去执行某个任务时,进程又会将任务细化。进程内部有很多线程,让这些线程去执行
(3)线程共享进程数据,但也拥有自己独立的一部分数据: 线程ID ,一组寄存器,栈,errno值,信号。
其中最重要的数据是栈和寄存器。私有栈是为了保存临时变量,便于函数调用等操作。私有寄存器是为了方便线程切换,保存上下文。

2、进程到线程:
进程:承担分配系统资源的实体
线程:共享进程所获得资源
在这里插入图片描述

二、Linux内核线程实现原理

创建线程使用的底层函数和进程一样,都是clone。从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的。进程可以蜕变成线程。线程可看做寄存器和栈的集合。
三级映射:进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。但线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。
因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
Linux线程详解(概念、原理、实现方法、优缺点)_第1张图片

三、创建线程

1、调用函数:
Linux线程详解(概念、原理、实现方法、优缺点)_第2张图片
返回值:成功返回0,失败返回错误码。

2、pthread_create函数
创建一个新线程。其作用对应进程中fork() 函数。Linux环境下,所有线程特点,失败均直接返回错误号。
在这里插入图片描述
参数说明:
(1)thread :
pthread_t 类型的指针,线程创建成功的话,会将分配的线程 ID 添入该指针指向的地址。线程后续的操作将该值作为线程的唯一标识。
(2)attr :
pthread_attr_t 类型,通过该参数可以定制线程属性,比如可以指定新建线程栈空间的大小,调度策略等。如果要创建的线程无特殊要求,该值设置成 NULL,标识采用默认属性。
(3)start_routine :
线程需要执行的函数。创建线程是为了让线程执行特定的任务。线程创建成功之后,该线程就会执行 start_routinue 函数,该函数之于线程,就如同 main 函数之于主线程。
(4)arg :
线程执行 start_routine 函数的参数。当执行函数需要传入多个参数时,线程创建者(一般是主线程)和新建线程约定一个结构体,创建者把信息填入该结构体,再把结构体的指针传给新建线程,新建线程只要解析这个结构体,就能获取到需要的所有参数。
在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态。

3、线程ID:
pthread_create 函数会产生一个 pthread_t 类型的线程 ID,存放在第一个参数指向的空间内。这里的线程 ID 和前面提到的 pid_t 类型的线程 ID 我们该如何去定位或者看待呢?
pid_t 类型的线程 ID :属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来在整个操作系统内唯一标识该线程。
pthread_t 类型的线程 ID :属于NPTL线程库的范畴,线程库的后续操作,就是根据该线程 ID 来操作线程的。对于 Linux 目前使用的 NPTL 实现而言,pthread_t 类型的 ID 本质上是进程地址空间上的一个地址。
(1)pthread_self 函数:可以获取到线程自身的 ID。其作用对应进程中 getpid() 函数。
pthread_t pthread_self(void); 返回值:成功:0; 失败:无!
在这里插入图片描述
线程ID:pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现。线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)。注意:不应使用全局变量 pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self。
(2)pthread_equal函数:
在同一个线程组内的,线程库提供了接口,可以判断两个线程 ID 是否对应着同一个线程:
在这里插入图片描述
返回值是 0 的时候,表示是同一个线程,非 0 则表示不是同一个线程。

4、pthread_exit函数:
参数value ptr:value ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。
在这里插入图片描述
pthread exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
调用phread_exit(),他会等待所有其他对等线程(就是同一个进程中的除自身外其他线程)终止,然后再终止主线程和整个进程。

5、clone函数:
虽然在同一程序中创建的GNU / Linux线程是作为单独的进程实现的,但它们共享其虚拟内存空间和其他资源。但是,使用fork创建的子进程可以获取这些项的副本。
Linux clone系统调用是fork和pthread_create的通用形式,它允许调用者指定在调用进程和新创建的进程之间共享哪些资源。
clone()的主要用途是实现线程:在共享内存空间中并发运行的程序中的多个控制线程。与fork()不同,这些调用允许子进程与调用进程共享其执行上下文的一部分,例如内存空间,文件描述符表和信号处理程序表。
在这里插入图片描述
使用clone()创建子进程时,它将执行函数fn(arg)。 fn参数是指向子进程在执行开始时调用的函数的指针。 arg参数传递给fn函数。
child_stack参数指定子进程使用的堆栈的位置。
虽然属于同一进程组的克隆进程可以共享相同的内存空间,但它们不能共享相同的用户堆栈。 因此,clone()调用为每个进程创建单独的堆栈空间
我们可以将clone视为进程和线程之间共享的统一实现。Linux上进程和线程之间的区别是通过将不同的标志传递给克隆来实现的。差异主要在于这个新流程与启动它的流程之间共享的内容。
CLONE_VM(since Linux 2.0):
如果设置了CLONE_VM,则调用进程和子进程在同一内存空间中运行。 特别是,由调用进程或子进程执行的内存写入在另一个进程中也是可见的。
如果未设置CLONE_VM,则子进程在clone()时在调用进程的内存空间的单独副本中运行。 其中一个进程执行的内存写入不会影响另一个进程,就像fork一样。
在没有vm命令行参数的情况下调用时,CLONE_VM标志关闭,父节点的虚拟内存被复制到子节点中。 子节点看到父节点放在buf中的消息,但无论写入buf的是什么,都会进入自己的副本而父节点无法看到它。
但是当传递vm参数时,会设置CLONE_VM并且子任务共享父级的内存。 现在可以从父母那里看到它写入buf的内容。

6、pthread_join函数
阻塞等待线程退出,获取线程退出状态。其作用对应进程中 waitpid() 函数。
int pthread_join(pthread_t thread, void **retval); 成功:0;失败:错误号
参数:thread:线程ID (注意:不是指针);retval:存储线程结束状态。
注意:

  1. 进程中:main返回值、exit参数–>int;等待子进程结束 wait 函数参数–>int *
  2. 线程中:线程主函数返回值、pthread_exit–>void *;等待线程结束 pthread_join 函数参数–>void **
    参数 retval 非空用法:
    调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
    (1)如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
    (2)如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
    (3)如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
    (4)如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。

四、线程的优缺点

1、线程的优点:
(1)创建一个新线程的代价要比创建一个新进程小得多,释放成本也更低。因为创建一个进程就意味着要创建PCB,分配虚拟地址空间,页表,物理内存等系统资源,而创建一个线程只需要创建一个PCB(TCB)即可
(2)与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。进程切换,需要切换对应的虚拟地址空间,更换页表等。过程繁琐。
(3)线程占用的资源要比进程少很多。
(4)能充分利用多处理器的可并行数量。
(5)在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
(6)计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现 I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。进程可以将线程串行执行变成并行执行。最后汇总,提高效率。但是尽量不要创建太多线程,线程切换也是需要成本的。
2、线程的缺点
(1)性能损失:
一个很少被外部事件阻塞的计算密集型线程往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
(2)健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。一个线程挂掉,因为线程共享一块资源。其他线程也会挂掉。进而导致进程退出,资源被回收。
(3)缺乏访问控制:
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。多线程访问临界资源,它的访问控制是由编程者决定。
(4)编程难度提高
编写与调试一个多线程程序比单线程程序困难得多,基于同步互斥。你需要不断的加锁。

你可能感兴趣的:(linux)