在IHttpAsyncHandler中使用Task或async/await

18

自从我开始写ASP.NET应用程序以来,当想要添加线程时,我有三种简单的方法可以在我的ASP.NET应用程序中完成线程:

  • 使用 System.Threading.ThreadPool
  • 使用自定义委托并调用其 BeginInvoke 方法。
  • 使用 System.Threading.Thread 类的自定义线程。

前两种方法提供了一种快速的方式来为应用程序启动工作线程。但不幸的是,它们会损害您的应用程序的整体性能,因为它们使用与处理HTTP请求的ASP.NET相同的线程池

然后我想使用新的任务(Task)或 async/await来编写IHttpAsyncHandler。您可以在Drew Marsh在这里解释的示例中找到一个例子:https://dev59.com/4ljUa4cB1Zd3GeqPQluI#6389323

我猜测使用Task或async/await仍会从ASP.NET线程池中使用线程,而出于显而易见的原因,我不希望这样做。

请告诉我是否可以像使用System.Threading.Thread类一样,在后台线程上使用Task (async/await)而不是从线程池中使用?

感谢您的帮助。

Thomas


我并不认为从同一线程池中消耗线程会影响性能。你确定吗? - svick
1
实际上,这取决于情况。如果我理解得很好,框架4中的asp.net线程池没有更多的限制。因此,如果需要处理更多的请求,新的线程将被注入到线程池中。在框架4之前,我记得线程池有一个限制。 - Tomasz Jaskuλa
3
简而言之,对于未来的访问者,目前最佳的解决方案(正如Stephen在他的回答中提到的)是使用HttpTaskAsyncHandler。 - Kirk Woll
6个回答

22

在这种情况下,Taskasyncawait非常出色。下面是同一个示例的重构版本,充分利用了async(它还使用了我AsyncEx库中的一些辅助类来清理映射代码):

// First, a base class that takes care of the Task -> IAsyncResult mapping.
// In .NET 4.5, you would use HttpTaskAsyncHandler instead.
public abstract class HttpAsyncHandlerBase : IHttpAsyncHandler
{
    public abstract Task ProcessRequestAsync(HttpContext context);

    IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
    {
        var task = ProcessRequestAsync(context);
        return Nito.AsyncEx.AsyncFactory.ToBegin(task, cb, extraData);
    }

    void EndProcessRequest(IAsyncResult result)
    {
        Nito.AsyncEx.AsyncFactory.ToEnd(result);
    }

    void ProcessRequest(HttpContext context)
    {
        EndProcessRequest(BeginProcessRequest(context, null, null));
    }

    public virtual bool IsReusable
    {
        get { return true; }
    }
}

// Now, our (async) Task implementation
public class MyAsyncHandler : HttpAsyncHandlerBase
{
    public override async Task ProcessRequestAsync(HttpContext context)
    {
        using (var webClient = new WebClient())
        {
            var data = await webClient.DownloadDataTaskAsync("http://my resource");
            context.Response.ContentType = "text/xml";
            context.Response.OutputStream.Write(data, 0, data.Length);
        }
    }
}

(正如代码中所述,.NET 4.5有一个HttpTaskAsyncHandler与我们上面的HttpAsyncHandlerBase类似)。

async真正酷的事情在于它在执行后台操作时不需要任何线程:

  • 一个ASP.NET请求线程启动请求并使用WebClient开始下载。
  • 当下载进行时,await实际上会从async方法中返回,使得请求线程退出,然后将该请求线程归还给线程池 - 这样请求时就有0个(零)线程服务了。
  • 当下载完成时,async方法将在一个请求线程上恢复。该请求线程仅短暂地用于编写实际响应。

这是最佳的线程解决方案(因为需要一个请求线程来编写响应)。

原始示例还以最佳方式使用线程 - 就线程而言,它与基于async的代码相同。但是我认为async代码更容易阅读。

如果您想了解更多关于async的内容,我在我的博客中有一篇介绍文章


1
一个ASP.NET请求线程启动请求,开始使用WebClient进行下载。对于由驱动程序完成工作并稍后传输到线程池线程的IO操作来说,这是正确的。如果不使用IO操作,将会有一个后台线程执行操作。 - Royi Namir
2
@RoyiNamir:只有在显式请求时(例如Task.Run),才会使用后台线程。除了少数例外情况(例如从MemoryStream异步读取),BCL永远不会代表您执行此操作。因此,避免不必要的后台线程非常容易 - 只需不使用它们即可! - Stephen Cleary
1
你写道:“异步的真正酷之处在于在执行后台操作时不会占用任何线程:”......我只是想说,除非使用IO异步方法(例如FileStream.BeginRead),否则它确实会占用一个后台线程。这将不会绑定线程……而常规下载将会绑定一个线程(后台线程)。 - Royi Namir
2
不确定你的意思。使用DownloadDataTaskAsync进行WebClient下载不会占用后台线程。 - Stephen Cleary
4
async本身不会使用任何后台线程,除非你通过调用Task.Run来显式地告诉它这样做。请注意不改变原意,并使其通俗易懂。 - Stephen Cleary
显示剩余6条评论

7

我已经通过互联网查找了几天的信息。现在让我总结一下:

ASP.NET线程池事实

  • 正如Andres所说:当async/await不会消耗额外的线程池线程时?只有在使用BCL Async方法的情况下才会使用IOCP线程执行IO绑定操作。

  • Andres继续说...如果您正在尝试异步执行一些同步代码或您自己的库代码,则该代码可能会使用额外的ThreadPool线程,除非您明确使用IOCP ThreadPool或您自己的ThreadPool。

但据我所知,您无法选择是否要使用IOCP线程,并且正确实现ThreadPool不值得付出努力。我怀疑是否有人会做出比已经存在的更好的实现。

  • ASP.NET使用来自公共语言运行时(CLR)线程池的线程来处理请求。只要线程池中有可用线程,ASP.NET就可以轻松地分派传入的请求。

  • 异步委托使用ThreadPool中的线程。

什么时候应该开始考虑实现异步执行?

  • 当您的应用程序执行相对较长的I/O操作(数据库查询、Web服务调用和其他I/O操作)时

  • 如果要进行I/O工作,则应使用I/O线程(I/O完成端口),并且特别是应使用您正在使用的任何库类支持的异步回调。它们的名称以单词BeginEnd开头。

  • 如果请求的处理成本较低,则并行处理可能是不必要的开销。

  • 如果传入请求率很高,则增加更多的并行性可能会产生少量的好处,实际上可能会降低性能,因为工作的传入速率可能足以使CPU保持繁忙状态。

我应该创建新线程吗?

  • 避免像避免瘟疫一样创建新线程。

  • 如果您实际上正在排队足够的工作项以防止ASP.NET进一步处理请求,则应使线程池饥饿!如果您同时运行数百个CPU密集型操作,那么当机器已经超载时,再有另一个工作线程来服务于ASP.NET请求有什么好处呢?

那么TPL呢?

  • TPL可以适应进程中可用的资源。如果服务器已经负载,TPL可以只使用一个工作线程并取得前进。如果服务器大部分空闲,它们可以增长到使用线程池可以节约的尽可能多的工作线程。

  • 任务使用线程池线程执行。

参考文献


3
说“0(零)线程将服务于此请求”并不完全准确。我认为你的意思是“来自ASP.NET线程池”,在一般情况下这是正确的。
什么情况下async/await不会消耗额外的ThreadPool线程? 只有在使用BCL Async方法(如WebClient async扩展提供的方法),该方法使用IOCP线程执行IO绑定操作时才会发生。
如果您尝试异步执行一些同步代码或自己的库代码,那么该代码可能会使用额外的ThreadPool线程,除非您明确使用IOCP ThreadPool或自己的ThreadPool。
谢谢, Andrés.

Andrés,这是一个非常有趣的观点。因此,使用async/await(或Task)首先会占用ASP.NET线程池线程,除非底层实现使用自己的线程池。因此,在AsyncHandler中执行昂贵的计算将使用另一个线程,但它将从ASP.NET线程池中获取?您能确认这一点吗?您有任何可以阅读更多信息的链接吗? - Tomasz Jaskuλa
请看我下面的帖子,我试图在这里回答,但是字符用完了... :) - Andres Vettori

1

并行扩展团队发布了一篇博客文章,介绍了如何在ASP.NET中使用TPL,解释了TPL和PLINQ如何使用ASP.NET线程池。该文章甚至还有一个决策图,帮助您选择正确的方法。

简而言之,PLINQ为查询的整个执行过程使用线程池中每个核心的一个工作线程,这可能会导致高流量时出现问题。

另一方面,任务和并行方法将适应进程的资源,并可以使用尽可能少的线程进行处理。

就异步CTP而言,async/await结构和直接使用任务之间的概念差异很小。编译器使用一些魔法将等待转换为任务和延续。最大的区别是您的代码更加清晰易懂,更容易调试。


1
另一个需要考虑的事情是async/await和TPL(Task)不是同一件事。
请阅读这篇优秀文章http://blogs.msdn.com/b/ericlippert/archive/2010/11/04/asynchrony-in-c-5-0-part-four-it-s-not-magic.aspx了解为什么async/await并不意味着“使用后台线程”。
回到我们的话题,在您想要在AsyncHandler中执行一些昂贵的计算的情况下,您有三个选择:
1)将代码留在Asynchandler中,以便昂贵的计算将使用线程池中的当前线程。 2)通过使用Task.Run或Delegate在另一个ThreadPool线程中运行昂贵的计算代码。 3)在自定义线程池(或IOCP线程池)中的另一个线程中运行昂贵的计算代码。

第二种情况可能足够,这取决于您的“计算”过程运行时间和负载大小。安全选项是#3,但编码/测试成本更高。我还建议在使用异步设计的生产系统中始终使用.NET 4,因为.NET 3.5存在一些硬性限制。


0

在SignalR项目中,有一个很好的针对.NET 4.0的HttpTaskAsyncHandler实现。你可能想要检查一下:http://bit.ly/Jfy2s9


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