1 初步理解线程控制

通过 ps -aL 命令可以查看线程。注意上图,这个ps命令也是一个可执行程序,他也有PID,但是这个LWP是什么呢?LWP(light weight process 轻量级线程), 它也存放在task_struct 结构体中。所以CPU调度的时候看的是轻量级进程LWP!
调度一个进程的时间片,是等分给这个进程的所有线程的。
如果线程出现异常,那么就会导致整个进程就会崩溃。这也是为什么说多线程会导致代码健壮性降低。
2 线程库
2-1 什么是库
库就是被人预先封装好的代码,你只需要调用就可以了,可以加快我们的开发,以及增加代码的可移植性。

用户只认识线程,因为所有操作系统的教程只讲线程,但是像Linux和Windows等操作系统对于线程的实现是不同的,所以提供的系统调用的接口也不同,所以才要有,pthread库把线程的系统调用封装起来,给用户提供同样的一批创建线程的接口,可以提高开发者的效率,不用再去特别熟悉每个系统的系统调用。
Linux系统,不存在真正意义上的线程,他所谓的概念,使用轻量级进程模拟的,但,OS中,只有轻量级进程,所以Linux只会给你提供创建轻量级线程的方法。所谓模拟线程,是我们的说法!
**Linux的线程实现,是在用户层实现的,我们称之为:用户级线程!!**pthread就是原生线程库

C++的方案实现,即C++的库的实现,一种库的例子。为了保证语言的跨平台可移植性。但是语言的跨平台或者可移植性,一般都是怎么实现的,??大力出奇迹,在所有平台全部实现一份,然后条件编译形成库。
2-2 POSIX线程库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"打头的
要使用这些函数库,要通过引入头文 <pthread.h>
链接这些线程函数库时要使用编译器命令的"-lpthread"选项
3 创建线程
3-1 创建线程接口
cpp
功能:创建⼀个新的线程
原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
参数:
thread:输出型参数,返回线程ID
attr:设置线程的属性,attr为NULL表⽰使⽤默认属性
start_routine:是个函数地址,线程启动后要执⾏的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
1 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
3 pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小
注意:
线程创建好了需要被主线程创建好,当不用的时候需要被主线程等待回收,否则会出现类似僵尸进程的问题
3-2 线程执行函数
当线程启动时,线程会跳转到传入的一个参数为 void* 返回值为 void* 的函数处执行对应的代码。这个函数就是经典的可重入函数
这个函数的参数是void* 类型,但是所以你可以传入一个内置类型或者一个类然后在线程执行流内进行强转得到参数

这个函数的返回值是void* 类型,所以你可以返回一个内置类型或者一个类在线程退出主线程得到这个退出码的时候通过类型转化得到你想要的参数。

3-3 线程ID
cpp
#include <pthread.h>
// 获取线程ID
pthread_t pthread_self(void);

打印出来的 tid 是通过 pthread 库中有函数 pthread_self 得到的,它返回⼀个 pthread_t 类型的变量,指代的是调用 pthread_self 函数的线程的 "ID"。
怎么理解这个"ID"呢?这个"ID"是 pthread 库给每个线程定义的进程内唯一标识,是 pthread 库维持的。
由于每个进程有自己独立的内存空间,故此"ID"的作用域是进程级而非系统级(内核不认识)。
其实 pthread 库也是通过内核提供的系统调用(例如clone)来创建线程的,而内核会为每个线程创建系统全局唯一的"ID"来唯一标识这个线程。
使用PS命令查看线程信息

LWP 是什么呢?LWP 得到的是真正的线程ID。之前使用 pthread_self 得到的这个数实际上是一个地址,在虚拟地址空间上的⼀个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。
在 ps -aL 得到的线程ID,有一个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的,并且线程ID是由pthread库维护的。而pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。
注意:
当新线程被创建之后,他和主线程谁会先运行是不确定的。
4 线程退出
4-1 线程退出情况
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数进行return,表示线程运行结束进行退出。注意这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用 pthread_ exit,终止自己。
3 main 函数即主线程退出,则同一进程中的所有线程都会退出 - ⼀个线程可以调用pthread_ cancel 终止同一进程中的另一个线程,注意如果线程被取消,退出结果是-1PTHREAD_CANCELED。
4-2 线程退出函数接口
pthread_exit 函数
cpp
功能:线程终⽌
原型:
void pthread_exit(void *value_ptr);
参数:
value_ptr:类似于返回值可以被 pthread_join 获取,注意value_ptr不要指向⼀个局部变量。
返回值:
⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调⽤者(⾃⾝)
用于线程自己退出。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用 malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_cancel 函数
cpp
功能:取消⼀个执⾏中的线程
原型:
int pthread_cancel(pthread_t thread);
参数:
thread:线程ID
返回值:成功返回0;失败返回错误码
用于主线程取消新线程
5 线程等待
5-1 线程等待原因
为什么需要线程等待?
1 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
2 创建新的线程不会复用刚才退出线程的地址空间。
5-2 线程等待接口
cpp
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID
value_ptr:它指向⼀个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为 thread 的线程终止。thread 线程以不同的方法终止,通过 pthread_join 得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr 所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终终止的,value_ptr所指向的单元存放的是传给
pthread_exit的参数。 - 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
为什么在用 pthread_join 的时候,为什么没有看到异常相关的字段呢? 因为 pthread_join 都是基于线程健康跑完的情况,不需要处理异常信号,异常信号,是进程处理的话题。因为等待的目标线程如果异常了,整个进程都退出了,包括main 函数的线程,所以 pthread_join 的异常没有意义,看也看不到。
所以如果 pthread_join 拿到了返回值,就一定是线程退出时设定的返回值,默认线程无异常。

6 线程分离
6-1 线程分离态
如果主线程不关心新线程,而是当新线程结束的时候,让他自己释放呢?? 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。则设置新线程为分离状态,即主线程取消新线程
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成系统泄漏。
6-2 理解线程分离
技术层面:
线程默认是需要等待的, 在task_struct 结构体中也有一个字段为 joinable.如果不想让主线程等待新线程,想让新线程结束之后,自己退出,设置为分离状态 (!joinable or detach)
理解层面:
线程分离,有两种情况:
1 主线程分离新新程
2 新线程把自己分离
分离的线程,依旧在进程的地址空间中,进程把所有资源,被分离的线程,依旧可以被访问,可以操作。
操作层面:
进行分离操作,就是将TCB的中表示joinable状态的标志位设置为分离。注意,如果线程被设置为分离状态,不需要进行join,join会失败。
6-3 线程分离的接口
cpp
功能:将线程设置为分离态
原型
int pthread_detach(pthread_t thread);
参数:
thread:线程ID
返回值:成功返回0;失败返回错误码
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
cpp
pthread_detach(pthread_self());
注意:
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
7 线程ID及进程地址空间布局
7-1 再次理解线程
Linux没有真正的线程,他是用轻量级进程模拟实现的,所以操作系统提供的接口,不会直接提供线程的接口,再用户层,封装轻量级进程,形成原生线程库(用户级别的库)。
线程的概念是在库中维护的,在库内部,就一定会存在多个被创建好的线程,那库要不要管理线程呢?要所以也是先描述,再组织
cpp
struct tcb //调用pthread_create的时候创建
{
// 线程应该有的属性
线程状态
线程id
线程独立的栈结构
线程栈的大小
.......
};
但是想优先级,时间片,还有上下文是不会在这个结构体内部的,因为这是用户级的库维护线程所创立的,而向上述提到的东西还是在Linux内核中的轻量级进程结构体中封装的
之前的文章提到过,动态库是ELF可执行程序,那线程库呢?线程库也是ELF可执行程序
而我们的可执行程序加载,形成进程,动态链接和动态地址重定向。要将动态库,加载到内存中并且映射到当前进程的地址空间中。

注意:
进程自己的代码去可以访问到pthread库内部的函数或者数据
Linux所有线程,都在库中
7-2 线程库级实现原理
pthread_ create 函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是⼀回事。 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
cpp
pthread_t pthread_self(void);

每个线程都必须要有自己独立的栈空间。
我们所调用的 pthread_tid 起始就是线程在库中,对应管理模块的起始地址
线程执行函数返回值也是先存放在库中的 TCB 中然后带出返回, 赋值给上层的 pthread_join 传入的二级指针,这也解释为什么要传入二级指针**,因为返回值是一级的void* 指针,而将返回值内容带出就要在函数内部修改这个变量,所以传入二级指针。**
之前我们了解明明轻量级进程会在线程退出时,由操作系统自动释放,为什么线程还需要join?因为join解救的不是内核结构体没有释放而导致的类似僵尸进程导致内存泄漏的问题,而是用户级线程库中维护的线程结构体需要释放
体现了用户级线程库和内核LWP之间的联动
7-3 理解线程的调用

当 pthread_create 被调用时,会先在库中创建线程的控制管理模块,并且要在内核中,创建轻量级进程(调用系统调用,执行什么方法,栈在那 ----> 调用系统调用 )

注意:
Linux 用户级线程 : 内核LWP = 1 :1
举例子理解:
今天我去食堂吃饭,我的一个舍友叫我带饭。这个时候我的大脑中就会创建关于这个任务的信息,比如什么时间去,什么人要,要买什么,类比于线程调用时先在pthread库中创立管理信息,并且在内核中创立对应的轻量级进程。而后我就去食堂给舍友带饭,即线程执行对应的执行函数。
7-4 线程栈
虽然 Linux 将线程和进程不加区分的统一到了 task_struct ,但是对待其地址空间的 stack 还是有些区别的。
对于 Linux 进程或者说主线程,简单理解就是main函数的栈空间,在fork的时候,实际上就是复制了父亲的 stack 空间地址,然后写时拷贝(cow)以及动态增长。如果扩充超出该上限则栈溢出
会报段错误(发送段错误信号给该进程)。进程栈是唯一可以访问未映射页而不一定会发生段错
误⸺超出扩充上限才报。
然而对于主线程生成的子线程而言,其 stack 将不再是向下生张的,而是事先固定下来的。线
程栈一般是调用 glibc/uclibc 等的 pthread 库接口 pthread_create 创建的线程,在文件映射区(或称之为共享区)。其中使用 mmap 系统调用,这个可以从 glibc 的 nptl/allocatestack.c 中的 allocate_stack 函数中看到:
cpp
mem = mmap (NULL, size, prot,MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
此调用中的 size 参数的获取很是复杂,你可以手工传入stack的大小,也可以使用默认的,⼀般而言就是默认的 8M 。这些都不重要,重要的是,这种stack不能动态增长,一旦用尽就没了,这是和生成进程的fork不同的地方。在glibc中通过mmap得到了stack之后,底层将调用 sys_clone 系统调用:
cpp
int sys_clone(struct pt_regs *regs)
{
unsigned long clone_flags;
unsigned long newsp;
int __user *parent_tidptr, *child_tidptr;
clone_flags = regs->bx;
//获取了mmap得到的线程的stack指针
newsp = regs->cx;
parent_tidptr = (int __user *)regs->dx;
child_tidptr = (int __user *)regs->di;
if (!newsp)
newsp = regs->sp;
return do_fork(clone_flags, newsp, regs, 0, parent_tidptr,
child_tidptr);
}
因此,对于子线程的 stack ,它其实是在进程的地址空间中map出来的⼀块内存区域,原则上是线程私有的,但是同一个进程的所有线程生成的时候,是会拷贝生成者的 task_struct 的很多字段,如果愿意,其它线程也还是可以访问到的,一定要注意。
7 -5 理解独立
独立的上下文:有独立的PCB(内核) + TCP (用户层,pthread库内部)
独立的栈:每个线程都有自己的栈,要么时进程自己的,要么是库中创建进程时,mmap申请出来的
注意:
但是这个"独立",是在相互间不告诉自己栈地址下才成立的,因为所有线程共用一个地址空间,如果一个线程一定要访问另一个线程的栈也是可能可以访问到的
7-6 线程的局部存储
cpp
_thread int count=1;
该count叫做线程的局部存储,就是在各个线程都拷贝一份
该线程的局部存储有什么用?我想要创建一个全局变量,但是我又不想让这个全局变量被其他线程看到!所以使用线程的局部存储保证这个变量改动和读取不会被其他线程影响
线程的局部存储只有你一个人访问,不存在并发问题
注意:线程的局部存储,只能存储内置类型和部分指针