目录
前言
哈喽,小伙伴们大家好。在之前的文章中我介绍过进程,想必小伙伴们对进程也都有一定的了解。今天我将介绍一个新概念——线程,线程的本质是什么呢?线程和进程的联系是什么呢?线程本身又有哪些特性呢?这些问题都将在下面的文章中得到答案,话不多说,我们赶紧开始吧。
一、Linux线程概念
线程概念:在一个进程里的执行路线就叫做线程。
我在之前的文章中提到过,每个进程都对应一个task_struct,而每个task_struct都指向不同的地址空间,在不同的地址空间中根据页表映射到物理内存中。这句话其实不太准确,因为这句话成立有一个前提,那就是进程内部只有一个执行流,也就是单线程的。
如果一个进程是多线程的,则如下图所示,每一个task_struct对应的是一个线程,而且这些线程都指向同一张地址空间表。站在内核角度,承担分配系统的基本实体,角度进程。控制块,地址空间,页表共同组成一个进程。
和进程间具有独立性不同,线程间的地址空间和物理内存是共享的,数据改变并不会有写时拷贝发生。
linux线程设计:
从linux内核的角度看,linux下并不存在真正意义上的多线程,所谓线程都是用进程模拟的。如果真的支持多线程,os就需要需要管理线程,线程的数量有很多,所以又要设置一套新的复杂的管理方法。而linux与其它操作系统不同,它并没有这样做,而是直接把线程模拟成进程来管理,减少了很多格外工作,这也是linux设计的非常精妙的一个地方。
在linux中,站在cpu的角度,它是无法区分进程和线程的,当然也不需要区分。cpu只关心一个一个独立的执行流。linux中的所有执行流,都叫做轻量级进程,每一个执行流对应一个task_struct,在cpu看来,task_struct的内容是要小于os原理上面的进程控制块的。(所谓os原理,从宏观角度看是所有操作系统都遵循的设计哲学,但每个操作系统又都有各自的特色)。
既然linux没有真正意义上的线程,所以linux也没有真正意义上的线程相关的系统调用。linux仅仅提供了创建轻量级进程的接口,创建进程共享空间。以上所说的,是站在内核的角度。从用户角度来说,我们只是想单纯使用多线程,并不关心如何实现,所以linux基于轻量级进程的系统调用,在用户层模拟实现了一套线程接口。
总结:
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
二、线程的特点
1、线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换代价要小很多
- 线程占用的资源要比进程小很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
2、线程的缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多。
- 线程异常
单个进程如果出现除零,野指针问题导致线程崩溃,进程也会崩溃。线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
3、进程与线程
进程是资源分配的基本单位,线程是调度的基本单位,线程共享进程数据,但也有一部分自己的数据。
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
进程的多个线程共享同一块地址空间,因此Text segment和data segment是共享的。如果定义一个函数,在各线程都可以调用,如果定义一个全局变量,在各线程都可以访问到。除此之外,各线程还能共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和用户组id
三、线程控制
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的,这些函数都为用户层函数,供用户调用。
- 要使用这些函数库,要通过引入头文<pthread.h>。
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项,因为不是c/c++库,不指定的话编译器找不到。
1、创建线程
功能:创建一个新的线程
原型:
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;失败返回错误码
我们在程序中创建一个线程,代码如下:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* routine(void* arg)
{
char* msg=(char*)arg;
while(1)
{
printf("%s\n",msg);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,routine,(void*)"thread 1");//创建线程
while(1)
{
printf("I am main\n");
sleep(2);
}
return 0;
}
进程运行后,使用ps命令查看进程。
我们发现有两个mythread也可以说是两个执行流在运行,它们的PID相同,但LWP不同。我们在上文中提到,在linux内核是无法区分进程和线程的,它会把所有执行流都当成一个轻量化进程处理,由此我们可以得出一个结论,OS在调度轻量化进程的时候,采用的并不是PID,而是LWP。应用层的线程和内核中的LWP是1:1的关系。
2、线程ID及地址空间布局
- pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中,我们可以通过访问第一个参数来查看。
- 线程库也提供了pthread_ self函数,来获取线程自身的ID。
pthread_t pthread_self(void);
pthread_t 是什么类型取决于具体实现,可能是无符号长整形或其它类型。但 pthread_t类型的线程id其本质是进程地址空间上的一个地址。
由于在我的云服务器上pthread_t是无符号长整型,所以按地址打印会报警告,但并不影响结果,运行结果如下。 可以看出,pthread_t确实是一个地址。
LWP是内核层用来描述线程的标识, pthread_t类型的id为用户层的ID。用户需要通过线程ID来找到对应的线程,那么具体是如何找到的呢?
我们直到,线程很多,需要被管理,那么这个管理工作由谁来做呢?Linux不提供真正的线程,只提供LWP,意味着OS只需要对LWP内核执行流进程管理,而用户层的接口和其它数据由pthread来管理。
pthread是一个动态库,我们知道动态库也是文件,保存在磁盘中,然后根据页表映射到地址空间的共享区。 动态库映射到内存中后,每个线程都对应着一个内存块保存它的数据和属性,我们可以通过线程ID找到相应内存块的首地址,从而找到线程的所有信息。cpu在调度进行线程切换时,只需要到动态库中,找到相应的线程内存块,把线程的临时数据保存到局部存储区中,把临时变量保存到内存块的栈中,然后切换到下一个线程内存块提取相关数据,便能实现进程切换。内核仅仅要做的就是当用户层线程按照某种方式交给它一些数据时,找到LWP按要求去跑就好了。
3、线程终止
线程终止有三种方法:
(1)return XXX
return为正常退出,返回退出信息,但要注意mian thread退出代表整个进程退出。
(2)exit
exit叫做终止进程,任何一个线程调用exit都会导致进程终止。
我们