Thread.Sleep(1) 会导致高频的上下文切换,高风险引爆CPU

今天在关注很久的博主"一线码农聊技术"发布了一篇 记一次 .NET 某药厂业务系统 CPU爆高分析,详细拜读了他的分析,感觉这个场景也挺常见的,于是就进行了阅读了一些资料和进行了一些思考。于是有了这篇内容,我也来分享下。

线程调度器作用机制

C# 线程调度器是 .NET Framework 提供的一种机制,用于管理应用程序中运行的线程。线程调度器负责分配 CPU 时间片给不同的线程,并确保它们按照预期的顺序执行。它还负责监视和调整线程的优先级,以及在需要时暂停或恢复线程的执行。线程调度器的有以下特征:

  1. 线程优先级:线程调度器允许您设置每个线程的优先级。这决定了线程在竞争CPU资源时的优先级。
  2. 抢占式:C# 线程调度器是抢占式的,这意味着高优先级的线程可以中断正在运行的低优先级线程,以便获得更多的 CPU 时间。
  3. 非确定性:由于线程调度器的存在,线程的执行时间是不确定的。这意味着一个线程可能需要等待另一个线程完成其工作,然后才能开始自己的工作。
  4. 可重入性:线程调度器支持可重入代码。这是因为线程可以在任何时候被中断和恢复。
  5. 定时器:线程调度器还提供了一个定时器机制,允许您安排代码在指定的时间间隔内运行。

结合上面的特征,我们可以知道。在多线程环境下,操作系统的线程调度器负责决定哪个线程可以获得 CPU 时间执行任务。线程调度器以时间片(通常为几十毫秒)为单位进行调度,每个线程在自己的时间片内执行任务,然后被挂起,让其他线程执行。

Thread.Sleep(1) 的影响

当一个线程调用 Thread.Sleep(1) 方法时,该线程会被挂起一毫秒,并让出 CPU 时间。在这一毫秒内,操作系统可能会选择重新调度其他线程来执行。然而,当 Thread.Sleep(1) 方法返回时,操作系统可能会立即重新调度原始线程,这样就会发生上下文切换。

还可能引发其他的问题:

  1. 延迟不准确:Thread.Sleep(1)并不能保证精确地暂停1毫秒,它仅表示至少暂停1毫秒。实际上,操作系统可能会将线程挂起更长的时间,这可能导致延迟不准确。
  2. 资源浪费:使用Thread.Sleep(1)会使线程进入睡眠状态,并释放CPU资源。然而,如果频繁地使用Thread.Sleep(1),则可能导致CPU资源的浪费,因为线程被唤醒后可能立即再次进入睡眠状态。
  3. 响应性下降:如果在关键路径上使用Thread.Sleep(1),那么可能会导致应用程序的响应性下降。特别是在实时应用程序或需要快速响应用户输入的场景下,较长的睡眠时间可能会导致用户感觉到延迟。
  4. 线程过多:如果在多线程应用程序中的每个线程都使用Thread.Sleep(1),那么可能会导致系统同时运行过多的线程,从而增加了调度和上下文切换的开销。频繁的上下文切换会导致性能下降和系统负担增加。即使 Thread.Sleep(1) 只挂起了一毫秒,但由于时间片调度机制的存在,操作系统可能会不停地在不同线程之间进行上下文切换,消耗大量的 CPU 时间。
  5. 不可移植性:Thread.Sleep(1)的行为在不同的操作系统和硬件上可能会有所不同。在某些平台上,最小的可用睡眠时间可能大于1毫秒,因此使用Thread.Sleep(1)可能不会产生预期的效果。

示例说明

假设有两个线程 A 和 B,它们轮流执行,当线程 A 执行到 Thread.Sleep(1) 时,它会挂起一毫秒。此时,操作系统可能会选择调度线程 B 来执行。然而,当线程 A 的挂起时间结束后,操作系统可能会又立即重新调度线程 A,导致频繁的上下文切换。

csharp 复制代码
using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread threadA = new Thread(RunA);
        Thread threadB = new Thread(RunB);

        threadA.Start();
        threadB.Start();

        threadA.Join();
        threadB.Join();
    }

    static void RunA()
    {
        while (true)
        {
            Console.WriteLine("A is running");
            Thread.Sleep(1);
        }
    }

    static void RunB()
    {
        while (true)
        {
            Console.WriteLine("B is running");
            Thread.Sleep(1);
        }
    }
}

解决方案

为了避免高频的上下文切换,可以考虑使用更长的等待时间,例如使用 Thread.Sleep(10) 等方法来减少上下文切换的次数。另外,也可以考虑使用其他等待机制,如 ManualResetEvent、Monitor.Wait 或 async/await 结合 Task.Delay 等,这些方法可以更加精确地控制等待时间,并减少上下文切换的次数。

相关推荐
coderWangbuer16 分钟前
基于springboot的高校招生系统(含源码+sql+视频导入教程+文档+PPT)
spring boot·后端·sql
攸攸太上22 分钟前
JMeter学习
java·后端·学习·jmeter·微服务
Kenny.志25 分钟前
2、Spring Boot 3.x 集成 Feign
java·spring boot·后端
sky丶Mamba42 分钟前
Spring Boot中获取application.yml中属性的几种方式
java·spring boot·后端
千里码aicood2 小时前
【2025】springboot教学评价管理系统(源码+文档+调试+答疑)
java·spring boot·后端·教学管理系统
程序员-珍2 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
liuxin334455662 小时前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端
数字扫地僧3 小时前
HBase与Hive、Spark的集成应用案例
后端
架构师吕师傅3 小时前
性能优化实战(三):缓存为王-面向缓存的设计
后端·微服务·架构
bug菌3 小时前
Java GUI编程进阶:多线程与并发处理的实战指南
java·后端·java ee