【问题标题】:HttpClient.SendAsync using the thread-pool instead of async IO?HttpClient.SendAsync 使用线程池而不是异步 IO?
【发布时间】:2014-07-27 17:34:53
【问题描述】:

所以我一直在研究通过 Reflector 实现 HttpClient.SendAsync。我特意想了解这些方法的执行流程,并确定调用哪个 API 来执行异步 IO 工作。

在探索了HttpClient 内部的各种类之后,我看到它在内部使用了HttpClientHandler,它派生自HttpMessageHandler 并实现了它的SendAsync 方法。

这是HttpClientHandler.SendAsync的实现:

protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    if (request == null)
    {
        throw new ArgumentNullException("request", SR.net_http_handler_norequest);
    }

    this.CheckDisposed();
    this.SetOperationStarted();

    TaskCompletionSource<HttpResponseMessage> source = new TaskCompletionSource<HttpResponseMessage>();

    RequestState state = new RequestState 
    {
        tcs = source,
        cancellationToken = cancellationToken,
        requestMessage = request
    };

    try
    {
        HttpWebRequest request2 = this.CreateAndPrepareWebRequest(request);
        state.webRequest = request2;
        cancellationToken.Register(onCancel, request2);

        if (ExecutionContext.IsFlowSuppressed())
        {
            IWebProxy proxy = null;

            if (this.useProxy)
            {
                proxy = this.proxy ?? WebRequest.DefaultWebProxy;
            }
            if ((this.UseDefaultCredentials || (this.Credentials != null)) || ((proxy != null) && (proxy.Credentials != null)))
            {
                this.SafeCaptureIdenity(state);
            }
        }

        Task.Factory.StartNew(this.startRequest, state);
    }
    catch (Exception exception)
    {
        this.HandleAsyncException(state, exception);
    }
    return source.Task;
}

我觉得奇怪的是,上面使用Task.Factory.StartNew 执行请求,同时生成一个TaskCompletionSource&lt;HttpResponseMessage&gt; 并返回由它创建的Task

为什么我觉得这很奇怪?好吧,我们继续讨论 I/O 绑定的异步操作如何在幕后不需要额外的线程,以及它是如何与重叠 IO 相关的。

为什么要使用Task.Factory.StartNew 来触发异步 I/O 操作?这意味着SendAsync 不仅使用纯异步控制流来执行此方法,而且还旋转一个 ThreadPool 线程“在我们背后” 来执行它的工作。

【问题讨论】:

    标签: c# asynchronous task-parallel-library dotnet-httpclient


    【解决方案1】:

    this.startRequest 是一个指向 StartRequest 的委托,而 StartRequest 又使用 HttpWebRequest.BeginGetResponse 启动异步 IO。 HttpClient 在幕后使用异步 IO,只是包装在线程池任务中。

    也就是说,请注意以下comment in SendAsync

    // BeginGetResponse/BeginGetRequestStream have a lot of setup work to do before becoming async
    // (proxy, dns, connection pooling, etc).  Run these on a separate thread.
    // Do not provide a cancellation token; if this helper task could be canceled before starting then 
    // nobody would complete the tcs.
    Task.Factory.StartNew(startRequest, state);
    

    This works around a well-known problem with HttpWebRequest: Some of its processing stages are synchronous. 这是该 API 的一个缺陷。 HttpClient 正在通过将 DNS 工作移至线程池来避免阻塞。

    这是好事还是坏事?这很好,因为它使HttpClient 非阻塞并且适合在 UI 中使用。这很糟糕,因为我们现在使用线程进行长时间运行的阻塞工作,尽管我们预计根本不使用线程。这降低了使用异步 IO 的好处。

    实际上,这是混合同步和异步 IO 的一个很好的例子。两者都使用并没有本质上的错误。 HttpClientHttpWebRequest 正在使用异步 IO 进行长时间运行的阻塞工作(HTTP 请求)。他们将线程用于短期工作(DNS,...)。一般来说,这不是一个糟糕的模式。我们正在避免大多数阻塞,我们只需要使一小部分代码异步。典型的 80-20 权衡。在 BCL(一个库)中找到这样的东西并不好,但在应用程序级代码中可以是一个非常明智的权衡。

    似乎最好修复HttpWebRequest。也许出于兼容性原因,这是不可能的。

    【讨论】:

    • 天哪!谢谢。我正在查看sourceof.net 并寻找该方法实现,它没有显示任何,只有方法签名。这确实是一种耻辱!我想知道是否有任何来自 MSFT 的人可以对此发表评论。
    • 这似乎是一个很小的实现细节,但这相当大,不是吗?如果我使用HttpClient.XXXAsync 方法一起查询许多请求,我不希望它们每个都在内部使用 ThreadPool 线程。这在任何地方都没有记录。
    • @YuvalItzchakov 是的,我很失望。这低于我们在使用 BCL 时惯用的质量标准。
    • 也就是说,不要害怕线程。同步 IO 并没有那么有害,而且现在人们对它的发生有一种非理性的恐惧。即使有 100 个专用于 DNS 解析的线程也不会真正造成任何损害。主要问题是每个线程 1MB 的堆栈大小。主要是这样。
    • 在线程池初始化时无论如何都会分配 1MB 的堆栈,不是吗?假设它不是默认分配的线程数。
    猜你喜欢
    • 2012-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-22
    • 2017-06-20
    • 2019-03-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多