目录
前言
一.线程的概念
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内核中并没有为线程构建数据结构。它的线程使用进程来模拟的。
本文讲解多线程概念,后序还有其它知识博文进行补充。
现在再理解进程:进程是承担分配系统资源的基本实体
线程是进程里的一个执行流,CPU调度的基本单位是线程。就比如上图的一个tast_struct就是一个线程。它们共用一个进程地址空间。一个进程可以有一个或者多个执行流。
由于在Linux中没有真正意义上的线程,是用进程来模拟的,所以CPU调度一个线程,看到的还是一个PCB(task_struct)。但是要比传统的进程更加轻量化。task_struct表示的还是进程控制块。
线程的主要作用是:将一个进程的代码和数据分割成几个部分,通过几个执行流(线程)去执行部分代码和数据。所以它比传统的进程更加轻量化。
注意一个进程至少有一个线程,进程与该线程的关系是1:n的。
为什么线程要指向进程的同一个虚拟地址空间?因为透过进程虚拟地址空间,可以看到进程的大部分资源,可以将进程资源合理分配给每一个执行流,就形成了线程执行流。
Linux下虽然没有真正意义上的线程,但是在内核中还是有一些关于线程的数据结构,只是没有专门描述线程的数据结构。
一个处理器只能处理一个线程,如果线程数比可用处理器数多,会有较大的性能损失,会增加额外的同步和调度开销,而资源不变。
编写多线程时,可能因为共享了不该共享的变量,一个线程修改了该变量会影响另外一个线程。多线程之间变量时同一个变量,多进程之间变量不是同一个变量,写时拷贝。
进程时访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
线程是进程的执行分支,线程出现异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止该进程内的所有线程也就终止了。
比如:一个线程数显除0或者野指针操作,导致硬件CPU或者MMU出现异常,传给操作系统,操作系统就会发送信号给进程,终止进程。
不同:
相同:
进程的多个线程共享同一个进程地址空间,因此数据段和代码段都是共享的,如果定义一个函数,每个线程都可以调用。如果定义一个全局变量,每个进程都可以访问,并且访问到的是同一个。并且线程含共享进程的:
线程与进程的关系:
总结:进程强调独立,但是又不是绝对的独立,比如进程间通信。线程强调共享(共享进程的代码和数据),但是又不是绝对的共享,线程有自己的数据。
由于在Linux中没有真正的线程,所以系统没有提供接口(系统调用),需要用户自己来编写。但是我们有一个第三方库,供我们来对线程进行操作。
要使用这些库函数需要引入头文件
链接这些线程函数库时要使用编译器命令"-lpthread"选项
为什么连接线程库要指明库名?标准库不用指明库名?
因为标准库是语言自带的,第三方库不是语言自带的,可能是系统或者是用户自己安装的,线程库是Linux系统安装的,不是语言提供的,对于gcc编译器来说是第三方库。gcc默认连接库是标准库(语言提供的)。编译器命令行参数中没有第三方库的名字。所以给编译器指明库名。
强调:找到库所在路径和使用该路径下的库文件,是两码事。找到路径找不到库,还需要指明库名。标准库中因为编译器命令行中有该库名。
创建线程库函数:
使用函数:
输出:
在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:
代码使用的是上面的代码:
我们发现进程mythread有两个线程,一个线程的id是7854,一个线程的ID是7855。整个进程的ID是7854。
但是有一个线程的ID和进程的ID相同,这不是巧合。线程组(进程)里的第一个线程,在用户态被称为主线程,在内核中被称为group leader。线程中创建的第一个线程,会将该线程的ID设置成和线程组的ID相同。所以线程组内存在一个线程ID和进程ID相同,这个线程为线程组的主线程。
至于线程组的其它线程ID则由内核负责分配。线程组的ID总和主线程ID一致。
一个进程至少有一个线程。如果没有创建线程,该进程就是单线程的单进程。
注意:线程和进程不一样,进程由父子进程的概念,但是在线程了没有,所有进程都是对等的关系。
这里讨论的线程ID就是创建线程函数pthread_create的第一个参数,返回的线程ID。和上面讨论的线程ID不同。
上面讨论的线程ID(LWP)属于进程调度范畴。因为线程是轻量级进程,是操作系统调度的基本单位,所以会需要一个ID来标识给线程。
这里讨论的线程ID,是创建线程函数pthread_create的第一个参数。该内存是线程第三方库为线程在内存中开辟的一块空间。该线程ID指向该空间的起始地址。这个进程ID数据线程库的范畴,线程库的后序操作,就是根据该线程ID来操作的。
为什么返回的是起始地址?
由于Linux没有真正意义上的线程,线程管理需要线程库来做,线程库管理线程也是要先描述再组织,描述如图,组织程一个数组,再返回数组的起始地址。
可以通过函数查询当前线程ID。
Linux没有真正意义上的线程,Linux也没有为线程提供接口。为了管理线程,需要我们自己用户来编写。但是有一个第三方库,POSIX线程库给我们提供了管理线程的功能。但是线程需要内核来调度和执行。
线程管理是通过第三方库POSIX线程库来进行管理的,线管管理是介绍线程库是如何管理线程的。
新线程都是主线程创建的,线程之间的关系都是平等的。
前面有介绍
注意:主线程退出,整个进程就退出了。
只需要某个线程终止而不让进程终止,有三种方法:
新线程也可以用pthread_cancel终止主线程
pthread_exit函数:
注意使用return和pthread_exit返回的指针所指向的内存单元必须是全局或者是malloc分配的,不能是在线程函数栈上分配的,因为线程退出时,函数栈帧被释放了。
pthread_cancel函数
注意不能使用exit(),exit的作用是不论在哪里调用,终止进程。
线程为什么需要等待?
新线程都是主线程创建的,主线程需要知道新线程是否正常退出。
可以对比进程的等待。
默认以阻塞方式等待。
线程退出和进程退出一样,有三种状态。
1.代码正常运行,结果正确,正常退出。
2.代码正常运行,结果不正确,不正常退出。
3.代码出现异常,异常退出。
前两种情况以退出码来表述退出情况,后面一种以退出信号来表示。
但是线程等待函数的第2个参数返回的是执行函数的返回值,也就是退出码,没有表示线程异常退出的情况,这是为什么的?
因为某个线程如果运行异常终止,整个进程都会终止。进程异常终止,就属于进程的等待处理的范畴了。不属于线程范畴。比如:一个线程函数有除0操作,硬件MMU发现异常,操作系统收到异常,向该进程发出信号,终止进程。信号处理的单位是进程。
总的来说就是,等待线程只关心正常运行的退出情况,获取退出码。不关心异常退出情况,异常退出情况上升至进程处理范畴。
怎么拿到退出码的?
函数退出时,进程控制块(PCB)种有一个变量,保存退出码。
调用pthread_join函数的线程默认以阻塞方式等待线程id为thread参数的线程终止,线程以不同的方式终止,得到的终止状态不同
分别获取上面三种退出情况的退出码:
return
pthread_exit
pthread_cancel
注意:可以是线程组内其它线程对目标线程分离,也可以是线程分离自己。
创建新线程的线程,不关心新线程的返回值,可以使用线程分离。
但是线程虽然分离了,当分离的线程因为异常终止,依然会导致进程终止。