文章目录
- 一、线程的优点
-
- 1. 创建的代价
- 2. 切换的代价
-
- 缓存和进程/线程切换
- 3. 占用的资源
- 4. 效率
- 二、线程的缺点
-
- 1. 性能损失
- 2. 健壮性降低
- 3. 缺乏访问控制
- 4. 编程难度高
- 三、线程分离
-
- 1. 线程分离
- 2. pthread_detach ()
-
- ① 函数细节
- ② 函数使用
- 四、线程自有和共享的数据
-
- 1. 线程自有的数据
- 2. 线程共享的资源
- 五、多个线程使用公共空间
-
- 1. 不要把公共空间传给多个线程
-
- 1.1 问题
- 1. 2 解决
- 2. 线程的传参和返回值,可以是各种对象
- 六、C++11 中的线程
-
- 1. C++11 的多线程
- 2. thread 类及使用
- 七、线程库对线程的管理
-
- 1. 线程的管理由谁来做?
- 2. 线程管理的细节
-
- ① 线程库首先要映射到当前进程的地址空间中
- ② 进程地址空间中的动态线程库
- ③ 整个系统的所有的线程都在这一个库中管理着
- 3. 执行流怎么找到栈的?
- 八、线程的封装
-
- 1. Pthread.hpp
- 2. test.cc
一、线程的优点
1. 创建的代价
创建一个新线程的代价要比创建一个新进程小的多。
创建线程只需要创建一个新的 PCB
,然后将曾经进程的资源指派给线程即可;而进程创建的时空成本是较高的,有各种数据结构的创建,数据的加载。
2. 切换的代价
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
缓存和进程/线程切换
- 在
CPU
内,每次读取当前进程的代码和数据时,都需要经过虚拟到物理的转换,然后得到内存中的数据放到CPU
中做处理,如果这样,那CPU
读取任何一条指令,都要访问内存,为了提高运行效率,CPU
中存在一种硬件cache
(高速缓冲存储器),在CPU
进行虚拟到物理的寻址时,本来是找到一行代码,较大概率会执行下一行(当然也可能会跳转到别处),所以会将周边数据全读到CPU
内部缓存起来,所以,之后CPU
再访问代码数据时,不用再去读取内存了,而是从cache
中再读取,大大提高CPU
寻址的效率。
- 进程切换和线程切换:
-
进程切换:
之前缓存的数据,就全都没有了,会重新加载新进程的数据 -
线程切换:
cache
中的数据在概率上依旧能用,不用重新清空和重新加载。
-
所以这才是线程成本低的主要原因。
3. 占用的资源
线程占用的资源要比进程少很多。
进程要占用多执行流,地址空间,页表,代码数据;而线程占用的都是一份的。
4. 效率
- 计算密集型应用:
排序、查找、加密、解密、压缩等动作都以计算为主,是计算密集型应用,主要应用的CPU
的资源。 - I/O 密集型应用:
下载、上传、拷贝等就是I/O
密集型应用。
应用要不就是计算密集型,要不就是 I/O
密集型,要不就是两者都是。
- 对于计算密集型应用:
- 为了能在多处理器系统上运行,将计算分解到多个线程中实现。一个线程处理一部分,效率会高。
- 当然也不是创建的线程越多越好,因为会有切换的成本,切换太多,说不定效率还不如一个进程从头算到尾。
- 所以,对于计算密集型,一般是
CPU
有多少个核就创建几个。
可以通过lscpu
查看一下核数:
- 对于I/O 密集型应用:
- 可以多创建进程,因为
I/O
过程大部分都在等,一个进程等10G
的数据,和10
个线程每个等1G
的数据,效率不一样。
- 可以多创建进程,因为
二、线程的缺点
1. 性能损失
多线程切换有成本,像上面的计算密集型应用,线程过多可能会有较大的性能损失(增加了额外的同步和调度开销,而可用资源不变)。
2. 健壮性降低
一个线程出问题,整个进程就挂掉了(这一点,线程 - 线程退出 中有讲到)。
在一个多线程程序里,因共享了不该共享的变量而造成不良影响的可能性很大。
3. 缺乏访问控制
线程间共享资源,对于共享资源,大家都可以任意时间访问,可能会有同时访问同一量的情况,可能会出错。
4. 编程难度高
编写与调试一个多线程程序比单线程程序要困难很多,往往需要全面深入的考虑以保证程序的健壮性。
三、线程分离
1. 线程分离
线程默认是 joinable
(可联接) 的,如果主线程不关心新线程的执行信息,可以将新线程设置为分离状态。
当线程处于分离状态后,退出时会自动释放线程资源。
- 分离和等待:
线程被分离后,就不能被pthread_join()
了,调用这个函数,函数就会返回一个错误码。 - 分离状态:
‘分离’仅仅是一种状态,一种不用主线程等的状态,而不是真正分离了。在这种状态下,线程出异常了,进程会受影响退出;主线程执行完了,进程退出,线程也会退出。
不管是分离还是等待,我们都希望主线程是最后退出的,所以分离的一个场景,就是主进程根本就不退出。
大部分程序都是一直在运行的,称为常驻进程。
2. pthread_detach ()
① 函数细节
thread
参数:要分离的线程的ID
。- 返回值:成功返回
0
,失败返回错误码
② 函数使用
- 主线程分离新线程:
#include <pthread.h>
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
using std::cout;
using std::endl;
void *thfunc(void *arg)
{
while (1)
{
sleep(1);
cout << "new" << endl;
}
return nullptr;
}
int main()
{
pthread_t rid;
pthread_create(&rid, nullptr, thfunc, nullptr);
pthread_detach(rid);
sleep(3);
cout << "man" << endl;
return 0;
}
运行结果:
- 新线程自己分离自己:
void *thfunc(void *arg)
{
pthread_detach(pthread_self());
while (1)
{
sleep(1);
cout << "new" << endl;
}
return nullptr;
}
int main()
{
pthread_t rid;
pthread_create(&rid, nullptr, thfunc, nullptr);
sleep(3);
cout << "man" << endl;
return 0;
}
运行结果:
- 在线程分离后,再通过
pthread_join()
等待线程会等待失败,函数会返回一个错误码:
四、线程自有和共享的数据
进程是资源分配的基本单位,线程是调度的基本单位。进程强调独立,线程强调共享。所以我们来看一下线程自有的和共享的部分。
1. 线程自有的数据
- 线程 ID: 是用户级别的,内核级的不使用线程
ID
,使用的是线程的LWD
。 - 硬件上下文: 每个线程都是被单独调度的执行流,所以要有自己的上下文数据,存放在一组寄存器中。
- 独立栈结构: 线程本质是在执行自己的函数,每个函数内都可以定义各种临时变量,临时变量都是存放在栈上的。每个线程都有独立的用户栈,不敢让它们一起用一块栈空间。
除了上面几个较为重要的,还有 errno
、信号屏蔽字、调度优先级等。
2. 线程共享的资源
- 地址空间
- 文件描述符表:因为文件描述符表表示的是进程和打开文件的关系,而不是线程。
- 每种信号共享的处理方式:这就是为什么一个线程出异常,整个进程就崩掉了。
- 当前工作目录:进程在哪里,线程就在哪里。
- 用户
id
和组id
。
五、多个线程使用公共空间
1. 不要把公共空间传给多个线程
我们来看下面这段代码:
我们想一次性创建 6
个线程,然后每一个线程都在自己的线程函数中打印一下自己的线程名字(从 1
到 6
号)。
#include <pthread.h>
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
using std::cout;
using std::endl;
const int threadsnum =