线程概念
- 线程(thread)就是进程里面的一个执行流,更准确的“线程是一个进程内部的控制序列”。当创建一个线程的时候,系统中会拷贝一个task_struct,这个task_struct是按照原进程的模样拷贝的。然后在该进程的地址空间内,将数据段和代码段划分出一块区域给新的线程使用。
- 也就是说新线程和主线程公用进程地址空间,于是也公用页表等信息。
- 在linux中没有用来描述线程的结构体,线程是用进程来模拟的。这导致linux中的进程比我们常说的进程要“轻”一些,我们称这样的进程为轻量级进程(LWP)。轻量级进程能够看见虚拟进程的大部分信息,但是只有一小部分属于自己。
- 这里我们要重新认识一下进程。所谓进程是承担分配系统资源的基本实体,也就是说系统分配资源的单位是进程。
- 而线程才是调度的基本单位。上图中的4个线程在操作系统看来就是4个LWP,虽然它们共用进程地址空间。
线程概念小总结
- 一切进程都至少有一个线程。
- 线程在进程内部运行,本质是在进程地址空间运行。
- linux中,在cpu眼中,看到的pcb都要比传统的进程更加轻量化。
- 透过进程地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
线程的优点
- 创建一个新的线程比创建一个新的进程代价小的多。
- 与进程相比,切换线程需要操作系统做的工作要少得多。
- 线程占用的资源要比进程少很多。
- 能充分利用多处理器的可并行数量。
- 在等待慢速IO操作结束的同时,程序可执行其他的计算任务。
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
- IO密集型应用,为了提高性能,将IO操作重叠,线程可以同时等待不同的IO操作。
线程的缺点
- 性能损失
- 一个很少被外部事件阻塞的计算密集型的线程往往无法与其他线程共享同一个处理器。如果计算密集型的线程的数量比可以的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加的额外的同步和调度开销,而可用的资源不变。
- 健壮性降低
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多。
线程异常
- 单个线程如果除零错误,野指针等问题导致线程崩溃,那么进程也会随之崩溃。
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程内部所有的线程也就随即退出。
- 信号的发送单位是进程而非线程。
线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验,(比如一边写代码一边下载代码开发工具。)
Linux进程 && 线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器(***),这表明它们有独立的数据。
- 栈 (***),因为栈是FIFO,如果共用栈,就可能导致一个线程的变量析构依赖另外线程的变量。
- errno (如果使用全局的errno,如果两个线程出错,后一个线程的errno就会覆盖掉前一个线程的erron。所以每个线程有独立的errno。)
- 信号屏蔽字
- 调度优先级
进程的多个线程共享同一地址空间,因此数据段,代码段都是共享的,如果定义一个函数,在各线程中都可以调用,一个全局变量,在各个线程都可以访问到,除此之外,还共享一下资源:
- 文件描述符
- 每种信号的处理方式
- 当前工作目录
- 用户id和组id
进程的创建
-
由于linux中没有提供进程的数据结构管理线程,所以我们需要自己来封装库帮助我们管理线程。这就是pthread库。
-
线程的创建使用的函数是pthread_create,
-
该函数有4个参数。第一个参数返回用户层面的线程id,是一个输出型参数。
-
第二个参数是属性信息,我不太懂,一般给空。
-
第三个参数是该线程要执行的代码,你可以将进程地址空间中为该线程划分的空间理解为该函数的空间。
-
第四个参数是该线程函数的参数。
-
该函数如果运行正确返回0,如果错误返回错误码。(你可能会问为什么不返回-1?因为有可能多个线程出错,必须使用不同的数字来标记。)
-
使用该函数需要引入pthread库。
上图是一个多线程进程,两个死循环,用来演示线程确实是不同的task_struct,是调度的基本单位。
你可能会有这样的疑惑,既然线程是系统的基本调度单位,而你的线程是用户级别的库管理的,那么内核是如何分辨出不同的线程呢?
- 用户创建了一个线程,那么库就会帮助我们在用户层创建一个tcb帮助我们管理线程,系统也会创建一个LWP,轻量级进程,我们只需要在tcb中封装LWP的id就可以标识该LWP。
- tcb帮助我们管理线程,而LWP帮助我们执行线程。
-
前面我们说过pthread_create会产生一个tid,而我们可以通过pthread_self拿到这个id。我们通过程序打印主线程和新线程的id。
-
我们发现线程的tid很想地址,tid就是线程在地址空间上tcb的首地址。
-
我们通过pthread库管理线程。而pthread库会被加载到进程空间的共享区。在库中,会给新线程创建独立的tcb,tcb中就有独立的线程栈,局部存储等信息,而这个tcb的首地址就是tid。
-
在命令行中,我们使用ps -aL可以查看轻量级进程。