总结一下线程的基本概念以及线程的创建、销毁、等待、分离
线程程序设计的引入进一步提高了程序的执行性能
本文通过对进程与线程及POSIX部分接口的相互关系的介绍,浅析操作系统中线程的概念。
线程是一个进程内部的执行序列,可以先从概念与进程简单对比
- 进程是资源执行的最小单位
- 线程是执行流的最小单位
线程的数据结构
- 类似进程的PCB结构,线程的维护通过TCB结构。但是实际上linux基于PCB模拟TCB
- 线程基于进程的mm_struct,拥有独立上下文私有栈空间,但仍共享代码段、数据段,共享堆空间,还包括
- 文件描述符表
- 信号的处理方式
- 当前工作目录
- 用户id/ 组id
线程的特性
- 线程的优势
- 线程创建的开销(空间、时间)比进程小的多
- 线程切换的代价比进程小的多
- 可以通过分解线程到不同处理器提升计算密集型作业的性能
- 可以通过重叠I/O提升I/O密集型作业性能,线程可以等待不同的I/O操作
- 线程的劣势
- 密集型作业中线程数 > 处理器数,额外的线程切换带来了额外的同步、调度的开销
- 线程缺乏保护,需要注意不该共享的变量等,使得健壮性降低
- 更复杂的线程间调度进一步增加了构建代码以及调试程序的难度
线程ID
被称为线程ID的有两个
- 第一个
pthread_t
类型,此类型仅仅用来进程内部区分个线程,属于NPTL实现的层面
(在Linux中,线程实现是Native POSIX Thread Libaray,简称NPTL) - 第二个
pid_t
类型,是操作系统对线程的唯一标识符,属于内核进程调度的层面
后者即为 ps -elf
看到的 LWP
1 | ps -eLf |head -1 && ps -eLf |grep a.out |grep -v grep` |
可以看到两行数据中 pid 是相同的
因为线程属于同一个进程,所以调用gitpid时,都会返回同一个进程pid
因为pid相同,只能用另一个标识区分内核下不同线程,即pid_t
类型的线程ID
每个线程拥有独立的task_struct,从task_struct对pid与线程ID得到更清晰的理解
1 | struct task_struct { |
- pid 即进程id
- tgid Thread Group ID 就是对应LWP的线程ID
- 进程内所有线程属于一个线程组,第一个被创建的线程(即原来单进程单线程对应的线程)为主线程,该线程ID = 线程组 ID
- NLWP 即为线程组内线程的数量
线程的创建与销毁
POSIX(可移植作业系统接口)标准规定了一系列对线程的操作函数(用户级函数库)
(使用makefile时要使用编译器命令的“-lpthread”)
以下是一个创建线程的例子
1 | //API介绍见下文 |
其中
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*star t_routine)(void*), void *arg);
- 用来创建一个线程
- thread:返回线程ID
- attr:线程属性,NULL表⽰使⽤用默认属性
- start_routine:线程启动后执行的函数的地址
- arg:线程启动的参数
- 成功返回0,失败返回错误码(其他函数一般返回-1,并设置错误码),也可以读取线程内错误码,但接受返回值的开销少一些
int pthread_join(pthread_t thread, void **value_ptr);
- 阻塞的方式等待线程结束,如果不做此操作,则将遇到类似「僵尸进程」的问题,即资源泄漏
&tid
即prhtread_t
类型的线程IDvoid **
用来接受线程函数返回值- 类似
pthread_create
,pthread_join
成功返回0,失败放回错误码
pthread_t pthread_self(void);
- 获取当前线程的用户级线程ID
int pthread_cancel(pthread_t thread);
- 取消一个执行中的进程
上文中的pthread_join
虽然达到了避免资源泄漏的问题,但是会让主线程进入阻塞态
在我们不关心返回值的时候,我们还可以通过分离线程同样避免资源泄漏的问题
用「分离线程」替代「等待线程」:
1 |
|
其中
int pthread_detach(pthread_t thread);
- 分离一个线程,可以不用阻塞主线程达到线程结束自动归还资源的目的(但无法接受返回值)
- 可以自己分离自己,也可以由主线程分离
- 分离的线程还依赖进程资源,发生异常依旧会引起 core dumped