Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

7.2 线程与并发

线程(Thread)是CPU调度的基本单位,是进程中的一个执行流。多线程技术让一个进程可以同时执行多个任务,充分利用多核CPU的性能,是现代并发编程的基础。

为什么需要线程

早期的操作系统只有进程的概念,每个进程有独立的地址空间,进程之间切换开销很大。随着多核CPU的出现,人们需要一种更轻量的执行单元,能够在同一个进程内并发执行,共享进程的资源,减少切换开销,线程就应运而生了。

相比多进程,多线程有以下优势:

  1. 创建和切换开销小:线程的创建、销毁、切换比进程快很多,因为线程共享进程的地址空间和资源,不需要重新分配资源
  2. 通信方便:同一个进程内的线程共享同一块地址空间,可以直接通过全局变量、堆内存通信,不需要复杂的进程间通信机制
  3. 资源利用率高:一个进程内的多个线程可以共享进程的内存、文件句柄等资源,不需要重复分配
  4. 充分利用多核CPU:多线程可以在多个CPU核心上并行执行,提升程序性能

什么是线程

线程是进程中的一个执行流,是CPU调度和分派的基本单位。

  • 一个进程可以包含多个线程,至少有一个主线程
  • 同一个进程内的所有线程共享进程的地址空间和资源
  • 每个线程有自己独立的栈、寄存器上下文、线程本地存储(TLS)

进程和线程的区别

特性进程线程
资源分配资源分配的基本单位,有独立的地址空间和资源CPU调度的基本单位,共享进程的资源,只有少量独立资源(栈、寄存器等)
切换开销大,需要切换地址空间、页表、缓存等小,只需要切换栈和寄存器
通信需要进程间通信机制(管道、共享内存等)可以直接通过全局变量、堆内存通信,需要注意同步问题
安全性高,进程之间地址空间隔离,一个进程崩溃不会影响其他进程低,线程之间共享地址空间,一个线程崩溃可能导致整个进程崩溃
并发性较低,切换开销大较高,切换开销小,适合高并发场景

例子:你打开一个浏览器进程,浏览器会有多个线程:一个线程渲染页面,一个线程处理用户输入,一个线程下载资源,多个线程同时工作,提升用户体验。如果每个任务都开一个进程,开销会非常大。

线程的实现模型

线程的实现有三种模型:用户级线程、内核级线程、混合型线程。

1. 用户级线程(User-Level Thread, ULT)

用户级线程完全在用户态实现,由用户态的线程库管理,操作系统内核感知不到线程的存在。

  • 优点
    • 线程切换不需要内核参与,开销极小,速度快
    • 调度可以由应用程序自己控制,更灵活
    • 不需要内核支持,可以在不支持线程的操作系统上实现
  • 缺点
    • 内核不知道线程的存在,如果一个线程发起阻塞的系统调用(比如read文件),整个进程都会被阻塞,其他线程也无法执行
    • 无法真正利用多核CPU,操作系统调度的单位是进程,同一个进程的多个线程只能在同一个核心上交替执行
  • 现在纯用户级线程已经很少使用了,早期的协程就是用户级线程的一种。

2. 内核级线程(Kernel-Level Thread, KLT)

内核级线程由操作系统内核管理,内核负责线程的调度和切换。

  • 优点
    • 线程的调度由内核负责,如果一个线程阻塞,其他线程还可以继续执行
    • 可以真正利用多核CPU,同一个进程的多个线程可以同时在不同的核心上并行执行
    • 内核提供完整的线程功能,支持复杂的调度策略
  • 缺点
    • 线程切换需要内核参与,开销比用户级线程大
    • 频繁的线程切换会消耗大量CPU资源
  • 现在主流操作系统的线程实现都是内核级线程,比如Linux的pthread,Windows的线程。

3. 混合型线程(N:M模型)

混合型线程结合了用户级线程和内核级线程的优点,用户态的多个用户级线程映射到内核的多个内核级线程上。

  • 优点
    • 兼顾用户级线程的低切换开销和内核级线程的并发性
    • 用户级线程切换开销小,内核级线程可以利用多核CPU
  • 缺点:实现复杂,需要用户态和内核态之间的协调
  • Go语言的goroutine调度就是N:M模型的实现,M个内核线程上运行G个goroutine,通过Go runtime调度。

上下文切换

上下文切换(Context Switch)是指CPU从一个进程/线程切换到另一个进程/线程执行的过程。

上下文切换的过程

  1. 保存当前进程/线程的上下文(程序计数器、寄存器的值、栈指针等)到PCB/TCB中
  2. 加载下一个要执行的进程/线程的上下文,恢复寄存器的值
  3. 跳转到程序计数器指向的位置,继续执行

上下文切换的开销

上下文切换有一定的开销,主要包括:

  1. 直接开销:保存和恢复寄存器、切换页表、刷新缓存等操作的CPU时间
  2. 间接开销:切换后CPU缓存失效,需要重新从内存加载数据,缓存命中率下降,导致程序执行变慢

线程切换的开销比进程切换小很多,因为线程不需要切换地址空间和页表,只需要切换寄存器和栈。

什么时候会发生上下文切换

  1. 进程/线程的时间片用完了
  2. 高优先级的进程/线程抢占CPU
  3. 进程/线程发起阻塞的系统调用(IO、锁、信号等),主动让出CPU
  4. 中断发生时

注意:过多的上下文切换会消耗大量CPU时间,降低系统性能。比如创建太多线程,会导致频繁的上下文切换,CPU大部分时间都花在切换上,真正执行任务的时间反而很少。

并发 vs 并行

并发(Concurrency)

并发是指多个任务在宏观上同时进行,微观上可能是交替执行的。比如单核CPU上运行多个进程,快速切换,让用户感觉多个程序同时在运行,这就是并发。

并行(Parallelism)

并行是指多个任务在物理上同时执行,需要多核CPU的支持,多个核心同时执行不同的任务。

区别

  • 并发是逻辑上的同时发生,并行是物理上的同时发生
  • 单核CPU只能实现并发,不能实现并行
  • 多核CPU可以同时实现并发和并行

多线程的性能收益

  • CPU密集型任务:计算密集型任务,多线程可以并行执行,加速比接近核心数,理论上n核CPU可以提升n倍性能
  • IO密集型任务:IO密集型任务大部分时间在等待IO,多线程可以在等待的时候切换执行其他任务,提升系统吞吐量,即使是单核CPU也能通过多线程提升性能

常见的线程模型

1. 一对一模型(1:1)

一个用户级线程对应一个内核级线程,是现在主流操作系统使用的模型,比如Linux的pthread,Windows的线程。

  • 优点:实现简单,支持真正的并行,一个线程阻塞不影响其他线程
  • 缺点:线程切换开销较大,线程数量不能太多,通常最多几千个线程

2. 多对多模型(N:M)

N个用户级线程映射到M个内核级线程,比如Go语言的goroutine,Erlang的进程。

  • 优点:切换开销小,可以支持大量的并发(几十万甚至上百万),兼顾并发性和性能
  • 缺点:实现复杂,需要用户态调度器的支持

3. 多对一模型(N:1)

N个用户级线程映射到1个内核级线程,也就是纯用户级线程,现在很少使用。

线程的组成

每个线程有自己独立的资源:

  1. 线程栈:每个线程有独立的栈,存储函数调用栈、局部变量,默认大小通常是1MB-8MB
  2. 寄存器上下文:程序计数器、栈指针、通用寄存器的值,线程切换时保存和恢复
  3. 线程ID(TID):每个线程有唯一的标识
  4. 线程本地存储(TLS):线程私有的全局变量,每个线程有独立的副本,其他线程访问不到
  5. 信号掩码:线程可以独立屏蔽某些信号

同一个进程内的线程共享以下资源:

  1. 进程的地址空间(代码段、数据段、堆、共享库等)
  2. 进程打开的文件描述符、Socket
  3. 进程的用户ID、组ID
  4. 信号处理函数
  5. 进程的当前工作目录

思考问题

  1. 进程和线程有什么区别?什么时候应该用多进程,什么时候应该用多线程?
  2. 上下文切换的开销主要来自哪里?为什么线程切换比进程切换快?
  3. 并发和并行有什么区别?单核CPU可以实现并行吗?
  4. 用户级线程和内核级线程各有什么优缺点?现在主流的实现是哪种?