为什么Thread.Sleep()会占用这么多的CPU资源?

15

我有一个ASP.NET页面,其伪代码如下:

while (read)
{
   Response.OutputStream.Write(buffer, 0, buffer.Length);
   Response.Flush();
}

任何请求此页面的客户端都会开始下载一个二进制文件。到此为止一切都正常,但是客户端没有下载速度的限制,所以将上面的代码更改为:

while (read)
{
   Response.OutputStream.Write(buffer, 0, buffer.Length);
   Response.Flush();
   Thread.Sleep(500);
}

现在速度问题已经解决了,但是当有100个并发的客户端一个接一个地连接(每个新连接之间有3秒的延迟)进行测试时,当客户端数量增加时CPU使用率也会增加,当有70~80个并发客户端时,CPU达到100%,任何新的连接都被拒绝。在其他机器上可能会有不同的数字,但问题是为什么Thread.Sleep()如此耗费CPU资源,是否有任何方法可以在不增加CPU占用率的情况下减缓客户端的速度?

我可以在IIS级别上做到这一点,但是我需要更多来自我的应用程序内部的控制。


肯定不是关于并发客户端数量或它们连接和接收数据的时间,因为使用相同数量的客户端进行了相同的测试,甚至在更高的下载速度下(没有任何限制,如Thread.Sleep),并且长时间测试时CPU利用率<5%,因此这与ASP.NET线程池中可用线程数以及它们被绑定到请求的时间无关。这是Thread.Sleep,但也许在我的情况下意味着不同的东西。 - Xaqron
这怎么就不涉及线程数量了呢?当您没有休眠时,客户端数量保持不变,因此使用的线程会更少。 - Jon Hanna
我已经做到了这一点(保持客户数量不变),但需要比让它们“休眠”更多的工作,但是我并没有看到 CPU 上升,那么为什么休息比工作更加密集呢?! 我猜(在查看了 Michael 和 Jon 的答案后)这与 ASP.NET 自动使用线程的方式有关(我猜 Jon 在控制台而不是在 asp.net 上进行测试),当我的应用程序没有休眠时,它会交换线程,但是当使用 Sleep 时,它不会将它们退回池中(只是像 Michael 猜测的那样的一个猜想)! - Xaqron
4个回答

49

让我们看一下Michael的回答是否合理。

现在,Michael明智地指出Thread.Sleep(500)不应该花费太多CPU资源。这在理论上都可以,但让我们看看它在实践中是否成立。

    static void Main(string[] args) {
        for(int i = 0; i != 10000; ++i)
        {
            Thread.Sleep(500);
        }
    }

运行此代码后,应用程序的 CPU 使用率保持在 0% 左右。

迈克尔还指出,由于 ASP.NET 必须使用的所有线程都处于休眠状态,因此它将不得不生成新线程,并表示这是代价高昂的。让我们尝试不休眠,而是大量生成线程:

    static void Main(string[] args) {
        for(int i = 0; i != 10000; ++i)
        {
            new Thread(o => {}).Start();
        }
    }

我们创建了许多线程,但它们只执行一个空操作。即使这些线程什么都不做,但它们会占用大量的CPU资源。

然而,由于每个线程的生命周期非常短暂,因此线程总数并不会很高。现在我们来将它们结合起来:

    static void Main(string[] args) {
        for(int i = 0; i != 10000; ++i)
        {
            new Thread(o => {Thread.Sleep(500);}).Start();
        }
    }

将我们已经证明在CPU使用上低廉的操作添加到每个线程中会增加CPU使用率,因为线程数量不断增加。如果我在调试器中运行它,它会推高到接近100%的CPU利用率。如果我在调试器外运行它,它会表现得稍微好一些,但只是因为它在达到100%之前抛出了内存不足异常。

所以,问题不在于Thread.Sleep本身,而在于所有可用线程都休眠的副作用,这迫使创建更多的线程来处理其他工作,正如Michael所说的那样。


谢谢Jon。所以说,当我的线程在工作时,它们的数量比它们工作时要少(这很有道理)?正如我所说,我在没有Thread.Sleep()的相同条件下重复了测试,并且100个客户端的情况都很好。我指的是所有100个客户端都连接并接收数据。但是当我添加Thread.Sleep()时,相同的线程开始增加CPU使用率,除此之外没有做其他事情。您对IHttpAsyncHandler(感谢Lunatic)和重新设计工作有什么想法吗? (我想知道ASP.NET是否适合这种工作) - Xaqron
由于任务池处理任务的方式,一个物理线程可能会为多个客户端提供服务。然而,通过将线程休眠来锁定它,它就不能再这样做了,因此线程数会增加,即使客户端数量没有增加。异步处理程序通常是处理长时间延迟的asp任务的好方法,但我不确定您实际上是为什么要进行睡眠。如果您想要限制数据,则可以使用IIS7比特率限制;根据http://learn.iis.net/page.aspx/149/bit-rate-throttling-extensibility-walkthrough/,它可用于非媒体文件,没有比特率。 - Jon Hanna
1
漂亮的回答,赞! - Doctor Jones

38

猜测:

我认为占用CPU的不是Thread.Sleep()方法,而是因为你让线程在响应请求时被占用太长时间,系统需要启动新线程(和其他资源)来响应新的请求,因为那些休眠的线程在线程池中不再可用。


2
开启新线程非常昂贵,在IIS上很容易出现线程饥饿的情况。 - FacticiusVir
我使用了100个没有下载速度限制的客户端重复了测试。我确信不增加CPU负载和增加CPU负载的代码之间唯一的区别就是Thread.Sleep()。我进行了更长时间、更多客户端的测试,CPU始终保持在1~2%。 - Xaqron
@ FacticiusVir:我不旋转。我使用“Sleep”。因此,ASP.NET工作线程会释放CPU。使用IIS设置可以设置连接速度限制,这不是我的选择。 - Xaqron
4
但是你仍然占用了工作池。您不应使用sleep,而应该使用async IO API。 - TomTom
@TomTom:您能否给出一个与我的情况相关的示例链接或提示(从ASP.NET应用程序向客户端发送二进制数据)? - Xaqron

2
与其使用ASP.NET页面,您应该实现一个IHttpAsyncHandler。ASP.NET页面代码会在您的代码和浏览器之间放置许多东西,这对于传输二进制文件不合适。此外,由于您试图执行速率限制,因此应使用异步代码来限制资源使用,而这在ASP.NET页面中很困难。 创建一个IHttpAsyncHandler相当简单。只需在BeginProcessRequest方法中触发一些异步操作,并不要忘记正确关闭上下文以显示已到达文件的末尾。IIS无法在此处为您关闭它。
以下是我非常糟糕的示例,演示如何执行由一系列步骤组成的异步操作,从0计数到10,每个操作间隔500毫秒。
using System;
using System.Threading;

namespace ConsoleApplication1 {
    class Program {
        static void Main() {
            // Create IO instances
            EventWaitHandle WaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset); // We don't actually fire this event, just need a ref
            EventWaitHandle StopWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
            int Counter = 0;
            WaitOrTimerCallback AsyncIOMethod = (s, t) => { };
            AsyncIOMethod = (s, t) => {
                // Handle IO step
                Counter++;
                Console.WriteLine(Counter);
                if (Counter >= 10)
                    // Counter has reaced 10 so we stop
                    StopWaitHandle.Set();
                else
                    // Register the next step in the thread pool
                    ThreadPool.RegisterWaitForSingleObject(WaitHandle, AsyncIOMethod, null, 500, true);
            };

            // Do initial IO
            Console.WriteLine(Counter);
            // Register the first step in the thread pool
            ThreadPool.RegisterWaitForSingleObject(WaitHandle, AsyncIOMethod, null, 500, true);
            // We force the main thread to wait here so that the demo doesn't close instantly
            StopWaitHandle.WaitOne();
        }
    }
}

您还需要根据情况以适当的方式在 IIS 中注册您的 IHttpAsyncHandler 实现。

1
我处理许多其他事情,比如处理cookies、将用户重定向到其他页面(HTTP重定向)... 我不确定是否需要深入了解IIS并自己处理所有内容。我会阅读相关资料并投票。感谢您的提示。 - Xaqron

0

这是因为每次线程让出时间片时,它都会获得优先级提升。避免频繁调用 sleep(特别是使用较小的值)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接