一、为什么要去减少上下文的切换
- 上下文的切换要保存现有线程的信息比如,CPU寄存器状态,程序计数器保存到内核空间内
- 获取之前保存的线程信息进行恢复
这些都会涉及到用户态到内核态的切换,所以会消耗CPU资源,内存资源和时间资源,所以我们要尽量减少上下文的切换
二、为什么用户级线程(User-Level Threads, ULT)切换不需要陷入内核?
用户级线程库(如早期的 Pthreads 在某些系统上的实现)在用户空间管理线程的创建、调度和切换。库维护自己的线程表和栈。ULT 之间的切换完全发生在用户态,由库代码(如 swapcontext
)完成,只需要保存/恢复用户寄存器(不包括特权寄存器),不涉及内核数据结构或特权指令。但是,ULT 有一个重大缺点:一个 ULT 阻塞(如进行阻塞式 I/O 系统调用),整个进程(包括其所有 ULT)都会被内核阻塞,因为内核只知道进程这个调度实体。内核级线程(KLT)则没有这个问题,一个 KLT 阻塞不会影响同一进程内的其他 KLT。
三、现代主流模型:混合模型(多对一、多对多)
现代操作系统(如 Linux, Windows, macOS)大多采用混合线程模型(通常是多对多模型,N:M 映射)。用户级线程库(如 NPTL in Linux)负责管理用户可见的“逻辑线程”,内核负责管理更轻量级的“内核调度实体”(通常是轻量级进程 LWP 或类似概念)。逻辑线程之间的切换如果发生在绑定到同一个内核调度实体的线程之间,且不涉及阻塞调用,可能完全在用户态由线程库完成(类似 ULT 切换)。但是:
- 调度实体切换必然涉及内核态:如果切换需要从一个内核调度实体切换到另一个(例如,用户级线程 A 绑定到 LWP1,线程 B 绑定到 LWP2,从 A 切换到 B),或者用户级线程库决定让出当前 LWP 的时间片给另一个 LWP,那么内核调度实体(LWP)的上下文切换就必然发生,这就会陷入内核态。
- 阻塞系统调用必然涉及内核态:任何导致阻塞的系统调用都会让当前线程陷入内核,内核在阻塞当前线程(及其关联的 LWP)时,会执行调度,切换到另一个就绪的 LWP(可能属于同一进程,也可能属于不同进程)。这个 LWP 切换过程就是一次需要陷入内核的上下文切换。
四、怎样减少上下文切换
1.减少线程数:可以通过合理的线程池管理来减少线程的创建和销毁,线程数不是越多越好,合理的线程数可以避免线程过多导致上下文切换。
2使用无锁并发编程:无锁并发编程可以避免线程因等待锁而进入阻塞状态,从而减少上下文切换的发生。
3使用CAS算法:CAS算法可以避免线程的阻塞和唤醒操作,从而减少上下文切换。
4使用协程(JDK 19的虚拟线程):协程是一种用户态线程,其切换不需要操作系统的参与,因此可以避免上下文切换。(避免的是操作系统级别的上下文切换,但是仍然需要在JVM层面做一些保存和恢复线程的状态,但是也成本低得多)
5合理地使用锁:在使用锁的过程中,需要避免过多地使用同步块或同步方法,尽量缩小同步块或同步方法的范围,从而减少线程的等待时间,避免上下文切换的发生。