目录
[3.1 线程创建](#3.1 线程创建)
[3.2 线程退出](#3.2 线程退出)
[3.3 线程等待](#3.3 线程等待)
[3.4 线程分离](#3.4 线程分离)
[3.5 线程ID](#3.5 线程ID)
[4.1 线程的优点](#4.1 线程的优点)
[4.2 线程的不足](#4.2 线程的不足)

一、引言
本文将介绍线程的基本概念、如何创建线程和线程切换与进程切换的开销,在此基础上理解为什么有了进程还要引入线程,最后完成一道综合题来熟悉线程的相关操作。
题目如下:
1)在主线程中启动线程1,打印"This is thread1!"。
2)在主线程中启动线程2,打印"This is thread2!"。
3)线程1需要等待线程2结束,打印"Thread1 wait thread2 succeed!"。
4)主线程回收线程1,打印"Main thread wait thread1 succeed!"后退出。
在文章结束后,这道题大家就都会做了。
二、线程概念
进程里的一个执行流就叫做线程(thread),它是操作系统能够调度的最小单位。在接触线程前,我们所写的代码全部都是单线程,也就是说一个进程里只跑了一个线程,这个线程就叫做主线程。
三、线程相关函数
下面的所有函数都是pthread库提供的,所以这些函数有一个共同特点------函数名都以pthread开头。在编译的时候,要带上"-lpthread"选项,指定要链接的库名称。
3.1 线程创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void*), void *arg);
- pthread_t *thread:指向线程ID的指针。
- pthread_attr_t *attr:通常设为空,表示使用默认属性。
- void *(*start_routine)(void*):参数为void*,返回值也为void*的函数指针,线程函数的入口点,表示线程从这个函数开始执行。
- void *arg:传递给start_routine的参数。
- 返回值:线程创建成功返回0,失败返回错误码。
这个函数是专门用来创建线程的。
3.2 线程退出
#include <pthread.h>
void pthread_exit(void *retval);
- void *retva:线程入口函数的返回值。
哪个线程调用这个函数,该线程就会退出。
3.3 线程等待
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
- pthread_t thread:要等待的线程ID,即创建线程时的第一个参数对应的值。
- void **retval:用于存储线程函数的返回值,返回值为一级指针,所以用二级指针来指向。
- 返回值:成功返回0,失败返回错误码。
已经退出的线程,仍然占用进程的地址空间,而且新创建的线程也不会去复用这块空间,所以需要将这块空间回收,pthread_join函数干的就是这个。哪个进程调用pthread_join函数,该进程执行到这个函授后就会被挂起,直到指定要等待的线程终止,回收它的空间后,才能继续向下执行。
3.4 线程分离
#include <pthread.h>
int pthread_detach(pthread_t thread);
- pthread_t thread:要分离的线程ID,即创建线程时的第一个参数对应的值。
- 返回值:成功返回0,失败返回错误码。
默认情况下,我们创建的新线程是要被join的,否则会占用进程内部的地址空间。如果我们不关心线程的返回值,我们就可以调用pthread_detach函数,将线程设置为分离状态,告诉操作系统,在该线程退出后,帮我释放该线程持有的资源。在进程部分,我们可以将创建出来的子进程托孤,交给操作系统来管理,在该进程退出后释放它占有的资源。
3.5 线程ID
#include <pthread.h>
pthread_t pthread_self(void);
- 返回值:线程ID。
哪个线程调用这个函数,就返回该线程对应的ID。
四、进程与线程的对比
已经有了进程,为何引入线程?
4.1 线程的优点
1)创建一个新线程的代价要比创建一个新进程小得多。
一个进程内部,可以有多个执行流,也就是可以有多个进程。这些线程共用进程的地址空间、页表等。因为线程不需要开辟新的地址空间、页表等,所以创建一个线程的代价当然要比创建一个进程的代价低得多。
2)与进程切换相比,线程切换的代价要少很多。
每个进程都是有自己独立的地址空间的,切换的时候需要保存和恢复大量的上下文信息,比如进程的代码段、数据段、打开的文件描述符等。
进程的切换会扰乱缓存机制。以进程A切换到进程B为例,一旦切换,CPU的高速缓存基本作废。因为每个进程它都有独属于自己的代码和数据,我进程B凭什么要去访问你进程A的代码和数据(非共享资源)。在进程切换的时候,快表TLB也要被刷新,快表里面存的就是一些虚拟地址到物理地址的映射,内存管理单元MMU首先拿虚拟地址去查快表,如果命中了,那么就直接找到了想要的数据在内存中的位置,而不用在经过页表的转化。所以,快表刷新以后,在一段时间内,CPU访问内存的效率是非常的低。
而线程的切换,只需要保存少量的寄存器状态(线程自己的上下文),无需切换地址空间或刷新TLB。不同的线程也很有可能要访问相同的数据或者代码,所以缓存不会失效。
3)可充分利用多核CPU,更好挖掘硬件潜力。
多线程允许单个进程内并发执行多个任务,特别适合多核CPU环境。例如在GUI应用中,主线程保持界面响应,工作线程处理后台计算,避免界面"卡顿"。
4.2 线程的不足
1)健壮性降低。
一个进程内部,有多个线程,一个线程出问题,其他线程也要受到牵连。比如,进程内部有个线程A,它越界访问了或者有除零行为,那么操作系统就会识别到异常,发出干掉这个进程的信号,这个信号干掉的可不只有线程A,而是整个进程。
2)编程难度提高。
因为多线程容易出问题,自然需要考虑的更多。
到这,本节一开始就提出的问题便有了答案。
五、线程试题代码
经过上面的学习,我们对线程有了一定的认识,下面来完成一开始就提出的问题。
cpp
#include <iostream>
#include <pthread.h>
pthread_t thread1, thread2;
void* func1(void* arg) {
std::cout << "This is thread1!" << std::endl;
pthread_join(thread2, nullptr); //等待thread2结束
std::cout << "Thread1 wait thread2 succeed!" << std::endl;
return nullptr;
}
void* func2(void* arg) {
std::cout << "This is thread2!" << std::endl;
return nullptr;
}
int main()
{
pthread_create(&thread1, nullptr, func1, nullptr);
pthread_create(&thread2, nullptr, func2, nullptr);
pthread_join(thread1, nullptr);//等待thread1结束
std::cout << "Main thread wait thread1 succeed!" << std::endl;
return 0;
}

六、结语
本篇是关于线程的最基本理解与操作,除此之外还有线程同步和互斥等进阶玩法。
完~