目录
- 1、Linux线程
- 1.1 线程的优缺点
- 1.2 线程异常和用途
- 1.3 线程等待
- 1.3 线程终止
- 1.4 线程分离
- 1.5 线程ID和地址空间布局
- 1.6 线程栈
1、Linux线程
- 进程是一个执行起来的程序,进程 = 内核数据结构 + 代码和数据。进程是承担分配系统资源的实体。
- 线程是执行流,执行粒度比进程更细,是进程内部的一个执行分支。线程是OS调度的基本单位。
进程是承担分配系统资源的基本实体,线程不参与分配,只需要划分进程的资源就可。新线程和主线程谁先运行,是不确定的。线程创建出来,要对进程的时间片进程瓜分。
也就是说进程执行的任务还要被细分成几份给更小的线程去执行,可以想象到线程除了执行粒度比进程更细之外大致相似,也就是说OS也要像管理进程一样管理线程,如果用像管理进程一样的方法管理线程,那么就太复杂了。
Linux执行流,统一称为轻量级进程(LWP),Linux中没有真正意义上的线程,Linux线程概念使用LWP模拟实现。
//新线程
void *run(void *args)
{
while (true)
{
cout << "new thread, pid : " << getpid() << endl;
sleep(1);
}
return nullptr;
}
int main()
{
cout << "I am a process, pid : " << getpid() << endl;
pthread_t tid;
pthread_create(&tid, nullptr, run, (void*)"thread_1");
//主线程
while (true)
{
cout << "main thread, pid : " << getpid() << endl;
sleep(1);
}
return 0;
}
虚拟内存可以将不连续的物理内存通过映射转化为连续的。当我们讨论虚拟地址到物理地址转换的时候,页表的映射关系已经在加载的时候关联好了。虚拟地址到物理地址转换的过程,是硬件(MMU)自动完成的。
:》如何理解进程划分资源给线程,如何做到?需要刻意做吗?
线程执行不同的入口函数,就拥有了这个函数的地址,资源自然而然就被划分好了。
虚拟地址和物理地址如何转换:
页表可以看作一个简单的数组,前20位用来映射物理地址,后12位作为标记位。
:》所以我们怎么知道当前内存是否被使用呢?
通过页表的映射我们可以得到页框的起始地址,在用起始地址除以4KB,就得到了管理物理内存数组的下标,通过下标也就找到了管理这个内存的信息。
缺页中断:当程序代码量大时只加载部分代码,执行完这部分代码后再往后执行就会在页表的标记位(是否命中)中发现错误,CPU出错触发软中断,发送信号,OS对这个信号的处理就是加载剩余代码,这个过程就是缺页中断。
1.1 线程的优缺点
| 线程优点:
-
创建一个线程的代价比创建一个进程的代价小的多。
-
与进程之间的切换相比,线程之间的切换需要OS做的工作要少很多。
- 最主要的区别是线程的切换虚拟内存空间依然是不变的,而进程切换会改变,这两种切换都是操作系统内核完成的,最显著的消耗是需要将寄存器中的内容切换。
- 另一个显著的消耗就是上下文的切换会扰乱处理器的缓存机制,当改变虚拟内存空间的时候,处理的页表缓冲TLB(快表)(缓存高频的虚拟到物理的映射)和cache(缓存代码和数据块)也会被全部刷新。
-
线程占用的资源比进程少。
-
能充分利用多处理器的可并行数量。
-
在等待慢速IO操作结束的同时,程序可执行其他的计算任务。
-
将计算分解到多个线程中实现,还可以同事等待不同的IO操作。
| 线程缺点:
- 如果线程数量过多会有调度的性能损失。
- 健壮性降低,线程之间是缺乏保护的。
- 缺乏访问控制,在一个线程中调用某些OS函数会对整个进程造成影响。
1.2 线程异常和用途
-
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
-
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
-
合理的使用多线程,能提高CPU密集型程序的执行效率。
-
合理的使用多线程,能提高IO密集型程序的用户体验。
1.3 线程等待
| 为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join
得到的终止状态是不同的:
- 如果thread线程通过
return
返回,retval
所指向的单元里存放的是thread线程函数的返回值。 - 如果thread线程被别的线程调用
pthread_cancel
异常终止,retval
所指向的单元里存放的是常数PTHREAD_CANCELED
。 - 如果thread线程是自己调用
pthread_exit
终止的,retval
所指向的单元存放的是传给pthread_exit
的参数。 - 如果对thread线程的终止状态不感兴趣,可以传NULL给
retval
参数。
1.3 线程终止
如果需要只终止某个线程而不是终止整个进程,有三种方法:
- 线程调用
return
退出。 - 线程调用
pthread_exit
退出。 - 线程调用
pthread_cancel
取消线程。
任何地方调用exit,表示进程退出。
取消线程,一定是目标线程已经启动了,join
得到的返回值是-1。
需要注意,pthread_exit
或者return
返回的指针所指向的内存单元必须是全局的或者是用malloc
分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
1.4 线程分离
- 默认情况下,新创建的线程是
joinable
的,线程退出后,需要对其进行pthread_join
操作,否则无法释放资源,从而造成系统泄漏。 - 如果不关心线程的返回值,
join
是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
主线程可以不等待新线程,将目标线程设置为分离状态。线程可以被分离,也可以自己分离。
| 线程被等待的状态:
joined
:线程需要被join(默认)detach
:线程分离(主线程不需要等待新线程)
注意:在多执行流情况下,主执行流是最后退出的。
这里执行5秒后
join
失败,进程退出,所以线程分离后就不能join
了。
线程不能直接进行程序替换,因为其他部分代码可能正在被其他线程执行,但是可以在线程内部创建子进程,进行程序替换。
1.5 线程ID和地址空间布局
pthread_create
函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
- 线程属性在线程库中维护,就是TCB,线程ID就是线程库中属性在地址空间中的地址。
- 主线程的栈只有一个,其他线程的栈是在共享区中被动态申请出来的。
线程局部存储只能修饰内置类型。
1.6 线程栈
虽然Linux将线程和进程不加区分的统一到了task_struct
,但是对待其地址空间的stack
还是有些区别的。
- 对于Linux进程或者说主线程,简单理解就是main函数的栈空间,在fork的时候,实际上就是复制了父亲的
stack
空间地址,然后写时拷贝(cow)以及动态增长。如果扩充超出该上限,则栈溢出会报段错误(发送段错误信号给该进程)。进程栈是唯一可以访问未映射页而不一定会发生段错误⸺超出扩充上限才报。 - 对于主线程创建的子线程而言,其
stack
不再是向下生长的,而是事先固定下来的,线程栈一般是调用glibc
的pthread
库接口pthread_create
在共享区创建的。也就是说这种栈一旦用尽就没了,这是和创建进程的fork
不同的地方。 - 对于子线程的
stack
,它其实是在进程的地址空间中map
出来的一块内存区域,原则上是线程私有的,但是同一个进程的所有线程生成的时候,是会浅拷贝生成者的task_struct
的很多字段,如果愿意,其它线程也还是可以访问到。
使用容器管理多线程:
#include "Thread.hpp"
#include <unordered_map>
#include <memory>
#define NUM 10
using thread_ptr_t = std::shared_ptr<ThreadModule::Thread>;
//创建多线程
int main()
{
std::unordered_map<std::string, thread_ptr_t> threads;
for (int i = 0; i < NUM; i++)
{
thread_ptr_t t = std::make_shared<ThreadModule::Thread>([](){
while (true)
{
std::cout << "hello world" << std::endl;
sleep(1);
}
});
threads[t->Name()] = t;
}
for (auto &thread : threads)
{
thread.second->Start();
}
for (auto &thread : threads)
{
thread.second->Join();
}
return 0;
}
本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~