Linux多线程——概念

Posted 两片空白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux多线程——概念相关的知识,希望对你有一定的参考价值。

目录

前言

一.线程的概念

        1.1 什么是线程

        1.2 线程的优点

        1.3 线程缺点

        1.4 线程异常

        1.5 线程用途

        1.6 Linux进程和线程对比

        1.7 关于进程和线程的问题  

       1.7.1 POSIX线程库

        1.7.2 进程ID和线程ID

        1.7.3 线程ID和进程地址空间

        1.7.4  线程库与内核线程的关系 

二.线程管理

        2.1线程创建

        2.2 线程终止

        2.3 线程等待

三.线程分离


前言

        Linux系统中并没有真正意义上的多线程,因为linux内核中并没有为线程构建数据结构。它的线程使用进程来模拟的。

        本文讲解多线程概念,后序还有其它知识博文进行补充。

一.线程的概念

        1.1 什么是线程

        现在再理解进程:进程是承担分配系统资源的基本实体

        线程是进程里的一个执行流,CPU调度的基本单位是线程。就比如上图的一个tast_struct就是一个线程。它们共用一个进程地址空间。一个进程可以有一个或者多个执行流。

        由于在Linux中没有真正意义上的线程,是用进程来模拟的,所以CPU调度一个线程,看到的还是一个PCB(task_struct)。但是要比传统的进程更加轻量化。task_struct表示的还是进程控制块。

        线程的主要作用是:将一个进程的代码和数据分割成几个部分,通过几个执行流(线程)去执行部分代码和数据。所以它比传统的进程更加轻量化。

        注意一个进程至少有一个线程,进程与该线程的关系是1:n的。

        为什么线程要指向进程的同一个虚拟地址空间?因为透过进程虚拟地址空间,可以看到进程的大部分资源,可以将进程资源合理分配给每一个执行流,就形成了线程执行流。

        Linux下虽然没有真正意义上的线程,但是在内核中还是有一些关于线程的数据结构,只是没有专门描述线程的数据结构。

        1.2 线程的优点

  • 创建一个新的线程的代价比创建一个进程小得多。创建一个线程虽然也需要创建数据结构,但是并不需要重新开辟资源,只需要将进程的部分资源分配给线程。创建一个进程不仅需要创建大量数据结构,还需要重新创建资源。
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作少。线程只是进程的部分资源,切换的资源少。
  • 线程占用的资源比进程少
  • 能充分利用多处理器的可并行数量。
  • 在等待慢速的I/O操作结束的同时,程序可以执行其它的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  • I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作。I/O操作是与外设交互数据,会很慢。

        1.3 线程缺点

  • 性能缺失

        一个处理器只能处理一个线程,如果线程数比可用处理器数多,会有较大的性能损失,会增加额外的同步和调度开销,而资源不变。

  • 鲁棒性降低

        编写多线程时,可能因为共享了不该共享的变量,一个线程修改了该变量会影响另外一个线程。多线程之间变量时同一个变量,多进程之间变量不是同一个变量,写时拷贝。

  • 缺乏访问的控制

        进程时访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响

  • 编程难度高

        1.4 线程异常

        线程是进程的执行分支,线程出现异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止该进程内的所有线程也就终止了。

        比如:一个线程数显除0或者野指针操作,导致硬件CPU或者MMU出现异常,传给操作系统,操作系统就会发送信号给进程,终止进程。

        1.5 线程用途

  • 合理使用多线程,能提高CPU密集型程序的执行效率。但是线程个数不是越多越好,与CPU和核数有关。设置的线程数越多,调度次数增加,同步数增加。
  • 合理利用多线程,能提高I/O密集型程序的用户体验。I/O效率慢,多数时间都在等待。可以同时等待多个I/O,效率会提高。

        1.6 Linux进程和线程对比

不同:

  • 进程是资源分配的基本单位
  • 线程是CPU调度的基本单位
  • 线程共享进程的数据,但是也有属于自己的数据
    • 线程ID,LWP和pthread_creat,创建线程函数第一个参数。两个线程ID不同
    • 一组寄存器,保存线程上下文数据
    • 栈:线程有自己的栈,如果每一个线程都共享栈,数据都压到一个栈里,数据就乱了。
    • errno,错误信息
    • 信号屏蔽字,block位图
    • 调度优先级

相同:

        进程的多个线程共享同一个进程地址空间,因此数据段和代码段都是共享的,如果定义一个函数,每个线程都可以调用。如果定义一个全局变量,每个进程都可以访问,并且访问到的是同一个。并且线程含共享进程的:

  • 文件描述符表 files_struct
  • 各种信号的处理方式
  • 当前工作目录
  • 用户id和组id

线程与进程的关系:

        总结:进程强调独立,但是又不是绝对的独立,比如进程间通信。线程强调共享(共享进程的代码和数据),但是又不是绝对的共享,线程有自己的数据。

        1.7 关于进程和线程的问题  

       1.7.1 POSIX线程库

        由于在Linux中没有真正的线程,所以系统没有提供接口(系统调用),需要用户自己来编写。但是我们有一个第三方库,供我们来对线程进行操作。

        要使用这些库函数需要引入头文件<pthread.h>

        链接这些线程函数库时要使用编译器命令"-lpthread"选项

为什么连接线程库要指明库名?标准库不用指明库名?

        因为标准库是语言自带的,第三方库不是语言自带的,可能是系统或者是用户自己安装的,线程库是Linux系统安装的,不是语言提供的,对于gcc编译器来说是第三方库。gcc默认连接库是标准库(语言提供的)。编译器命令行参数中没有第三方库的名字。所以给编译器指明库名。

        强调:找到库所在路径和使用该路径下的库文件,是两码事。找到路径找不到库,还需要指明库名。标准库中因为编译器命令行中有该库名。

创建线程库函数:

使用函数:

 输出:

        1.7.2 进程ID和线程ID

        在Linux中由于没有真正的线程,目前的线程都是用原生线程库(Nagtive POSIX Thread Library)来实现。在这种实现下,线程又被称作轻量级进程,因为线程仍然使用进程描述符task_struct,但是只是执行进程的部分内容。

        没有线程之前,一个进程对应内核的一个进程描述符,对应进程的ID。引入线程之后,一个进程对应了一个或者多个线程,每一个线程作为CPU调度的基本单位,在内核态也有自己的ID。

        线程组,多线程的进程,又被称为线程组。每一个线程在内核中都存在一个进程描述符(task_struct),因为Linux下,用进程来模拟线程。进程结构体中的pid,表明上看是进程ID,其实不是,它实际对应线程ID进程描述符中的tgid,对应用户层面的进程ID

        总结:进程有自己的ID在源码中是tgid,线程也有自己的ID,在源码中是pid 。

        进程ID有什么用呢?可以表示线程属于哪个进程的。就可以知道进程有多少线程

        在创建线程使用的函数pthread_create的第一个参数返回的也是线程的id但是和这里的线程id,不同,这里的线程id是用来标识线程的,后面有介绍创建线程函数返回的id。

        查看线程id:

代码使用的是上面的代码:

  • LWP显示的是线程ID。

        我们发现进程mythread有两个线程,一个线程的id是7854,一个线程的ID是7855。整个进程的ID是7854。

        但是有一个线程的ID和进程的ID相同,这不是巧合。线程组(进程)里的第一个线程,在用户态被称为主线程,在内核中被称为group leader。线程中创建的第一个线程,会将该线程的ID设置成和线程组的ID相同。所以线程组内存在一个线程ID和进程ID相同,这个线程为线程组的主线程。

        至于线程组的其它线程ID则由内核负责分配。线程组的ID总和主线程ID一致。

       一个进程至少有一个线程。如果没有创建线程,该进程就是单线程的单进程。

         注意:线程和进程不一样,进程由父子进程的概念,但是在线程了没有,所有进程都是对等的关系。

        1.7.3 线程ID和进程地址空间

        这里讨论的线程ID就是创建线程函数pthread_create的第一个参数,返回的线程ID。和上面讨论的线程ID不同。

        上面讨论的线程ID(LWP)属于进程调度范畴。因为线程是轻量级进程,是操作系统调度的基本单位,所以会需要一个ID来标识给线程。

        这里讨论的线程ID,是创建线程函数pthread_create的第一个参数。该内存是线程第三方库为线程在内存中开辟的一块空间。该线程ID指向该空间的起始地址。这个进程ID数据线程库的范畴,线程库的后序操作,就是根据该线程ID来操作的

        为什么返回的是起始地址?

        由于Linux没有真正意义上的线程,线程管理需要线程库来做,线程库管理线程也是要先描述再组织,描述如图,组织程一个数组,再返回数组的起始地址。

 可以通过函数查询当前线程ID。

        1.7.4  线程库与内核线程的关系 

        Linux没有真正意义上的线程,Linux也没有为线程提供接口。为了管理线程,需要我们自己用户来编写。但是有一个第三方库,POSIX线程库给我们提供了管理线程的功能。但是线程需要内核来调度和执行。

二.线程管理

        线程管理是通过第三方库POSIX线程库来进行管理的,线管管理是介绍线程库是如何管理线程的。

        2.1线程创建

        新线程都是主线程创建的,线程之间的关系都是平等的。

前面有介绍

        2.2 线程终止

注意:主线程退出,整个进程就退出了。

只需要某个线程终止而不让进程终止,有三种方法:

  1. 从线程函数return,这种情况对主线程不适用,因为主线程退出,整个进程就退出了。
  2. 线程可以调用pthread_exit终止。
  3. 一个线程可以调用pthread_cancel终止同一进程里的线程。

新线程也可以用pthread_cancel终止主线程

pthread_exit函数:

         注意使用return和pthread_exit返回的指针所指向的内存单元必须是全局或者是malloc分配的,不能是在线程函数栈上分配的,因为线程退出时,函数栈帧被释放了。

pthread_cancel函数

注意不能使用exit(),exit的作用是不论在哪里调用,终止进程。

2.3 线程等待

        线程为什么需要等待?

        新线程都是主线程创建的主线程需要知道新线程是否正常退出。

  • 已退出的线程,线程库为它开辟的空间还没有被释放,仍然在进程的内存地址空间。线程等待,需要将其空间释放。
  • 创建的新线程不会复用刚才退出线程的地址空间。

        可以对比进程的等待。

默认以阻塞方式等待。

线程退出和进程退出一样,有三种状态。

1.代码正常运行,结果正确,正常退出。

2.代码正常运行,结果不正确,不正常退出。

3.代码出现异常,异常退出。

前两种情况以退出码来表述退出情况,后面一种以退出信号来表示。

        但是线程等待函数的第2个参数返回的是执行函数的返回值,也就是退出码,没有表示线程异常退出的情况,这是为什么的?

        因为某个线程如果运行异常终止,整个进程都会终止。进程异常终止,就属于进程的等待处理的范畴了。不属于线程范畴。比如:一个线程函数有除0操作,硬件MMU发现异常,操作系统收到异常,向该进程发出信号,终止进程。信号处理的单位是进程。

        总的来说就是,等待线程只关心正常运行的退出情况,获取退出码。不关心异常退出情况,异常退出情况上升至进程处理范畴。

怎么拿到退出码的?

函数退出时,进程控制块(PCB)种有一个变量,保存退出码。

调用pthread_join函数的线程默认以阻塞方式等待线程id为thread参数的线程终止,线程以不同的方式终止,得到的终止状态不同

  • 如果线程通过return终止,pthread_join函数的第二个参数retval直接指向return后面的返回值。
  • 如果线程通过pthread_exit终止,pthread_join函数的第二个参数retval直接指向pthread_exit参数。
  • 如果线程通过被其它线程调用pthread_cancel终止,pthread_join函数的第二个参数retval直接存放的是一个常数宏PTHREAD CANCELED,值是-1。#define PTHREAD CANCELED (void *)-1。
  • 如果对不关心返回值,可以将ret_val设为NULL。

分别获取上面三种退出情况的退出码:

return

pthread_exit 

 pthread_cancel

三.线程分离

  • 默认情况下,新创建的线程是joinable的,需要创建新线程的线程等到新线程,新线程退出后需要对其进行pthread_join操作,否则无法释放资源,造成内存泄漏。
  • 如果不关心返回值,我们可以告诉系统,将线程分离,当线程退出后,自动释放线程资源。

注意:可以是线程组内其它线程对目标线程分离,也可以是线程分离自己。

 创建新线程的线程,不关心新线程的返回值,可以使用线程分离。

 但是线程虽然分离了,当分离的线程因为异常终止,依然会导致进程终止。

以上是关于Linux多线程——概念的主要内容,如果未能解决你的问题,请参考以下文章

Linux多线程——概念

Linux 多线程

Linux 多线程

Linux 多线程

Linux 多线程

Linux系统编程 多线程