Linux线程的概念
什么是线程?
- 在一个程序中一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,在CPU看来,PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
线程的概念:
linux下的线程是一个轻量级进程。在传统的操作系统下进程是pcb(task_struct结构体),而linux下的线程是通过进程pcb描述实现的,并且同一个线程组(pcb)中的线程共用同一个虚拟地址空间,因此linux下的pcb相较于传统的pcb更加轻量化。因此线程是轻量级进程。
所以说:线程是cpu调度的基本单位,进程是系统资源分配的基本单位,这句话的解释:linux下线程是pcb,cpu调度程序运行时通过pcb调度的;而进程(线程组)是资源分配的基本单位,共同使用虚拟地址空间、页表等信息。
对比多进程与多线程进一步理解线程的概念:
多进程处理:启动一个进程处理一个任务,多个pcb进行程序调度完成多个任务,每个pcb都有一个自己独立的虚拟地址空间
多线程:有多个pcb共同调度着同一虚拟地址空间,相当于一个身体有多个头
虚拟地址空间中代码段就是要完成的任务,代码段里面有多个函数,不同的线程pcb调度着不同的函数同时完成多个函数(pcb在之前是进程)
线程的优点
- 创建一个线程的代价要比创建一个新进程小的多
- 与进程之间的切换相比,线程之间的切换需要操作系统的工作要小很多
- 线程占用的资源要比进程小很多
- 能充分利用的多处理器的并行数量
- 计算密集型IO应用,为了能在多处理系统上运行,将计算分解到多个线程中实现(见下文讲解)
- IO密集型应用,为了提供效率,将IO操作并行处理。线程可以同时等待不同的IO操作(见下文讲解)
多线程和多进程进行多任务处理的优缺点分析
多线程的优点(多线程用的多)
- 线程间共用一个虚拟地址空间,因此线程间的通信更加方便灵活(全局变量、 函数重载、传参)也能进行线程间通信
- 线程的创建与销毁成本更低—创建进程还需要创建页表,进行分配资源
- 线程的切换调度成本更低—不需要切换页表,避免页表切换
- 线程间共用进程的大部分资源,因此创建和销毁成本更低,调用成本也更低
多线程的缺点
- 线程间缺乏访问控制,某些异常针对于整个进程发生效果(如exit()退出一个进程),进程就是线程组,进程退出,造成所有线程退出,产生段错误
多进程的优点
- 稳定,健壮性高。适用于对主程序安全稳定性更高的场景:shell/服务器(主程序不能挂,得找子程序背锅)。在多线程中,一个线程若访问了一块非法的地址,针对于整个进程,这些信息都是共用的,发生错误信息则是针对于整个pcb(进程),整个进程都退出。
多线程和多进程进行多任务处理的优势在哪里?
在讲解多线程和多进程进行多任务处理的优点之前,我们不妨先来简单了解一下IO密集型程序和CPU密集型程序
IO密集型程序:在程序中进行着大量的IO操作,操作着其它的设备,意味着对CPU的消耗不高,大部分时间进行着IO就绪,拷贝数据
IO操作: IO等待 + 数据拷贝(
IO等待:
- 网卡上面获取数据,别人发了才有数据,别人不发则等待
- 磁盘IO读取数据,得磁盘磁头指向指定位置
数据拷贝:
拷贝到缓冲区,或从缓冲区拷贝到设备
CPU密集型程序:在程序中不断进行着数据运算
在进行CPU密集型程序时:
现在的CPU都是多核的,每个CPU都有自己的寄存器,并可以并行,在CPU很多的(资源足够)的时候,多线程和多进程都可以
在进行IO密集型程序时(如文件IO):
文件IO,等待+数据拷贝
多线程 就是多个执行流都可以对数据进行等待,多个IO操作同时发起,同时进行等待,并压缩了等待时间。
优点:提高运行效率 分摊压力
一个执行流就是一个线程(下图)
那么是不是线程越多越好?
线程不能太多,pcb会进行大量的调度切换。调度切换浪费的时间大于提高的效率 就得不偿失了。
线程间用了共同的虚拟地址空间,就是运用了相同的栈,就会出现调用栈混乱,多线程同样是这样,那么我们解下来来了解一下线程的独有与共享
线程的独有与共享
独有
- 每个线程都有自己的栈,防止调用栈混乱。
- 每个线程都是一个执行流,有自己的一套寄存器保存数据。
- 每个线程都是一个pcb,都有自己的
- 线程标识(描述符)
- 优先级
- errno(perror就是靠errno获得系统调用错误的信息)
- 信号屏蔽字(某些线程阻塞自己不想操作的信号,有些重要的执行流(pcb)不想被信号打断,想执行自己的任务,把某些特定的信号阻塞起来)
共享:
- 虚拟地址空间(代码段,数据段)
- 文件描述符表()
- 信号处理方式(每个线程的处理方式要是不一样,那么就不知道一个信号交给哪一个线程去处理)
- 当前进程的工作路径
- 用户ID,组ID
线程控制
操作系统并没有给用户直接提供创建一个线程的接口。(线程就是个pcb,轻量级进程)所以久封装了一套线程库,用于线程控制。封装了一套库,就调度内核的执行流所有的接口都是库函数接口。
通过用户线程(用户态实现的线程) 也是依靠 内核中的轻量级进程(线程)执行流完成调度的
注意:创建线程用的是库函数,意味着我们用的是动态库里面的信息,而它被映射到共享区
在这里我们引入一个共享区概念
共享区:共享的信息都会放到共享区 ,比如共享内存,物理内存开辟的一块空间,映射到虚拟地址空间上的共享区,分配一块虚拟地址。而且线程是用户态创建的线程,是用到动态库的信息,所以线程所用到的东西几乎都是在共享区
每个线程都在共享区有一块独立的空间,称为线程地址空间。线程地址空间就包含了库中封装的一系列描述信息,线程描述,用户态的栈区等等。这块空间依然在进程的虚拟地址空间内,相对独立
线程创建:
int pthread_create(pthread_t *tid, pthread_attr_t *attr,pthread_routine handler, void *arg)
第一个参数:tid是线程地址空间在进程虚拟地址空间的首地址 (获取一个id)
第二个参数:线程属性 还有一堆接口,需要属性设置接口去设置,使用非常麻烦,通常置空
第三个参数:函数指针:线程入口函数 ---- 告诉线程要完成什么代码(任务) —完成任务的最基本单元就是函数
第四个参数: 传输给线程入口函数的参数(第三个参数函数的参数)
返回值: 成功返回0,失败返回非0
线程终止:
- 线程入口函数中return----(main函数中的return是退出整个进程,整个进程退出,那么线程全部也退出)
- void pthread_exit(void *retval);—退出调用线程
- int pthread_cancel(pthread_t tid);—退出一个指定的线程
默认情况下,一个线程退出并不会自动释放资源,需要被等待
线程等待:
int pthread_join(pthread_t thread, void** retval);
等待一个线程退出,获取返回值,释放资源。线程有一个属性,默认情况下是joinable属性,线程退出后不会自动释放资源
**线程分离:**分离一个指定的线程。将线程的属性从joinable设置成detach,表示分离一个线程,被分离,处于detach属性的线程退出后,则自动释放资源,不需要被等待。
int pthread_detach(pthread_t thread);