【问题标题】:What happens when HttpClient usages are not awaited不等待 HttpClient 使用时会发生什么
【发布时间】:2014-08-21 16:25:22
【问题描述】:

给定的代码类似于

Task.Run(() =>
{
    using (var client = new HttpClient())
    {
        var responseTask = client.GetAsync(urlToInvoke);
    }
});

在这种情况下,GetAsync 似乎没有实际运行。任务是在完成之前取消还是这里实际发生了什么?

现在,如果你稍微改变一下并插入

Task.Run(() =>
{
    using (var client = new HttpClient())
    {
        var responseTask = client.GetAsync(urlToInvoke);

        Task.Delay(5000).Wait()
    }
});

GetAsync 确实完全执行。这里发生了什么? Task.Delay 是否将自己与 responseTask 内部的同一个 Task 关联最终使其等同于 responseTask.Wait()

【问题讨论】:

  • 什么都没有发生......你只是得到了一个任务对象,它永远不会被执行。
  • @CrazyDart 那么为什么它在包含Task.Delay 的情况下“工作正常”?
  • 如果一个未等待的任务以异常结束,应用程序may crash。当您等待任务时,您会将异常传递到await 的位置。
  • 我怀疑它不是导致它工作的延迟......它是等待。如果您访问结果或调用等待,它将在该任务对象上执行。

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


【解决方案1】:

你想错了。这是类内部发生的事情的伪版本。

class HttpClient : IDisposeable
{
    private CancelationTokenSource _disposeCts;

    public HttpClient()
    {
        _disposeCts = new CancelationTokenSource();
    }

    public Task<HttpResponseMessage> GetAsync(string url)
    {
        return GetAsync(url, CancellationToken.None);
    }

    public async Task<HttpResponseMessage> GetAsync(string url, CancelationToken token)
    {
        var combinedCts =
            CancellationTokenSource.CreateLinkedTokenSource(token, _disposeCts.Token);
        var tokenToUse = combinedCts.Token;

        //... snipped code

        //Some spot where it would good to check if we have canceled yet.
        tokenToUse.ThrowIfCancellationRequested();

        //... More snipped code;

        return result;
    }

    public void Dispose()
    {
        _disposeCts.Cancel();
    }

    //... A whole bunch of other stuff.
}

重要的是,当您退出 using 块时,内部取消令牌被取消。

在您的第一个示例中,任务尚未完成,因此如果调用 ThrowIfCancellationRequested()tokenToUse 现在将抛出。

在您的第二个示例中,任务已经完成,因此取消内部令牌的行为对返回的任务没有影响,因为它已经达到完成状态。

这就像问为什么这会导致任务被取消。

using (var client = new HttpClient())
{
    var cts = new CancellationTokenSource()
    var responseTask = client.GetAsync(urlToInvoke, cts.Token);

    cts.Cancel();
}

但这不是

using (var client = new HttpClient())
{
    var cts = new CancellationTokenSource()
    var responseTask = client.GetAsync(urlToInvoke, cts.Token);

    Task.Delay(5000).Wait()
    cts.Cancel();
}

【讨论】:

  • 很好的说明,我所有的注意力都放在了 TPL 上,而我从来没有考虑过 HttpClient 处置的影响。
【解决方案2】:

当您不await(或Wait)任务时,它们不会自行取消。它们会继续运行,直到达到以下三种状态之一:

  • RanToCompletion - 成功完成。
  • 已取消 - 取消令牌已取消。
  • Faulted - 任务内部发生未处理的异常。

但是,在您的情况下,由于没有人等待任务完成,因此处理 HttpClient 的使用范围结束。这反过来将取消所有客户端的任务,在这种情况下为client.GetAsync(urlToInvoke)。这样内部async 任务将立即结束并被取消,而外部任务 (Task.Run) 将简单地结束而不做任何事情。

当您使用Task.Delay(5000).Wait()(基本上是Thread.Sleep(5000))时,任务有机会在使用范围结束之前完成。然而,应该避免这种操作模式。它会阻塞整个Wait 中的线程,并可能导致单线程SynchronizationContexts 中的死锁。这也隐藏了任务中可能的异常(这可能会在早期版本的 .Net 中破坏应用程序)

您应该始终等待任务完成,最好是异步完成,正如 Servy 所说,这里没有理由使用 Task.Run 进行卸载,因为 GetAsync 是异步的,不会阻塞调用线程。

using (var client = new HttpClient())
{
    var response = await client.GetAsync(urlToInvoke);
}

【讨论】:

  • 这里也不需要使用Task.Run,因为该方法本身是异步的,不会阻塞当前线程。
  • @Servy 我根本不希望 Worker 线程阻塞,这就是它在 Task.Run 语句中的原因。否则我会在原始线程上支付等待时间,在我的情况下,对站点的第一个请求会使唤醒时间更长。
  • @ChrisMarisic 但是代码已经是异步的。在另一个线程中启动异步操作是没有意义的。它已经不会阻塞当前线程。这就是异步的含义。
  • Thread 1 { async stuff, takes 10 seconds } WaitAll() 线程 1 仍然需要 10 秒。 Thread 1 { Thread 2 { async stuff, takes 10 seconds } WaitAll() } } 线程 2 需要 10 秒,线程 1 需要 0。除非我们陷入迂腐,否则在场景 1 作为 IIS 工作线程中,客户端(Web 浏览器)肯定会被阻塞整整 10 秒。场景 2 客户端完全没有被屏蔽。
猜你喜欢
  • 1970-01-01
  • 2020-08-24
  • 2012-09-11
  • 1970-01-01
  • 2014-11-22
  • 2016-01-10
  • 2018-12-28
  • 1970-01-01
  • 2011-10-04
相关资源
最近更新 更多