一、线程库
在 Linux 中,内核并没有明确的线程概念,只有轻量级进程的概念。因此操作系统没有提供直接的线程系统调用,仅提供轻量级进程的系统调用。
为了降低学习成本并提供线程接口,应用层对轻量级进程接口进行了封装,提供了 pthread 线程库。
pthread 线程库又叫原生线程库,几乎所有 Linux 平台默认自带。对于 g++ 来说属于第三方库,编译时需使用 -lpthread 选项。
二、线程创建 pthread_create
功能:创建一个新的线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数说明:
thread: 返回线程 ID(输出型参数)attr: 设置线程的属性,设为 NULL 表示使用默认属性(一般设为 NULL)start_routine: 函数地址,线程启动后要执行的函数arg: 传给线程启动函数的参数(可以通过类传多个)
返回值:成功返回 0;失败返回错误码。pthreads 函数出错时不会设置全局变量 errno,而是将错误代码通过返回值返回。pthreads 同样也提供了线程内的 errno 变量以支持其它使用 errno 的代码(局部存储)。建议通过返回值来判定错误,因为读取返回值比读取线程内 errno 变量的开销更小。
2.1 简单看看多线程


链接库时只需 -lpthread,因为该库已默认安装在系统路径下,编译器知道其位置,只是需要指定链接哪个库。
如果想查看所有的轻量级进程,可以用 ps -aL(a 是 all 的意思,L 是轻的意思)。
我们会发现主线程的 PID 和 LWP(CPU 调度的基本单位)是一样的,这应该也是用来让 CPU 区分切换的是主线程还是次线程的一个标识。
监视线程的方法:

2.2 全局变量
所有的线程都可以看到全局变量。


以前的进程间通信,无论是管道、共享内存、消息队列……它们让两个进程看到同一份代码和资源的方法都比较麻烦。线程天生就具有看到同一份资源的能力,所以也给我们的通信提供了很好的应用场景和技术准备。
为什么不研究多进程并发,而是研究多线程并发呢?因为多进程的写时拷贝、通信等都很麻烦,而线程的共享性更容易实现,它是先进性的表现。但是方便的同时也伴随着线程之间的相互影响、健壮性差等问题,因此这些需要我们程序员在代码上去解决这类问题。
2.3 tid vs LWP

我们会发现 tid 和 LWP 差距非常大。因为 lwp 是操作系统的轻量级进程的概念,只需要 OS 知道就行,而 tid 是给用户使用的(本质是一个地址)。

2.4 线程函数参数返回值为啥都是 void*
以往进程返回是通过返回错误码来告知我们错误信息,但线程中的函数为什么是 void* 呢?
因为不止可以传整形、字符串……还可以传类对象!类里面可以放很多内置类型,其实就相当于可以传很多参数,以及返回很多返回值。
即使你只想传一个整形或者字符串,你也可以封装在类里面传。能传类的话尽量传类,因为它具有可扩展性,未来想增加别的类型就很方便。
比方说我们要计算 1-100 相加,我们可以写个 request 的类传递给它 1-100 的区间,然后再写个 Respond 的类帮助我们把运行结果返回回来。

要注意一定不要在主线程里面创建局部变量传递给次线程。

如果我们主线程要传类对象给次线程,就必须在堆区开辟空间。这样虽然 td 指针被释放了,但是我们可以通过 args 把这个指针传递给线程,这样每个线程就可以去访问自己在堆中的对象了。


其实堆区的资源大家都看得到,比如我 2 号线程也可以去看 1 号线程堆区的数据,但是这样没有意义。所以线程可以看到全部的堆空间,但是每个线程访问的是堆的不同位置。
可是我们为什么不直接在线程里去写这个参数,而是要让主线程通过类传递过去呢?
因为主线程可能需要给不止一个次线程分配任务。比如说我想让 1 线程算 1-100,让 2 线程算 101-200……也就是可以让每个线程并行地去共同完成同一个任务,而我只需要将需要处理的数据通过类告诉他们就行,最后我再对结果进行汇总(主线程重分配和管理,次线程重实践)。
甚至你还可以把方法都写进类里面,这样你的线程就更简洁了。
你次线程需要什么类,需要什么方法,我可以通过类来告诉你,你只管调用就行。
三、线程等待 pthread_join
你主线程把我新线程创建出来了,你不得管我吗?万一我还没退你先退了怎么办?
所以要尽量保证主线程最后退。
怎么让主线程最后退呢?你可能会想到写个死循环,然后把工作都交给次线程去干。这是这样真的好吗?我只是想让你管理我,不是想让你当甩手掌柜然后自己啥代码也不执行,而且我要是自己退了,你就搁那傻傻循环啥也不管吗?你难道不关心我的运行结果吗?你难道不需要释放我的空间吗?
所以你主线程必须要等待子线程(1、将已经退出的线程的空间释放掉 2、创建新的线程时不会复用刚在退出线程的地址空间)。
功能:等待线程结束
int pthread_join(pthread_t thread, void **value_ptr);参数:
thread: 线程 IDvalue_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 参数。

问题是为什么是 void** 呢?
因为 OS 作为管理者也需要知道执行结果,这个执行结果会先被携带结构体里,然后我们可以通过二级指针将我们自己的 void* 变量地址传递给他,然后把他拷贝过来。

四、线程分离 pthread_detach 和 pthread_self
默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join(需要由主线程回收)操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join 是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
讲个小故事理解分离:
比如说五六十年代那个时候,很多家庭里面会一次性有很多小孩由父母管理,然后这些小孩长大以后,有其中一个小孩跟父亲特别不对付(一种情况是小孩自己提出分家——子线程自己想分离,还有一种情况是父亲嫌弃你让你离开——父线程要求你分离),而这个小孩虽然可以共享家里的一部分资源,但是其实已经不是一家人了。所以不管你以后怎么样了,父亲都不会管你了(就相当于线程分离之后,虽然他还可以用到进程的公共资源,并且他也有自己 idea 资源,但是父线程已经不关心他了,此时以后不管怎么都没人管他,只能自生自灭由 OS 去回收他)。
所以可以由线程组内的其他线程对目标线程进行分离,也可以是线程自己分离。
pthread_detach:
int pthread_detach(pthread_t thread);
pthread_self:
pthread_t pthread_self(void); // 可以获得线程自身的 ID
joinable 和分离是冲突的,一个线程不能既是 joinable 又是分离的。本质上就是将我们线程库中我们认为的 tcp 结构体里的一个关于线程是否分离的标记位给改了。
五、线程终止 pthread_exit 和 pthread_cancel
只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数 return。这种方法对主线程不适用,从 main 函数 return 相当于调用 exit。
- 线程可以调用 pthread_exit 终止自己。
- 一个线程可以调用 pthread_cancel 终止同一进程中的另一个线程。
pthread_exit 函数
功能:线程终止
void pthread_exit(void *value_ptr);参数:
value_ptr: value_ptr 不要指向一个局部变量(独立栈空间会被释放)。返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(线程都终止了返回没有意义)。
pthread_cancel 函数
功能:取消一个执行中的线程
int pthread_cancel(pthread_t thread);参数:
thread: 线程 ID返回值:成功返回 0;失败返回错误码
六、C++ 的线程库
C++ 其实也有自己的线程库 thread。可实际上他的底层也是封装的 pthread 的原生线程库,也需要指定链接。


而以往我们在 Windows 系统下在 VS 中使用线程库,我们其实并不需要这样,这是因为 Windows 下他有自己专门的线程库。因为 Windows 实现的时候就是有专门的 TCB 结构体,所以我们包 cpp 头文件的时候,他的底层其实是 Windows 类型的系统调用。
C++ 具有跨平台性,根据不同的平台(Linux 和 Windows),他用的是条件编译。外面虽然呈现出来的头文件和接口是一样的,但是不同的平台内部封装所使用的系统调用是不一样的。
所以在 Linux 下的 C++ 底层封装的是 Linux 的原生线程库(由于是用的进程模拟线程,所以并没有专门的 TCB 结构体,他的系统调用接口只有轻量级进程的概念,所以又封装了一个原生线程库给我们,而使用第三方库都需要链接,C++ 底层也是这个原生线程库,所以也要链接)。而 Windows 下的线程库就是原生 Windows 下的系统调用,所以他并没有第三方库的概念。
所以你平时写代码在不同的环境下没有感觉,是写库文件的设计者帮助你把这种差异给屏蔽掉了。所以我们平时推荐使用语言里的库方法而非系统调用接口,因为这样代码就不具备可移植性和跨平台性了。
七、用户级线程 vs 内核级 LWP
用户级线程和内核级 LWP 是 1:1 的关系。
线程共享进程数据,但也拥有自己的一部分数据:
- 线程 ID
- 一组寄存器(保存上下文)
- 独立栈(完成调用链)
- errno(局部存储)
- 信号屏蔽字
- 调度优先级
进程的多个线程共享同一地址空间,因此 Text Segment、Data Segment 都是共享的。如果定义一个函数,在各线程中都可以调用;如果定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式 (SIG_IGN、SIG_DFL 或者自定义的信号处理函数)
- 当前工作目录
- 用户 ID 和组 ID
- 线程的概念是库给我们维护的,所以线程库注定要维护多个线程属性集合。先描述再组织。
- 不用维护线程的执行流,这是由 OS 的轻量级进程完成的(已经帮我们封装了)。

- 原生线程库必然要被加载到内存中,因此我们的线程属性集合也应该在线程库中维护。

- 线程控制块就是库帮我们维护的一个用户级线程结构体 tcb,而 pthread_t 类型的线程 ID,本质就是一个进程地址空间上的一个地址,就是指向他的。

7.1 独立线程栈
执行流的本质就是独立的调用链。所以每个线程都需要建立自己独立的调用链,所以就必须得有一个独立的栈结构。
- 支持我们在应用层来完成我们整个调用链对应的临时空间的开辟和释放
- 这样对于局部变量来说,就可以保持线程的独立性
虽然是独立栈,但其实其他线程想要访问在技术角度也是可以做到的(定义一个全局的指针,然后在某一个线程中让他保存其中的一个局部变量的地址,然后主线程再当全部线程创建完成之后,再去查看这个全局的指针变量),因为线程与线程之间几乎没有秘密。
7.2 局部存储
如果我们想要一个只属于线程的全局变量呢?通过局部存储(他会被存储在一个区域中)。


问题:可是这看上去很鸡肋啊!干嘛要定义这种私有的全局变量啊,我直接在自己的独立栈定义局部变量不就行了?
可是如果你的线程内部将来也调用函数了呢?比如说你想让别的函数也能够知道你线程的 id 或者是其他属性,那你还得把这个局部变量通过参数传递给他。所以局部存储私有的全局变量最核心的意义就是可以让该线程独立栈内部调用链上所有的函数都可以看得到这些信息,而不需要传参或者是频繁地调用系统调用。


