Linux多线程——概念
Posted 两片空白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux多线程——概念相关的知识,希望对你有一定的参考价值。
目录
前言
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 线程终止
注意:主线程退出,整个进程就退出了。
只需要某个线程终止而不让进程终止,有三种方法:
- 从线程函数return,这种情况对主线程不适用,因为主线程退出,整个进程就退出了。
- 线程可以调用pthread_exit终止。
- 一个线程可以调用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多线程——概念的主要内容,如果未能解决你的问题,请参考以下文章