找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1385

积分

0

好友

177

主题
发表于 2026-2-13 02:12:24 | 查看: 27| 回复: 0

尽管现代计算机多采用多核CPU,多线程通常能带来更高的并发性能,但这并不意味着简单地启动更多线程就能获得更好的效果。过多的线程会增加创建销毁的开销,并引发频繁的上下文切换,反而可能导致程序的吞吐量(TPS)下降。

时间片

在多任务操作系统中,需要同时执行的作业数量常常会超过CPU的核心数。但一个CPU核心在任意时刻只能执行一个任务。那么,如何让用户产生多个任务在同时进行的错觉呢?答案就在于操作系统设计者巧妙采用的时间片轮转机制。

时间片,就是CPU分配给每个任务(线程)执行的一小段时间。

思考:单核CPU究竟是如何支持多线程运行的呢?

线程的上下文包含了某一时刻CPU寄存器和程序计数器的内容。CPU通过时间片分配算法循环执行多个任务(线程)。由于每个时间片都非常短暂,从宏观上看,CPU就像是在多个线程之间快速切换执行。

因此,即使在单核CPU上,通过这种快速的切换也能实现“并发”执行多线程的效果。当然,频繁的切换会带来开销,多核CPU则能在物理层面真正并行执行多个线程,从而在一定程度上减少这种上下文切换。

超线程

现代CPU除了处理器核心,还包含了寄存器、各级缓存、浮点/整数运算单元以及内部总线等组件。多核CPU意味着有多个物理处理器核心。但这也带来了新的挑战:不同核心上运行的线程需要通过外部总线通信,并处理因缓存不一致导致的数据同步问题。

Intel提出的超线程技术,旨在让单个CPU核心能够“同时”处理两个线程。其原理在于,一个线程可能正在使用核心的运算单元,而另一个线程可能正在访问缓存或其他设备,此时两个线程可以并发工作。为了实现这种细粒度的协调,CPU内部增加了一个辅助核心。根据Intel的数据,这一设计大约会增加5%的芯片面积,但能带来15%~30%的性能提升。

上下文切换

上下文切换主要发生在以下几种情况:

  • 线程切换:同一进程内的两个线程之间切换。
  • 进程切换:两个不同进程之间切换。
  • 模式切换:同一线程在用户态和内核态之间切换。
  • 地址空间切换:将虚拟内存地址映射切换到物理内存地址。

上下文切换的过程是:CPU在切换任务前,会先将当前任务的状态(上下文)保存下来,以便下次切换回来时能够恢复;然后加载下一个任务的状态并开始执行。这个“保存状态-加载状态”的过程就是上下文切换。

每个线程都拥有自己的程序计数器(PC,指向下一条要执行的指令)、一组寄存器(保存当前工作变量)和一个堆栈(记录调用历史)。

  • 寄存器是CPU内部的高速小型存储器,用于快速存取常用值(如运算中间结果),以加速程序运行。
  • 程序计数器是一个特殊寄存器,用于指示CPU当前正在执行或接下来要执行的指令在内存中的位置。

一次完整的上下文切换包括以下步骤:

  1. 挂起当前任务(线程/进程),将其在CPU中的状态(上下文)保存到内存某处。
  2. 从内存中检索下一个任务的上下文,并将其恢复到CPU的寄存器中。
  3. 跳转到程序计数器所指向的位置(即任务上次被中断的代码行),恢复执行。

线程上下文切换时序图

线程上下文切换会有什么问题呢?

上下文切换本身会产生额外开销。这常常表现为,在高并发场景下,使用多线程有时反而比串行执行更慢。因此,减少上下文切换次数是提升多线程程序效率的关键。

  • 直接消耗:CPU寄存器需要保存和加载、系统调度器代码需要执行、TLB(快表)需要刷新、CPU流水线(pipeline)需要清空。
  • 间接消耗:多核CPU的缓存之间需要同步共享的数据。间接消耗对程序的影响取决于线程操作数据工作集的大小。

切换查看

在Linux系统下,可以使用 vmstat 命令来查看上下文切换的次数。输出结果中的 cs 列就表示上下文切换的数量(通常,空闲系统每秒的上下文切换次数在1500以下)。

vmstat命令查看上下文切换

线程调度

抢占式调度

在这种调度方式下,每个线程能够执行的时间片长度、以及线程何时切换,都由操作系统内核控制。内核可能给所有线程分配相同长度的时间片,也可能根据优先级等因素进行差异化分配。在这种机制下,一个线程的阻塞不会导致整个进程阻塞。

Java 采用的正是抢占式线程调度。JVM中的线程会按照优先级分配CPU时间片,优先级高的线程通常会获得更多的执行机会,但这并不意味着它能独占CPU,优先级低的线程同样会分到时间片,只是可能更少。

抢占式调度示意图

协同式调度

在这种调度方式下,线程在执行完自己的任务后,会主动通知系统将CPU让给其他线程。这种方式就像接力赛跑。线程的执行时间由线程自身控制,切换时机可以预测,也不存在复杂的多线程同步问题。但它有一个致命的缺点:如果一个线程编写不当,在运行中发生阻塞且不释放CPU,就可能导致整个系统崩溃。

协同式调度示意图

线程让出CPU的情况

  • 当前运行线程主动放弃CPU,例如调用 yield() 方法。(注意:基于时间片轮转的JVM和操作系统不会让线程永久放弃CPU,通常只是放弃本次时间片)。
  • 当前运行线程因为某些原因进入阻塞状态,例如等待I/O操作完成。
  • 当前运行线程任务执行完毕,即 run() 方法执行结束。

引起线程上下文切换的因素

  • 时间片用完:当前线程的时间片耗尽,系统CPU正常调度下一个就绪线程。
  • 中断处理:硬件中断或软件中断“打断”当前执行。CPU响应中断时,会在当前程序与中断处理程序之间进行上下文切换。软件中断原因包括I/O阻塞、未抢到锁资源等。
  • 用户态/内核态切换:某些系统调用会引发用户态到内核态的转换,这可能伴随一次上下文切换。
  • 锁竞争:多个线程竞争同一锁资源时,未能获取锁的线程可能会被挂起,从而引发上下文切换。

因此优化手段有:

  • 无锁并发编程:例如将数据按Hash取模分段,不同线程处理不同段的数据,避免共享资源的锁竞争。
  • CAS算法:使用如Java Atomic 包提供的原子操作,通过Compare-And-Swap机制更新数据,无需加锁。
  • 使用最少线程:合理设置线程池大小,避免创建过多闲置线程。
  • 协程:在单线程内实现多任务的调度与切换,避免线程切换的开销。

合理设置线程数量是平衡CPU利用率和切换开销的关键。

  • 高并发、低耗时:建议使用较少的线程数。
  • 低并发、高耗时:建议使用较多的线程数以充分利用CPU。
  • 高并发、高耗时:需要具体分析任务类型,可能需结合任务队列、动态调整线程数等策略。

希望这篇关于单核CPU与多线程原理的解析,能帮助你更深入地理解并发编程的底层机制。想了解更多系统性的技术知识,欢迎在 云栈社区 与更多开发者交流探讨。




上一篇:微信服务号图标迎来改版:蓝色双菱形设计取代经典橙色购物袋
下一篇:从代码评审到职业规划,避免程序员成长中的七个常见问题
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 10:24 , Processed in 0.699301 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表