【问题标题】:Using Task or async/await in IHttpAsyncHandler在 IHttpAsyncHandler 中使用 Task 或 async/await
【发布时间】:2012-03-02 18:47:30
【问题描述】:

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

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

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

然后我想用一个新的Task或者async/await来写IHttpAsyncHandler。你可以找到一个例子是 Drew Marsh 在这里解释的:https://stackoverflow.com/a/6389323/261950

我的猜测是,使用 Task 或 async/await 仍会消耗 ASP.NET 线程池中的线程,并且出于明显的原因我不想要。

您能否告诉我我是否可以在后台线程上使用 Task (async/await) 就像使用 System.Threading.Thread而不是来自线程池? p>

提前感谢您的帮助。

托马斯

【问题讨论】:

  • 从同一个池中消耗线程会损害性能对我来说并不明显。你确定是这样吗?
  • 事实上我依赖。如果我理解得很好,框架 4 中的 asp.net 线程池中的线程没有更多限制。因此,如果需要处理更多请求,则将新线程注入线程池。我记得在框架 4 之前,线程池有一个限制。
  • tl;dr 对于未来的访问者,此时的规范解决方案(正如斯蒂芬在他的回答中提到的那样)是只使用 HttpTaskAsyncHandler

标签: asp.net threadpool task-parallel-library async-await ihttpasynchandler


【解决方案1】:

这种情况是 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 的代码相同。但 IMO 的 async 代码更易于阅读。

如果您想了解更多关于async 的信息,我的博客上有intro post

【讨论】:

  • 一个 ASP.NET 请求线程启动请求,并开始使用 WebClient 下载。 -----仅适用于驱动程序执行工作的 IO 操作然后它被转移到一个线程池线程。如果它不使用 IO 操作,将会(!!!!)有一个后台线程来执行操作。
  • @RoyiNamir:后台线程仅在您明确请求时使用(例如,Task.Run)。除了少数例外(例如,从 MemoryStream 异步读取),BCL 永远不会代表您执行此操作。因此,避免不必要的后台线程真的很容易 - 只是不要使用它们!
  • 您写道:“异步的真正酷之处在于它在执行后台操作时不需要任何线程:”.......我只是说它确实需要后台线程,除非您使用 IO 异步方法(例如 FileStream.BeginRead )。这不会绑定线程...。定期下载时 --- 会绑定线程(后台线程)
  • 不知道你在做什么。使用DownloadDataTaskAsync 下载WebClient 不会占用后台线程。
  • async - 本身 - 将使用任何后台线程。除非你通过调用Task.Run明确告诉它
【解决方案2】:

说“0(零)个线程将为该请求提供服务”并不完全准确。 我认为您的意思是“来自 ASP.NET 线程池”,在一般情况下这是正确的。

什么时候 async/await 不会消耗额外的 ThreadPool 线程? 仅在您使用 BCL 异步方法(如 WebClient 异步扩展提供的方法)的情况下,该方法使用 IOCP 线程来执行 IO 绑定操作。

如果您尝试异步执行某些同步代码或您自己的库代码,则该代码可能会使用额外的线程池线程,除非您明确使用 IOCP 线程池或您自己的线程池。

谢谢, 安德烈斯。

【讨论】:

  • 安德烈斯,这很有趣。因此,除非底层实现使用它自己的线程池,否则使用 async/await(或 Task)首先会消耗 ASP.NET 线程池线程。因此,例如在 AsyncHandler 中进行昂贵的计算将使用另一个线程,但它将取自 ASP.NET ThreadPool?你能确认吗?你有我可以进一步阅读的链接吗?
  • 看我下面的帖子,试图在这里回答,但字符用完了...... :)
【解决方案3】:

Parallel Extensions 团队有a blog post 介绍将 TPL 与 ASP.NET 结合使用,其中解释了 TPL 和 PLINQ 如何使用 ASP.NET 线程池。该帖子甚至还有一个决策图,可帮助您选择正确的方法。

简而言之,PLINQ 使用线程池中的每个内核一个工作线程来执行整个查询,如果流量很大,这可能会导致问题。

另一方面,Task 和 Parallel 方法将适应进程的资源,并且可以使用最少一个线程进行处理。

就 Async CTP 而言,async/await 构造与直接使用 Tasks 在概念上几乎没有区别。编译器使用一些魔法在幕后将等待转换为任务和继续。最大的不同是你的代码更干净,更容易调试。

【讨论】:

    【解决方案4】:

    要考虑的另一件事是 async/await 和 TPL(任务)不是一回事。

    请阅读这篇出色的帖子 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 中,因此昂贵的计算将使用 ThreadPool 中的当前线程。 2) 使用 Task.Run 或 Delegate 在另一个 ThreadPool 线程中运行昂贵的计算代码 3) 在自定义线程池(或 IOCP 线程池)的另一个线程中运行昂贵的计算代码。

    第二种情况对您来说可能就足够了,具体取决于您的“计算”过程运行多长时间以及您有多少负载。安全选项是#3,但在编码/测试方面要贵得多。我还建议始终将 .NET 4 用于使用异步设计的生产系统,因为 .NET 3.5 中有一些硬性限制。

    【讨论】:

      【解决方案5】:

      这几天我一直在通过互联网寻找信息。让我总结一下我到目前为止的发现:

      ASP.NET 线程池事实

      • 正如 Andres 所说:什么时候 async/await 不会消耗额外的 ThreadPool 线程?仅在您使用 BCL 异步方法的情况下。使用 IOCP 线程执行 IO 绑定操作。

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

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

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

      • Async delegates 使用 ThreadPool 中的线程。

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

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

      • 如果你想做 I/O 工作,那么你应该使用 I/O 线程(I/O 完成端口),特别是你应该使用任何库类支持的异步回调使用。他们的名字以BeginEnd开头。

      • 如果处理请求的计算成本很低,那么并行性可能是不必要的开销。

      • 如果传入的请求率很高,那么增加更多的并行性可能会带来很少的好处,实际上可能会降低性能,因为传入的工作率可能高到足以让 CPU 保持忙碌。

      我应该创建新线程吗?

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

      • 如果您实际上排队了足够多的工作项以阻止 ASP.NET 处理进一步的请求,那么您应该使线程池处于饥饿状态!如果您同时运行数百个 CPU 密集型操作,那么在机器已经超载的情况下,让另一个工作线程来处理 ASP.NET 请求会有什么好处。

      TPL 呢?

      • TPL 可以适应在进程中使用可用资源。如果服务器已经加载,TPL 可以使用最少一个工作人员并向前推进。如果服务器大部分是免费的,它们可以增长到使用线程池可以腾出的尽可能多的工作线程。

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

      参考文献

      【讨论】:

        【解决方案6】:

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

        【讨论】:

          猜你喜欢
          • 2015-04-06
          • 1970-01-01
          • 2012-08-27
          • 2019-10-08
          • 2013-09-13
          • 1970-01-01
          • 2015-09-09
          • 1970-01-01
          • 2018-06-05
          相关资源
          最近更新 更多