ASP.NET框架中的异步页面 - 其他线程在哪里,如何重新连接?

9

对于异步操作的这个愚蠢问题,我很抱歉。以下是我的理解:

IIS有一组有限的工作线程等待请求。如果一个请求是长时间运行的操作,它将阻塞该线程。这会导致较少的线程用于服务请求。

解决方法-使用异步页面。当请求到达时,主工作线程被释放,另一个线程在其他地方创建。因此,主线程能够服务其他请求。当在另一个线程上完成请求时,会从主线程池中选择另一个线程,并将响应发送回客户端。

1)这些其他线程在哪里?是否有另一个线程池?

2)如果ASP.NET喜欢在这个其他线程池中创建新线程(?),为什么不增加主工作线程池中的线程数-它们都在同一台机器上运行,对吧?我不明白将请求移动到这个其他线程池的好处。内存/ CPU应该是相同的,对吗?

3)如果主线程将请求交给了这个其他线程,为什么请求没有断开连接?它神奇地将请求移交给其他某个地方的工作线程,当长时间运行的进程完成时,它会从主工作线程池中选择一个线程并将响应发送给客户端。我很惊讶……但是这是如何工作的呢?


1
请勿更改标签。这项技术适用于所有的程序。 - rkrauter
4个回答

10

ASP.NET中的异步页面使用异步回调,异步回调使用线程池,而且它是用于服务ASP.NET请求的相同线程池。

然而,事情并不是那么简单。.NET ThreadPool有两种类型的线程 - 工作线程和I/O线程。I/O线程使用所谓的I/O完成端口,这是一种无需线程或线程不可知的等待文件句柄上的读/写操作完成的手段,随后运行回调方法。(请注意,文件句柄不一定指磁盘上的文件;就Windows而言,它也可以是套接字、管道等。)

一个典型的.NET Web开发人员并不需要了解这些。当然,如果您正在编写实际的Web服务器或任何类型的网络服务器,则绝对需要学习这些知识,因为它们是处理数百个传入连接的唯一方法,而不必实际生成数百个线程来为它们提供服务。如果您感兴趣,可以查看托管I/O完成端口教程(CodeProject)。

无论如何,回到主题;当您在高层次上与线程池交互时,即通过编写:

ThreadPool.QueueUserWorkItem(s => DoSomeWork(s));

这不使用I/O完成端口。它将工作提交给线程池管理的普通工作线程之一。如果您使用异步回调,情况是相同的:

Func<int> asyncFunc;

IAsyncResult BeginOperation(object sender, EventArgs e, AsyncCallback cb,
    object state)
{
    asyncFunc = () => { Thread.Sleep(500); return 42; };
    return asyncFunc.BeginInvoke(cb, state);
}

void EndOperation(IAsyncResult ar)
{
    int result = asyncFunc.EndInvoke(ar);
    Console.WriteLine(result);
}

同样的问题。在EndOperation中,你正在使用一个ThreadPool worker线程。你可以通过插入以下调试代码来验证这一点:

void EndSimpleWait(IAsyncResult ar)
{
    int maxWorkers, maxIO, availableWorkers, availableIO;
    ThreadPool.GetMaxThreads(out maxWorkers, out maxIO);
    ThreadPool.GetAvailableThreads(out availableWorkers, out availableIO);
    int result = asyncFunc.EndInvoke(ar);
}

在那里设置断点,你会发现availableWorkersmaxWorkers少一个,而maxIOavailableIO是相同的。

但在.NET中,一些异步操作是“特殊”的。这实际上与ASP.NET无关 - 它们在Winforms或WPF应用程序中也会使用I/O完成端口。例如:

这只是一个不完整的列表。基本上,几乎.NET Framework中的每个类都会暴露自己的BeginXYZEndXYZ方法,并且可能执行任何I/O操作,都会使用I/O完成端口。这样做是为了让应用程序开发人员更轻松,因为在.NET中实现I/O线程有点困难。 我猜.NET Framework的设计者故意选择使发布I/O操作变得困难(与工作线程相比,您可以编写ThreadPool.QueueUserWorkItem),因为如果您不知道如何正确使用它们,则相对来说是“危险”的;相比之下,在Windows API中生成这些操作实际上非常简单。
如前所述,您可以使用一些调试代码验证正在发生的情况:
WebRequest request;

IAsyncResult BeginDownload(object sender, EventArgs e,
    AsyncCallback cb, object state)
{
    request = WebRequest.Create("http://www.example.com");
    return request.BeginGetResponse(cb, state);
}

void EndDownload(IAsyncResult ar)
{
    int maxWorkers, maxIO, availableWorkers, availableIO;
    ThreadPool.GetMaxThreads(out maxWorkers, out maxIO);
    ThreadPool.GetAvailableThreads(out availableWorkers, out availableIO);
    string html;
    using (WebResponse response = request.EndGetResponse(ar))
    {
        using (StreamReader reader = new
            StreamReader(response.GetResponseStream()))
        {
            html = reader.ReadToEnd();
        }
    }
}

如果您执行此操作,您会发现线程统计信息不同。 availableWorkers 将匹配 maxWorkers,但是 availableIOmaxIO 少一个。那是因为您正在运行 I/O 线程。这也是为什么你不应该在异步回调中进行任何昂贵的计算 - 在 I/O 完成端口上发布 CPU 密集型工作是低效和有害的。
所有这些都解释了为什么当您需要执行任何 I/O 操作时强烈建议您在 ASP.NET 中使用 Async 页面。该模式只对 I/O 操作有用;非 I/O 异步操作将最终被发布到 ThreadPool 中的工作线程中,并且您仍将阻止后续 ASP.NET 请求。但是,您可以生成几乎无限数量的异步 I/O 操作,并且不需要考虑第二个想法;直到 I/O 完成并且回调准备好开始为止,这些操作不会使用任何线程。 因此,总结一下 - 只有一个 ThreadPool,但其中有不同种类的线程,如果您执行慢速 I/O 操作,则使用 I/O 线程要比较更有效率。这与 CPU 或内存无关,与 I/O 和文件句柄有关。
关于 #3,这并不是一个“为什么请求没有被断开”的问题,更像是一个“为什么会”这样的问题。套接字并不会因为当前没有线程正在发送或接收数据而被关闭,就像如果没有人迎接客人,你的前门也不会自动关闭一样。如果服务器没有回答客户端操作,客户端操作可能会超时,并可能选择从其端断开连接,但这是另一个问题。

@Aaron:我的理解一直是相反的。它们本来就是线程池线程,但这并不适用于那些需要大量CPU时间的后台任务,而是适用于需要等待一段时间才能完成的任务,例如等待Web服务响应或数据库查询。 - John Saunders
@John:我们似乎确实在第二部分达成了一致。关于第一部分,我可能是错的(我承认了这一点),但我找不到任何明确说明其中之一的文档,并且我不明白如何以其他方式避免线程池饥饿。我很想看到一个明确的解释;目前来说,这是我对机制的最佳理解。我确实知道通过手动排队项目可以使ASP.NET线程池饥饿,所以如果异步方法确实使用线程池线程,ASP.NET必须执行一些奇怪的黑魔法来防止请求饥饿。 - Aaronaught
这取决于你排队的内容。如果你排队的是一个开始查询然后返回的任务,那就没什么大问题。但如果你排队的是一个需要计算半秒钟的任务,那就会有问题。 - John Saunders
@John:我知道I/O与CPU问题,但是你的评论让我想起了.NET中大部分未记录的I/O完成端口使用情况,所以我决定运行一些测试来验证我的猜测。我已经更新了答案,你基本上是正确的,它们是“ThreadPool”线程,但不与工作线程共享,它们是专门为IOCP回调保留的线程。 - Aaronaught

10
您没有说明正在使用哪个版本的IIS或ASP.NET。很多人都把IIS和ASP.NET当作同一个东西来谈论,但实际上它们是两个协同工作的组件。请注意,IIS 6和7侦听I/O完成端口,从HTTP.sys接收完成请求。IIS线程池用于此操作,其最大线程数为256。该线程池的设计并不擅长处理长时间运行的任务。 IIS团队的建议是,如果您要执行大量工作(由ASP.NET ISAPI和/或在IIS 7上的“集成模式”处理程序执行的工作),则切换到另一个线程。否则,您将会占用IIS线程并阻止IIS从HTTP.sys中接收完成请求。 您可能并不关心这些内容,因为您没有编写本机代码,也就是说,您没有为IIS 7管道编写ISAPI或本机处理程序。您可能只是使用ASP.NET,因此更关心它的线程池及其工作方式。
有一篇博客文章介绍了ASP.NET如何使用线程:http://blogs.msdn.com/tmarq/archive/2007/07/21/asp-net-thread-usage-on-iis-7-0-and-6-0.aspx。请注意,对于IIS 7上的ASP.NET v2.0和v3.5,应将MaxConcurrentRequestsPerCPU增加到5000- 默认情况下设置为12是一个错误。在IIS 7上,ASP.NET v4.0的MaxConcurrentRequestsPerCPU的新默认值为5000。
回答您的三个问题:

1) 首先简单介绍一下,每个 CPU 一次只能执行一个线程。当你有多个线程时,就需要付出代价——每次 CPU 切换到另一个线程时都需要做上下文切换,而这些是很昂贵的。然而,如果一个线程被阻塞等待工作...那么切换到另一个可以立即执行的线程是有意义的。

那么如果我有一个线程正在执行大量的计算工作并且重度使用 CPU,并且这需要很长时间,我应该切换到另一个线程吗?不!当前线程正在有效地使用 CPU,所以切换只会产生上下文切换的成本。

那么如果我有一个线程向另一个服务器发出 HTTP 或 SOAP 请求并且需要很长时间,我应该切换线程吗?是的!您可以异步执行 HTTP 或 SOAP 请求,因此一旦“发送”完成,您就可以取消当前线程并且在“接收”完成之前不使用任何线程。在“发送”和“接收”之间,远程服务器正忙,因此在本地,您不需要在一个线程上阻塞,而是要利用 .NET Framework 提供的异步 API,以便您可以取消并在完成时得到通知。

好的,那么你的第一个问题是“这些其他线程在哪里?是否有另一个线程池?”这取决于情况。大多数在 .NET Framework 中运行的代码都使用 CLR 线程池,它由两种类型的线程组成:工作线程和 I/O 完成线程。那么没有使用 CLR 线程池的代码呢?那么它可以创建自己的线程,使用自己的线程池或者任何它想要的,因为它可以访问操作系统提供的 Win32 API。根据我们之前讨论的内容,线程来自哪里并不重要,在操作系统和硬件方面,线程就是线程。

2) 在您的第二个问题中,您说:“我不明白将该请求移动到其他线程池的好处。” 如果你没有想要弥补那个昂贵的上下文切换,那么你就是正确的。这就是为什么我举了一个慢速HTTP或SOAP请求到远程服务器的例子,作为切换的一个好理由。顺便说一下,ASP.NET没有创建任何线程。它使用CLR线程池,而该池中的线程完全由CLR管理。他们很好地确定需要更多线程的时间。例如,这就是为什么ASP.NET可以轻松地从并发执行1个请求扩展到并发执行300个请求,而不需要做任何事情。传入的请求通过调用QueueUserWorkItem被发布到CLR Threadpool中,而CLR则决定何时调用WaitCallback(请参阅MSDN)。

3) 第三个问题是,“如果主线程将请求交给另一个线程,为什么请求不会断开连接?” 嗯,在请求最初到达服务器时,IIS从HTTP.sys接收I/O完成。然后,IIS调用ASP.NET的处理程序(或ISAPI)。 ASP.NET立即将请求排队到CLR Threadpool中,并返回挂起状态给IIS。这个挂起状态告诉IIS我们还没有完成,但是一旦我们完成了,我们会让您知道的。现在,ASP.NET管理该请求的生命周期。当CLR ThreadPool线程调用ASP.NET WaitCallback(请参阅MSDN)时,它可以在该线程上执行整个请求,这是正常情况。或者,如果请求是异步的(即具有异步模块或处理程序),它可以切换到一个或多个其他线程。无论哪种方式,请求都有明确定义的完成方式,当最终完成时,ASP.NET将告诉IIS我们完成了,如果不使用Keep-Alive,则IIS将向客户端发送最后的字节并关闭连接。

敬礼, 托马斯


3

1)线程位于w3svc或运行ASP.NET引擎的IIS特定版本中的进程中。

2)不确定你的意思。实际上,您可以控制工作线程池中有多少线程。这篇文章非常好:http://msdn.microsoft.com/en-us/library/ms998549.aspx

3)我认为您混淆了请求和连接...老实说,我不知道IIS的内部工作原理,但通常在同时处理多个请求的应用程序中,会有一个主监听线程,然后将实际工作交给子线程(并且什么也不做)。原始请求没有“断开”,因为这些事情发生在网络协议栈的完全不同的级别上。Windows Server没有问题接受TCP端口80上的多个连接。考虑一下TCP/IP的工作方式以及它正在发送多个离散的信息包。您认为“连接”就像从A水龙头到B水龙头的单个软管,但当然它并不是真的。它更类似于收集任何溢出物的桶。

希望这有所帮助。


1

答案还取决于您所谈论的IIS版本。在早期版本中,ASP.NET没有使用“IIS线程”。它们是.NET线程池线程。在IIS 7中,IIS和ASP.NET管道已合并。我不知道ASP.NET现在使用哪些线程。

底线是,不要生成自己的线程。


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