一、线程的概念
1.理解线程
在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
一切进程至少都有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
之前的进程创建常画的图:
一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表等的一些数据结构;当把磁盘中的数据和代码加载进内存中后,虚拟地址和物理地址就是通过页表建立映射的 ;
此时我们再创建一批"进程",并不创建地址空间,只创建task_struct,共用第1个进程的地址空间,创建的效果如下:
此时我们创建的就是3个线程:
- 其中每一个线程都是当前进程里面的一个执行流,也就是我们常说的“线程是进程内部的一个执行分支”。
- 线程在进程内部运行,本质就是线程在进程地址空间内运行,也就是说曾经这个进程申请的所有资源,几乎都是被所有线程共享的。
如何理解之前的进程?
观察上图,我们从现在理解进程就不能说一个task_struct结构了,一个进程它包含了进程地址空间、文件相关的属性、各种信号、页表等
从内核角度来理解进程:
进程:它是承担分配系统资源的基本实体。
线程:它是CPU调度的基本单位,承担进程资源的一部分的基本实体。
换言之,当我们创建进程时是创建一个task_struct、创建地址空间、维护页表,然后在物理内存当中开辟空间、构建映射,打开进程默认打开的相关文件、注册信号对应的处理方案等等。而我们之前接触到的进程都只有一个task_struct,也就是该进程内部只有一个执行流。反之,内部有多个执行流的进程叫做多执行流进程。
CPU如何看待task_struct?
CPU不管有多少条执行流,只看task_struct,你task_struct有1条执行流就是单执行流的task_struct,有多执行流,你就是多执行流的task_struct。如下图:
因此,CPU看到的虽说还是task_struct,但已经比传统的进程要更轻量化了。
Linux下并不存在真正的线程?而是用进程模拟的?
操作系统中存在大量的进程,一个进程内又存在一个或多个线程,因此线程的数量一定比进程的数量多,当线程的数量足够多的时候,很明显线程的执行粒度要比进程更细。
如果Linux实现真的线程,那么就需要对这些线程进行管理。比如说创建线程、终止线程、调度线程、切换线程、给线程分配资源、释放资源以及回收资源等等,搞一套与进程类似的线程管理模块,整个难度就比较大。
相对于其他操作系统,Linux系统内核只提供了轻量级进程的支持,并未实现线程模型。Linux本身只有进程的概念,而其所谓的“线程”本质上在内核里仍然是进程。
进程是资源分配的单位,同一进程中的多个线程共享该进程的资源。Linux中所谓的“线程”只是在被创建时clone了父进程的资源,因此clone出来的进程表现为“线程”,这一点一定要弄清楚。因此,Linux“线程”这个概念只有在打引号的情况下才是最准确的。
2.线程的优点
1.创建一个新线程的代价要比创建一个新进程小得多
在创建进程的时候,就需要创建相应的进程地址空间,页表,加载相应的代码和数据;而创建线程只要创建一个PCB,分配进程的资源即可;
2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程之间的切换,只需要切换线程的上下文,不需要更新页表,加载有效数据;
3.线程占用的资源要比进程少很多
线程本身就不是主要申请资源的角色,只是分担进程的资源;
4.能充分利用多处理器的可并行数量
5.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
计算密集型应用:比如加密、大数据运算等,主要使用的是CPU资源;贴合实际情况,比如我们经常用的好压,你解压一个文件,它就要涉及到大量的解压算法;
7.I/O密集型应用,为了提高性能,将I/O等待时间操作重叠。线程可以同时等待不同的I/O操作。
I/O密集型应用:比如网络下载、云盘、ssh、在线直播、看电影等,主要使用得到是内存和I/O资源;
3.线程的缺点
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。比如:进程之间是相互独立的,我们打开各种软件,一个软件的崩溃并不会影响其他软件,变相的也就增加了进程的健壮性,而线程就不同了,因为大部分资源都是共享的,一个线程的崩溃就会导致其他所有线程崩溃,进而导致整个进程崩溃;
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
4.线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
5.线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
二、Linux进程VS线程
1.进程和线程
进程是资源分配的基本单位;线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器(存储自己的上下文信息)
- 栈(每个线程都有临时数据,都需要压栈出栈,各自独立)
- errno
- 信号屏蔽字
- 调度优先级
2.多线程共享
共享同一地址空间,因此代码段(Text Segment)、数据段(Data Segment)都是共享的:
如果定义一个函数,在各线程中都可以调用;
如果定义一个全局变量,在各线程中都可以访问到;
除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
3.进程和线程的关系
三、Linux线程控制
1.POIX线程库
pthread库是由第3方提供的,是一个原生线程库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要使用这些函数库,要通过引入头文<pthread.h>
链接这些线程函数库时要使用编译器命令的 “ -lpthread ” 选项
错误检查:
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
2.线程创建
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数
thread:获取创建成功的线程ID,该参数是一个输出型参数
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值
成功返回0
失败返回错误码
下面用pthread_create来创建线程:
#include<stdio.h> #include<pthread.h> #include<unistd.h> void* thread_run(void* args) { const char* id = (const char*)args; while(1){ printf("I am %s thread, %d\n", id, getpid()); sleep(1); } } int main() { pthread_t tid; //定义一个线程ID pthread_create(&tid, NULL, thread_run, (void*)"thread 1"); while(1){ printf("I am mian thread, %d\n",getpid()); sleep(1); } return 0; }
程序一旦运行起来,首先进程被创建,于此同时一个线程也被创建,这就是主线程;此时主线程通过pthread函数来创建其他线程
此时使用ps axj
的命令查看进程信息:虽然此时该进程中有两个线程,但是我们看到的进程只有一个,因为这两个线程都是属于同一个进程的。
使用ps -aL
命令,可以显示当前的轻量级进程。
右图它们的pid是一样的,LWP表示轻量级进程,LWP是不一样的。CPU根据LWP进行调度,系统根据PID来判断是不是同1个进程。
这里需要提一下,一个进程有多个线程,我们把属于同一个进程内的多个线程统称为线程组,那么这个线程组的组ID我们称之为当前进程的PID,也就是主线程的PID值;
如何获取线程ID?
常见获取线程ID的方式有两种:
- 创建线程时通过输出型参数获得。
- 通过调用pthread_self函数获得。
pthread_t pthread_self(void);
调用pthread_self函数即可获得当前线程的ID,类似于调用getpid函数获取当前进程的ID。
例如下面的代码,我们通过主线程创建一个新线程,主线程不断的打印新线程的ID,新线程去执行回调函数,打印出自己的ID:
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* thread_run(void* args) { while(1){//新线程打印自己的ID printf("我是新线程[%s],我线程ID是:%lu", (const char*)args, pthread_self()); sleep(1); } } int main() { pthread_t tid; pthread_create(&tid, NULL, thread_run, (void*)"new thread"); while(1){//主线程先是创建新线程并打印新线程的ID printf("我是主线程,我创建的线程ID是:%lu", tid); //1 //printf("我是主线程,我创建的线程ID是:%lu, 我的线程ID是:%lu", tid, pthread_self());//2 sleep(1); } }
运行代码(主线程的打印1),可以看到这两种方式获取到的线程的ID是一样的。
当我们给主线程也去获取自己的ID是(主线程的打印2),运行结果如下:
确实当前进程中是有两个线程,因为他们的ID都不一样;
如何创建多个线程呢?
我们让主线程一次性创建五个新线程,并让创建的每一个新线程都去执行Routine函数,也就是说Routine函数会被重复进入,即该函数是会被重入的。
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* thread_run(void* args) { while(1){ sleep(1); } } int main() { pthread_t tid[5]; for(int i = 0; i < 5; i++){ pthread_create(tid + i, NULL, thread_run, (void*)"new thread"); } while(1){ printf("I am main thread ID, %lu\n",pthread_self()); printf("################# begin ################\n"); for(int i = 0; i < 5; i++){ printf("I creat thread [%d] is: %lu\n", i, tid[i]); } printf("################# end #################\n"); sleep(1); } return 0; }
运行代码,可以看到这五个新线程是创建成功的。并且我们通过ps -aL
命令查看当前的轻量级进程,也发现确实创建出5个线程,其中第一个线程是主线程,以为它的PID和LWP是一样的;