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);
}
在那里设置断点,你会发现availableWorkers
比maxWorkers
少一个,而maxIO
和availableIO
是相同的。
但在.NET中,一些异步操作是“特殊”的。这实际上与ASP.NET无关 - 它们在Winforms或WPF应用程序中也会使用I/O完成端口。例如:
这只是一个不完整的列表。基本上,几乎.NET Framework中的每个类都会暴露自己的
BeginXYZ
和
EndXYZ
方法,并且可能执行任何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
,但是
availableIO
比
maxIO
少一个。那是因为您正在运行 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,这并不是一个“为什么请求没有被断开”的问题,更像是一个“为什么会”这样的问题。套接字并不会因为当前没有线程正在发送或接收数据而被关闭,就像如果没有人迎接客人,你的前门也不会自动关闭一样。如果服务器没有回答客户端操作,客户端操作可能会超时,并可能选择从其端断开连接,但这是另一个问题。