目录
1. 线程概念
2. 线程vs进程
3. 线程的优缺点
4. 线程创建
4.1 pthread_create
4.2 pthread_self
5. 线程终止
5.1 return
5.2 pthread_exit
5.3 pthread_cancel
6. 线程等待
7. 线程分离
1. 线程概念
线程:轻量级进程,在进程内部执行,是OS调度的基本单位;进程内部线程共用同一个地址空间,同一个页表,以及内存中的代码和数据,这些资源对于线程来说都是共享的资源
进程:在用户视角下,内核数据结构+该进程对应的代码和数据;在内核的视角下,承担分配系统资源的基本实体
其实在之前没有了解线程之前的进程,就是一个只有一个主线程的进程
现在又该如何理解线程和进程之间的关系呢?
线程和进程之间就像家庭成员和家庭之间的概念,家庭是由所有家庭成员共同组成的,在这个社会中可能有三口之家,五口之家
我们在社会中被描述为一个整体,比如户口本就是按家庭发放的,房屋是按家庭建造的,社会资源按家庭分配的,例如疫情期间买菜
家庭成员和家庭之间的关系就是一荣俱荣,一损俱损的关系(线程异常时会触发信号机制,终止进程,内核所有线程都会退出)
我们在家庭中扮演不同的角色,也在使用不同的资源,家庭负责分配这些资源,其中有大部分资源是家庭成员之间共享的,比如卫生间,客厅,但总有一个房间是给你住的,作为你的私人空间
家庭资源:空间,数据
私人空间:后面会说
在Linux中,没有专门为线程管理,创建新的数据结构,而是沿用了进程的task_struct,用它来模拟线程,线程和进程有很多共性,为此Linux下的所有版本都安装了pthread系统库,为了提供用户使用的接口
库底层调用了clone系统接口,所以系统提供一部分,库提供一部分,就组成了Linux下的线程
所以使用线程库的时候,需要包含头文件<pthread.h>,编译时需要-lpthread
线程共享进程数据,但也拥有自己的一部分数据
线程独有:
- 线程ID
- 一组寄存器(上下文)
- 独立栈结构
- errno变量在线程内部
- 信号屏蔽字(block)
- 调度优先级
线程共有:
- 文件描述符表
- 信号处理方式
- 当前工作目录
- 用户id和组id(权限)
线程独立的栈结构(保存临时数据)和上下文数据,体现了线程的动态属性
线程如何实现独立的栈结构:
线程库提供了线程创建接口,它会在地址空间的共享区中创建对应线程的栈结构,其实在上面系统接口中就有所体现clone中有一个参数child_stack的指针,栈地址,栈的组成应该是系统提供的
通过clone系统接口的第一个参数函数指针,其实如何让线程执行自己代码,其实就是执行函数代码
2. 线程vs进程
进程是资源分配的基本单位
线程是CPU调度的基本单位
线程切换的成本更低
因为线程间共享地址空间和页表,这些不需要切换
其次,CPU内部有L1~L3 的 cache 高速缓存(对内存的代码和数据根据局部性原理,预读到CPU内部,进而提高效率)也不用切换
3. 线程的优缺点
优点:
- 创建,切换,占用资源都比进程小
- 能充分利用多处理器可并行的数量
- 在等待慢速IO操作结束的同时,程序可以执行其他任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中
- IO密集型应用,为了提高性能,将IO操作重叠,可以同时等待不同的IO
缺点:
缺乏访问控制(后续会学互斥,同步,信号量来解决这一问题)
4. 线程创建
这里介绍pthread线程库的接口,他们都有一个规律,函数都是以pthread_为开头,且函数的返回值都是成功返回0,失败返回errno,因为每一个线程都有独立的errno
4.1 pthread_create
函数功能:
创建一个线程参数:
thread:输出型参数,返回线程ID(NPTL后面解释)
attr:设置线程属性,一般为nullptr
start_routine: 线程执行的函数地址,函数类型必须参数和返回值都是void*
arg:作为传给线程函数的参数
线程ID有两个,一个属于OS系统层面的,一个属于NPTL(Nativa POSIX Thread Libaray本地可移植性操作系统接口线程库)本地线程库层面的
在Linux内核中,OS其实在调度的时候并不知道是线程还是进程,它只认识task_struct
struct task_atruct
{
pid_t pid;
pid_t tgid; // Thread Group ID
struct task_struct *group_leadler;
struct list_head thread_group;
// ...
}
在操作系统看来它最关心的是pid作为线程的唯一标识符,而tgid则是线程组的id,也就是进程id。在用户层调用getpid()不论是在哪个线程内部,返回的都是进程的id,所以其实getpid()底层获取的是task_struct的tgid。
用户可以通过指令查看当前的进程id
ps -eLf
其中选项-L会显示LWP:线程id 和 NLWP:线程组内线程个数
在线程组内,所有的线程都是对等关系
pthread_t 类型其实是线程库里定义的线程id,其实就是线程结构体在地址空间的起始地址,线程库的线程id用于线程库接口的使用
4.2 pthread_self
函数功能:
获得自己的线程ID,返回值就是
5. 线程终止
5.1 return
return返回值可以设为nullptr标识什么都不返回,也可以返回指针指向的全局静态或者动态分配的空间,但是不能返回栈上的临时数据,因为线程终止时,线程的栈会被销毁
5.2 pthread_exit
函数功能:
终止自己这个线程
参数:
retval:和return返回的值一个道理
5.3 pthread_cancel
函数功能:
终止一个正在执行的线程,一般用于终止其他线程
参数:
thread:要被终止的线程id
注意:
pthread_cancel(pthread_self());不推荐这样做,因为这样操作是未定义的,会出现奇怪的错误
6. 线程等待
已经退出的线程如果不等待回收,其空间没有被释放,会处于“僵尸”状态,造成内存泄漏
函数功能:
等待回收线程参数:
thread:被等待的线程id
retval:输出型参数,用于获取线程终止时的返回值,不关心设置nullptr,因为线程的返回值是void*,所以在函数内部拿到这个值,就要void**
当return/pthread_exit终止时,拿到线程函数的返回值
当pthread_cancel异常终止时,拿到PTHREAD_CANCELED(-1)
7. 线程分离
一般创建的线程都需要pthread_join等待,但若不关心线程的返回值,join就是一种负担,分离线程,线程退出时,自动释放资源
函数功能:
线程分离
参数:
thread:线程id
注意:
1. pthread_detach(pthread_self());这里可以使用
2. joinable和分离是冲突的,一个线程不能同时被等待,又被分离
完。