我们已经将页表的工作原理这个坑填上了,接下来就是学习真正的多线程部分,而这部分的知识可能会与我们之前学的进程有些出入,但是在进程部分的知识大部分还是成立的,只不过我们之前讲的那种进程是比较特殊的,也就是单执行流进程,而从现在开始,一个进程就不再是单执行流了,而是多个执行流,也就是多线程!
Ⅱ. Linux线程概念
一、什么是线程
大多数的教材对线程的概念定义:线程是操作系统中能够独立运行的最小单元,它是在进程内部创建的执行流,与同一进程中的其他线程共享进程资源。与进程不同的是,线程没有独立的地址空间和系统资源,线程的创建、撤销和切换都由操作系统内核完成。
这听起来是不是有点别扭,特别是当会想起以前学习进程的时候,我们知道 进程 = 内核数据结构 + 进程对应的代码和数据,还认为进程就是我们操作系统进行调度的一个单位,但是对于今天的线程来说,其实不太准确!
我们之前谈进程的时候,一直以为一个进程就是一个执行流,这么说不能算错误,但是一旦扯到多进程,那么就不正确了,下面我们来讲讲到底什么是多线程!
在 linux 中,设计线程的原则也和我们以前所学的进程一样,都是需要遵循一个道理:先描述、再组织!
也就是说我们需要对 “线程” 进行数据结构上的管理封装,而在大多数操作系统包括 linux 中,这个结构体称为 TCB(线程控制块),虽说和我们之前所学的 **PCB(进程控制块)**相差一个单词,但却有天壤之别!
💥**注意:**其实在 linux 中的线程和我们平时所学的教材中是有所出入的,但是它们的概念原理都是一致的,只不过 linux 的线程机制是具体化的!也就是说不同的操作系统的线程设计都是不同的! 比如说 Windows 下设计的 TCB 是一个完整的、更贴切教材说法的一种结构体,也就是说它的 TCB 设计的和 PCB 是类似的,而 Linux 中的 TCB 其实是一种 “轻量级进程” 的说法(下面会讲),这种设计带来的好处就是省去了很多与进程类似的重叠设计工作,也能减少开辟线程的成本,毕竟像进程一样的线程,开辟的空间不会小到哪里去的!
Linux源码中线程的设计涉及到多个文件,其中最为核心的是内核中的线程调度器和线程管理器。这些代码分布在多个目录和文件中,包括但不限于:
kernel/sched/目录:包含内核的线程调度器实现代码,其中最重要的文件是sched.c。kernel/threads/目录:包含内核的线程管理器实现代码,其中最重要的文件是process.c和thread.c。include/linux/sched.h:包含内核线程调度器的头文件,其中定义了与调度器相关的结构体和函数原型。include/linux/thread_info.h:包含与线程相关的头文件,其中定义了与线程相关的结构体和函数原型。此外,还有许多其他文件和目录涉及到
Linux内核的线程实现,具体取决于所使用的内核版本以及具体的架构。
针对于上面的说法,以及我们后面讨论的,都是关于 linux 的线程设计,所以我们下面必须重新认识一下我们之前所学的 task_struct 与 PCB 的关系!
二、重谈 PCB 与 task_struct
我们以前在没涉及到线程的时候,单单认为 task_struct 就是 PCB,但是从今天开始我们就得纠正一下!
在 Linux 操作系统中,PCB 是用来管理进程的数据结构,存储了进程的所有信息,如进程状态、进程ID、进程优先级、程序计数器、堆栈指针、打开文件列表、信号处理器等等。
而 task_struct 是 Linux 内核中管理进程和线程的数据结构,它是一个非常重要的结构体,包含了进程的所有信息和状态,也包含了一些用于多进程和多线程间通信的字段,如进程ID、进程状态、进程优先级、进程堆栈、进程虚拟内存空间、进程打开的文件列表、进程信号处理器等等。
💥在 Linux 中,进程和线程都是通过 task_struct 结构体来表示的。
由于 Linux 中线程和进程的实现方式不同,进程和线程在内核中的数据结构也有所区别,但是它们都可以通过task_struct 结构体来表示。因此,可以说 PCB 是进程的数据结构,而 task_struct 是 Linux 内核中用来管理进程和线程的数据结构。在 Linux 中,进程和线程都被视为一个个任务,每个任务都对应一个 task_struct 结构体,而且由于线程与进程共享进程的资源,因此线程也被视为一种特殊的进程,即 “轻量级进程”,也对应一个 task_struct 结构体。(后面我们会继续讲到!)
下面我们给出一张图片,可以帮我们更好的理解 PCB 与 task_struct 的关系:

三、线程的特性
- 一切进程至少都有一个执行线程,若为单执行流,则与我们之前讲的进程原则都是符合的!
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流,简单地说,就是每个线程都共享同一个虚拟地址空间!
- 每个线程除了共享同一个虚拟空间之外,其实还有独立的一些资源,比如独立栈资源,这个我们后面会讲到!
- 单个 “轻量级进程” 的执行粒度,一定比之前我们所讲的进程要更加的细!
- 多个线程各自有一个栈,共有一个堆!
- 线程包含
CPU现场,但是线程只是进程中的一个执行流,执行的是程序中的一个片段代码,多个线程才能完整整体程序的运行! - 线程使用公共变量/公共内存时需要使用同步机制,因为他们在同一地址空间内!
四、线程的优点
- 减少资源占用
- 相对于进程,线程更轻量级,占用更少的系统资源,例如内存和
CPU时间。在同一个进程内部创建多个线程,它们共享相同的内存空间,从而避免了进程间的通信和资源竞争,减少了资源的占用。
- 提高了程序的并发性和响应性
- 线程能够将一个程序的任务分为多个并发的部分,从而提高程序的并发性和响应性。因为线程是轻量级的,它们可以在同一个进程内快速地创建、销毁和切换,从而实现更快速的响应和更高效的资源利用。
- 提高了系统的性能和吞吐量
- 由于线程之间的切换代价比进程之间的切换小得多,因此在高并发场景下,采用多线程的方式可以提高系统的性能和吞吐量。
- 在等待慢速
I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现,能提高
CPU密集型程序的执行效率。 I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
- 线程之间的通信简单
- 共享地址空间和页表信息,因此传参以及全局数据都可以实现通信,而不同进程之间的通信更为复杂,通常需要调用内核实现。
并发和并行是两个不同的概念,它们的区别不仅仅是由进程和线程引起的。虽然进程和线程可以用来实现并发和并行,但它们并不是这些概念的唯一来源。
并发 是指两个或多个任务在同一时间段内被执行,而不一定是同时执行。在一个单处理器的系统中,多个进程或线程之间通过时间片轮转等方式,共享 CPU 时间来实现并发。在并发模型下,多个任务之间相互独立,它们的执行顺序是不确定的,可能会出现竞态条件和死锁等问题。
并行 是指两个或多个任务同时执行,通常是在多个处理器或多个核心上执行。在一个多处理器的系统中,多个进程或线程可以同时执行不同的任务,以提高系统的性能和吞吐量。在并行模型下,多个任务之间相互协作,它们的执行顺序是确定的,不会出现竞态条件和死锁等问题。
在并行执行模式下,由于多个任务可以在不同的处理器或核心上同时执行,因此它们的执行顺序是确定的。这是因为并行执行模式下,每个任务都有自己的执行上下文和执行环境,不会受到其他任务的影响。
在一个典型的多处理器系统中,每个处理器都有自己的本地缓存和本地内存,处理器之间通过总线或其他互联技术进行通信和同步。这种分布式的执行环境,使得每个任务可以在独立的处理器上执行,互不干扰,从而保证了它们的执行顺序是确定的。
此外,多处理器系统中还有一些硬件和软件机制,如锁、原子操作、信号量、条件变量等,可以保证多个任务之间的同步和互斥,避免竞态条件和死锁等问题的发生,也有利于确保并行执行的正确性和可靠性。
总之,因为并行执行模式下,每个任务都有独立的执行上下文和执行环境,同时又有各种同步和互斥机制的支持,因此它们的执行顺序是确定的。
因此,尽管进程和线程是实现并发和并行的一种方式,但并发和并行的区别并不是由进程和线程引起的,而是由任务的执行方式和上下文环境所决定的。
五、线程的缺点
- 性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。线程共享同一进程的地址空间,因此需要进行复杂的内存管理以确保线程之间的数据不会相互干扰或造成内存泄漏等问题。
- 编程与调试难度提高
- 编写与调试一个多线程程序比单线程程序困难得多,因为多线程程序需要考虑线程之间的同步和互斥,以避免竞态条件和死锁等问题。在某些情况下,线程的调试确实比较困难,比如当多个线程同时执行时,可能会出现竞争条件、死锁等问题,调试起来比较复杂。
六、线程的异常问题
线程的异常问题指的是在多线程编程中,如果一个线程发生异常,它可能会导致整个程序崩溃或者其他线程出现异常。因为所有的线程共享同一地址空间和资源,一个线程的异常可能会影响到其他线程的正常运行。
一个线程的异常通常指的是线程的运行时错误,比如内存访问错误、除以零错误等。这些错误通常都是不可避免的,因为线程的运行是并发的,而且多线程程序通常需要访问共享资源,所以在多线程编程中,要特别注意异常处理。
为了避免线程的异常导致程序崩溃,需要使用一些技术来保护线程。比如,可以使用异常处理器来捕获线程的异常,然后在异常处理器中进行处理,让程序能够继续运行下去。另外,可以使用锁来保护共享资源,从而避免多个线程同时访问同一个资源而出现异常。
Ⅲ. 线程与进程的关系
在学习线程的接口之前,我们必须先搞清楚线程和进程的异同,其实我们已经在上面讲线程概念的时候提及过了,但是这部分还是比较重要的,所以我们需要反复强调,它们的关系总结起来就是如下几点:
- 进程是承担分配系统资源的基本单位,而线程是
CPU调度的基本单位 - 线程是进程中的一个执行流
- 向系统申请资源的是进程,而线程需要资源时伸手向进程要
- 每个进程都是独立占有资源的
- 每个线程会共享进程的同一地址空间,但是也拥有一部分线程各自的私有资源:
- 线程ID
- 一组寄存器(即上下文结构)
- 独立栈空间
errno- 信号屏蔽字(阻塞信号集)
- 调度优先级
- 线程除了共享进程的地址空间之外,还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(**
SIG_IGN、SIG_DFL**或者自定义的信号处理函数) - 当前工作目录
- 用户id、组id
进程和线程的关系如下图:
