【Linux】初始线程
目录
🌟一、线程概念
🌟二、pthread原生线程库
🌟三、线程 VS 进程
3.1 线程切换轻量化原理
3.2 线程私有数据
3.3 线程优缺点
1)、线程优点
2)、线程缺点
🌟四、线程相关函数及功能
4.1 线程创建--create
4.2 获取自身线程id--self
4.3 线程取消--cancel
4.4 线程终止--exit
4.5 线程等待--join
4.6 线程分离--detach
🌟五、完结
🌟一、线程概念
一个进程需要访问的大部分资源,如代码,数据,new\malloc 开辟的空间数据,命令行参数,环境变量等,甚至是系统调用的内核代码。进程都是通过虚拟地址空间来访问的,也就是进程地址空间,换而言之,进程地址空间就是进程的资源窗口。
进程的创建是复杂的,首先要创建进程的PCB,进程地址空间,页表,再将进程自身的代码和数据换入内存中,并进行虚拟地址和物理地址的映射。接着将进程的状态改为R运行状态,再把进程的 PCB 挂在运行队列中排队。
但如果现在已经存在一个进程了,我们把这个进程的PCB复制多份,然后让所有 "进程" 的 PCB 全都指向同一个虚拟地址空间,然后通过技术手段,将这份虚拟地址空间合理分配给每个"进程",当 CPU 调度该 "进程" 时,每个 "进程" 只会调用其中一部分的代码和数据,也就是执行我们所要完成的一小部分。 这样既不影响最终结果,也会减轻单个进程的负担,相当于把一个任务拆分成几个小任务完成。
我们把这种 "进程" ,比传统进程更轻量化的进程,叫做线程。
线程是在进程内部执行,比进程更加轻量化的一种执行流!
🌟二、pthread原生线程库
在Linux中,如果Linux真的支持线程,那么就会存在大量的线程,操作系统要对这些线程进行管理,那就是进行 先描述,再组织 。即操作系统要创建一个结构体struct TCB,这样就可以将这些线程用链表的形式管理起来。
1、 线程又是进程内部的一种执行流,所以要将线程和进程进行耦合。
2、 线程同样也需要被调度,所以对线程的调度有属于自己的调度算法。
进程的 PCB 和线程的 TCB 所使用的属性其实是差不多的,都需要pid、状态、调度优先级…并且两者都是需要被调度被运行的,所以其实可以用PCB来代替TCB。
PCB包含操作系统管理进程所需的各种信息,将其用于线程管理时,可把线程视为轻量级进程。这样能以统一的视角看待进程和线程。当操作系统进行线程调度时,依据PCB中的优先级字段,选择优先级最高的线程执行,就如同对进程的调度一样。
我们将这种直接创建PCB,然后让所有PCB指向同一个地址空间,在对资源进行分配的线程实现方式称为Linux中线程的实现方案!!我们也将这种线程称为轻量级进程!!
在Linux中没有真正意义上的线程,Linux是用轻量级进程来充当线程。但这是Linux OS自身的独特设计,对于我们用户来说,只认识进程和线程。
所以我们在Linux中封装了一层软件层。该软件层向下调用轻量级进程的相关接口,向上为用户提供线程的控制接口,这层软件层叫做 pthread 原生线程库,所以我们在使用线程时,需要主动链接pthread库。
可以通过 ps -al 来查看系统线程
🌟三、线程 VS 进程
1、 线程是进程内部的一种执行流,线程比进程更加轻量化
2、线程是CPU调度的基本单位;而进程是资源分配的基本单位
3、进程拥有独立的进程地址空间和页表, 而线程是共享进程的进程地址空间,页表,和其他资源
4、进程的栈是由进程地址空间维护的,而线程的栈是由pthread维护,映射到共享区的
声明:共享区是虚拟地址空间的一段区域,共享区通过虚拟地址隔离使线程的物理内存完全独立
3.1 线程切换轻量化原理
CPU在调度执行某一行代码时,计算机会将该行周围的代码全部加载到CPU cache缓存中。而CPU需要访问内存时,优先访问的是缓存。如果数据在缓存中,访问速度会非常快,如果不在,那就从较慢的主内存中加载。
而cache缓存是根据虚拟地址来快速查找数据的,一旦进程切换,那么同一个虚拟地址对应的物理地址就会不一样,所以需要重新对数据进行热加载,即将数据加载到cache缓存中;
但对于线程切换来说,下一个线程极有可能还是访问的这些代码和数据,也就是说不需要更改虚拟地址,因为资源所在的物理地址是不变的!
除此之外,在CPU中还存在很多寄存器来存储进程/线程的上下文,存在一些寄存器保存的内容执行地址空间和页表。对于线程来说,地址空间和页表是相同的,这也意味着线程切换时我们不需要将所有线程的寄存器全部切换走;只需将少部分保存临时数据的寄存器切换即可!
那么轻量化的方面就体现出来了->:
- 需要切换的寄存器少!
- 不需要重新更新cache!
3.2 线程私有数据
因为线程是进程的内部执行流,所以线程和进程共享大部分数据,如代码段,数据段,全局数据等都是共享的。除了这些,还包括以下资源
1、共享当前进程的文件描述符表
2、每种信号的处理方式
3、当前工作目录
4、用户id和组id
当然,线程也会有自己私有的数据,具体如下->:
1、线程拥有独立的寄存器硬件上下文。即当线程被切换出去时,操作系统会把寄存器中的值(上下文)保存到该线程专属的数据结构中,因为线程是被独立调度的,所以每个线程必须拥有独立的上下文,比如运行中产生的临时数据。
2、线程都有独立栈结构。一般地址空间中的栈属于主进程,然后通过在堆上申请空间,充当其他新线程的栈空间
3、线程ID、信号屏蔽字、调度优先级、errno等(注:errno是一个全局变量,通过errno可以确定错误的类型)
3.3 线程优缺点
1)、线程优点
1、线程的创建,删除,切换比进程更加简单轻量,线程占用的系统资源比进程更少
2、对于计算密集型应用,线程可以利用多处理器并行的特点,将计算分解成多个线程并行处理,提高效率
3、对于I/O密集型应用,可以将I/O操作进行重叠,以线程同时等待不同的I/O操作,提高性能!
2)、线程缺点
- 如果线程的数量比处理器的物理核心多,相当于增加了额外的同步和调度开销,而可用的资源不变,导致性能损失!(CPU的物理核心不是无限的)
- 线程会导致整体程序的健壮性降低。如果存在一些错误导致线程异常,会导致进程整体异常退出!
- 线程缺乏访问控制!进程是访问控制的基本粒度。对于线程,OS中并没有提供相关的方法进行线程访问控制!
- 编写难度高!线程出现问题,通常是很难进行排查的。
🌟四、线程相关函数及功能
4.1 线程创建--create
【函数接口】:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);Compile and link with -pthread//编译时,需要链接pthread原生线程库返回值:成功返回0;失败返回错误码
thread:输出型参数,返回线程id。
attr:设置线程属性,为NULL表示使用默认属性。
start_routine:新线程执行方法的函数地址。
arg:传给start_routine函数的参数。
错误检查:传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
【实例】:
void* ThreadRoution(void* arg)
{std::cout << "I am new thread" << std::endl;const char* threadname = (const char*)arg;std::cout << threadname << std::endl;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoution, (void*)"thread-1");std::cout << "I am main thread" << std::endl;sleep(1);return 0;
}
4.2 获取自身线程id--self
pthread_t pthread_self(void);
4.3 线程取消--cancel
int pthread_cancel(pthread_t thread);返回值:成功返回0;失败返回错误码
4.4 线程终止--exit
线程退出有3种方式:return返回、pthread_cancel取消线程、pthread_exit()线程终止。(不能调用exit()函数,该函数用于进程退出,一般调用,所以相关线程将全部退出!)
【线程终止函数原型】:
void pthread_exit(void *retval);
retval:retval不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。
pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,即形参retval必须是全局的或者是malloc分配的。不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时会发现线程函数已经退出了,就是意思这个栈区的指针已经销毁了,那就是野指针的问题了。
4.5 线程等待--join
一般情况下,主线程需要对新线程进行等待,否则新线程就会一直占用资源,会导致类似于进程的“僵尸”问题,导致内存泄漏!
int pthread_join(pthread_t thread, void **retval);thread:需要等待的线程idretval:指向待等待线程的返回值
4.6 线程分离--detach
默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join 是一种负担,这个时候,我们可以将线程设置为分离状态,告诉系统,当线程退出时,自动释放线程资源!
线程组内其他线程对目标线程进行分离,也可以是线程自己分离!但 joinable 和分离是冲突的,一个线程不能既是 joinable 又是分离的!
int pthread_detach(pthread_t thread);
- 线程分离后是可以cancel的,但不能join ! !