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 等,这些方法可以更加精确地控制等待时间,并减少上下文切换的次数。

相关推荐
m0_7482405416 分钟前
Spring Boot 实战篇(四):实现用户登录与注册功能
java·spring boot·后端
Code侠客行17 分钟前
Swift语言的多线程编程
开发语言·后端·golang
Code侠客行23 分钟前
Swift语言的软件开发工具
开发语言·后端·golang
m0_748230211 小时前
Spring Boot拦截器(Interceptor)详解
java·spring boot·后端
小白的一叶扁舟1 小时前
RabbitMQ原理、使用与实践指南
java·spring boot·后端·spring cloud·rabbitmq·java-rabbitmq
晓风残月( ̄ε(# ̄)~1 小时前
Spring参数校验,数组入参校验 :List<E>
java·经验分享·spring boot·后端·spring·spring cloud·list
2501_901839521 小时前
Ruby语言的软件开发工具
开发语言·后端·golang
李歘歘2 小时前
Golang——常用库context和runtime
开发语言·后端·golang
007php0072 小时前
go语言zero框架中在线截图chromedp 设置超限的网页长度
java·开发语言·后端·docker·云原生·容器·golang
Cikiss2 小时前
图解Git——分布式Git《Pro Git》
分布式·git·后端·源代码管理