【Linux】基础:线程的概念
摘要:本文介绍Linux下的线程概念,首先将会线程在系列教材中的定义进行抛出,从常规的操作系统进行理解线程的概念,在具体说明Linux下的进线程的管理与组织方式,以及由于该组织方式的差异,导致的接口和资源的特殊关系。并通过实验进行简单的验证。
文章目录
- 【Linux】基础:线程的概念
- 一、概述
- 二、进程与线程
- 三、接口关系
- 四、资源关系
- 五、简单验证
- 六、其他
一、概述
在以往学习过程中,一般常见对于线程的概念为:在进程内部运行的一个执行分支(执行流),是属于进程的一部分,粒度要比进程更加细化和轻量化。
对于常见的操作系统,例如Windows操作系统,一个进程可能会存在多个线程,而操作系统就需要对这些线程进行管理,管理的方式与进程类似,通过相应的进程控制块来记录线程相关的属性,一般称为TCB
即线程控制块。可是对于Linux有着其他的线程组织方式。
对于Linux来说,并没有线程TCB的概念,而是通过把线程称组织为“轻量级线程”,同样使用结构体PCB进程控制块,使用PCB来模拟线程。在Linux中,只创建task_struct
,共享同一个地址空间,将当前进程的资源(代码和数据),划分成若干份,提供每个PCB使用。
此时中央处理器CPU认知的PCB并不是过去所学习的进程的PCB,而是“轻量级进程”的PCB,一个PCB就是对应了一个需要被调度的执行流 。为此不用维护复杂的进程和线程的关系,不用单独的为线程设计任何算法和数据结构,直接使用进程的一套方法即可。只需要聚焦在线程间的资源分配上可以完成对线程的管理与组织。
其组织示意图如下(红框表示进程):
二、进程与线程
在Linux中对于进程的理解需要做出改变,对于过去进程的理解,是只有一个执行流的,可引入了线程的概念后,进程的内部可以具有多个执行流。
对于进程来说,创建的成本是非常高的,是需要较多的时间和空间的的,而且创建过程中要使用的资源非常多。但是对于线程的创建,只需要创建相应的PCB,并建立与进程地址空间的对应以及完成进程资源的分配。线程也是CPU调度的基本单位,承担进程资源的一部分基本实体,进程划分空间给线程。
线程与进程比较概述:
调度:进程是拥有资源和独立调度(传统操作系统)的基本单位,调度开销大;线程是独立调度的基本单位,开销小。
并发性:相互间可以并发,提高并发性,提高系统资源的利用率和系统的吞吐量;
拥有资源:进程系统中拥有资源的基本单位,而线程不拥有系统资源,但线程隶属于进程的资源。体现:同一进程的所有线程都有相同的地址空间。
独立性:每个进程都拥有独立的地址空间和资源,除了共享全局变量,其他进程不可以访问。而一进程的线程对其他进程不可见,对同一进程的不同线程则是共享进程的地址空间和资源;
系统开销:创建、切换、通信线程的开销都比进程小;
支持多处理机系统:传统单线程进程,不管有多少处理机,进程只能运行一个处理机;而多线程进程,可以多个线程分配多个处理机上执行;
三、接口关系
对于Linux来说,由于是使用了进程进行模拟,所以Linux下不会提供直接的操作线程的接口,所提供的是统同一空间内创建PCB的方法,分配资源给制定的PCB的接口。但是这种方式对于用户来说,是非常不友好的。为此,需要系统级别的工程师,在用户层对Linux轻量级进程进行接口封装,打包成库,让用户直接通过库调用接口。相关接口包括线程创建、线程等待、线程分离等,将在后续文章进行介绍。
由于使用了外部库,因此在进行接口调用时,需要对头文件和编译添加相应的内容。
四、资源关系
所有轻量级进程是在进程地址空间内部运行的,在进程地址空间中标识了大部分资源。对于各线程来说,大部分进程资源是共享的,对于在使用同一块地址空间而言,Text Segment、Data Segment都是共享的,对于函数和全局变量等在进程地址空间的数据也可以调用,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
可是也有会部分是私有资源,比如:栈、PCB和上下文,具体如下:
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
五、简单验证
在此使用接口pthread_create()
创建线程,具体创建过程将会在其他文章中介绍,并通过shellps -aL
命令,查看进程与线程的数量。可以发现进程是只有一个的,而对于线程,Linux将其称为light weight process)
即LWP
,是有多个的,代码如下:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void* thread_run(void* args){
const char *name = (const char*)args;
while(1){
printf("%s: pid---->%d\n",name,getpid());
sleep(1);
}
}
int main(){
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"No.1 thread");
while(1){
printf("Main thread: pid---> %d\n",getpid());
sleep(1);
}
}
[root@VM-12-7-centos Blog_pthread]# ps -aL
PID LWP TTY TIME CMD
135308 135308 pts/3 00:00:00 test_pthread
135308 135309 pts/3 00:00:00 test_pthread
六、其他
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型,线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高:编写与调试一个多线程程序比单线程程序困难得多
线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
补充:
- 代码将会放到: https://gitee.com/liu-hongtao-1/c–c–review.git ,欢迎查看!
- 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!