HttpClient.SendAsync 使用线程池而不是异步 IO

本文关键字:异步 IO SendAsync 线程 HttpClient | 更新日期: 2023-09-27 18:15:10

所以我一直在挖掘通过反射器实现HttpClient.SendAsync。我有意想要了解的是这些方法的执行流程,并确定调用哪个 API 来执行异步 IO 工作。

在探索了HttpClient内部的各种类之后,我看到它在内部使用从HttpMessageHandler派生的HttpClientHandler并实现了其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<HttpResponseMessage>并返回它创建的Task

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

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

HttpClient.SendAsync 使用线程池而不是异步 IO

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

也就是说,请注意以下评论 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);

这解决了 HttpWebRequest 的一个众所周知的问题:它的某些处理阶段是同步的。这是该 API 中的一个缺陷。 HttpClient通过将 DNS 工作移动到线程池来避免阻塞。

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

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

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