取消 HttpClient 请求 - 为啥 TaskCanceledException.CancellationToken.IsCancellationRequested 为假?

Posted

技术标签:

【中文标题】取消 HttpClient 请求 - 为啥 TaskCanceledException.CancellationToken.IsCancellationRequested 为假?【英文标题】:Cancelling an HttpClient Request - Why is TaskCanceledException.CancellationToken.IsCancellationRequested false?取消 HttpClient 请求 - 为什么 TaskCanceledException.CancellationToken.IsCancellationRequested 为假? 【发布时间】:2015-03-28 15:44:30 【问题描述】:

给定以下代码:

var cts = new CancellationTokenSource();

try 

    // get a "hot" task
    var task = new HttpClient().GetAsync("http://www.google.com", cts.Token);

    // request cancellation
    cts.Cancel();

    await task;

    // pass:
    Assert.Fail("expected TaskCanceledException to be thrown");

catch (TaskCanceledException ex) 

    // pass:
    Assert.IsTrue(cts.Token.IsCancellationRequested,
        "expected cancellation requested on original token");

    // fail:
    Assert.IsTrue(ex.CancellationToken.IsCancellationRequested,
        "expected cancellation requested on token attached to exception");

我希望 ex.CancellationToken.IsCancellationRequested 在 catch 块内是 true,但事实并非如此。我是不是误会了什么?

【问题讨论】:

ex.CancelationToken 实例是否等于 (ReferenceEqual) 到 cts?文档指出:“如果令牌与取消的操作相关联,则令牌的 CancellationToken.IsCancellationRequested 属性返回 true”。 @Alex: CancellationToken 是一个结构体,所以ReferenceEquals() 将始终返回 false。 @AndreasNiedermair:没有。我的意思是,如果你有CancellationToken token = cts.Token; 并评估object.ReferenceEquals(token, token)(即将CancellationToken与其自身 进行比较),即使这样也会返回false,因为值类型必须在被装箱之前被装箱作为object 引用传递,因此装箱的对象将总是不同,即使它们是从相同的值获得的。 @usr:他发布的代码有效。我将它复制/粘贴到一个空白控制台应用程序中(以它自己的方法,因为Main() 不能是async),它的行为与报告的完全相同。 请注意,此代码具有竞争条件。操作可能被取消,或者它可能在被取消之前正常完成。您应该在开始实际工作之前取消令牌(或确保在取消令牌之前工作不可能完成)以确保不会发生这种情况。 【参考方案1】:

之所以如此,是因为HttpClient 内部(在SendAsync 中)使用TaskCompletionSource 来表示async 操作。它返回 TaskCompletionSource.Task,这就是你 await 执行的任务。

然后它调用base.SendAsync 并在返回的任务上注册一个延续,相应地取消/完成/故障TaskCompletionSource 的任务。

在取消的情况下,它使用TaskCompletionSource.TrySetCanceled 将取消的任务与新的CancellationToken (default(CancellationToken)) 相关联。

您可以通过查看TaskCanceledException 看到这一点。在ex.CancellationToken.IsCancellationRequested 之上是false ex.CancellationToken.CanBeCanceled 也是false,这意味着这个CancellationToken 永远不能被取消,因为它不是使用CancellationTokenSource 创建的。


IMO 它应该改用TaskCompletionSource.TrySetCanceled(CancellationToken)。这样TaskCompletionSource 将与消费者传入的CancellationToken 相关联,而不仅仅是默认的CancellationToken。我认为这是一个错误(尽管是一个小错误),我提交了一个 issue on connect 关于它。

【讨论】:

很好的答案,+1 用于报告问题。我不太确定这是次要的,因为我(和others)依靠它来区分用户取消和HttpClient 的超时。 @ToddMenier 这是一个非常有趣的问题。它也让我找到了这个有趣的(我希望的)发现:***.com/q/29355165/885318 嗯,你所想的重载是内部的,所以可能有某种原因不应该以这种方式使用它。在进行TaskCompletionSource 取消操作时会发生很多有趣的事情(例如,您可以尝试多次取消 TCS)。而且 TCS 和它内部创建的 Task 都与取消令牌有任何关系——这只是那些泄漏的抽象之一。确实有两种Task,Task-as-CPU-work vs Task-as-async-I/O,HttpClient 使用后者。有时我希望他们真的是分开的…… 为我连接问题链接 404s @StriplingWarrior 你打开的问题在同一个地方。无论如何,他们上周回复说它将为 .NET 4.7.2 修复【参考方案2】:

@Bengie 这对我不起作用。我不得不稍微改变一下。 IsCancellationRequested 总是返回 true,所以我不能依赖它。

这对我有用:

using (CancellationTokenSource cancelAfterDelay = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)))

    DateTime startedTime = DateTime.Now;

    try
    
        response = await request.ExecuteAsync(cancelAfterDelay.Token);
    
    catch (TaskCanceledException e)
    
        DateTime cancelledTime = DateTime.Now;
        if (startedTime.AddSeconds(timeout-1) <= cancelledTime)
        
            throw new TimeoutException($"An HTTP request to request.Url timed out (timeout seconds)");
        
        else
            throw;
    

return response;

【讨论】:

【参考方案3】:

我将超时设置为无限以禁用它,然后传入我自己的取消令牌。

using(CancellationTokenSource cancelAfterDelay = new CancellationTokenSource(timespan/timeout))
...
catch(OperationCanceledException e)

if(!cancelAfterDelay.Token.IsCancellationRequested)
throw new TimeoutException($"An HTTP request to request.Uri timed out ((int)requestTimeout.TotalSeconds seconds)");
else
throw;

【讨论】:

以上是关于取消 HttpClient 请求 - 为啥 TaskCanceledException.CancellationToken.IsCancellationRequested 为假?的主要内容,如果未能解决你的问题,请参考以下文章

HttpClient获取请求取消,以防操作时间超过x分钟

HTTP 请求 PostAsync 与 HttpClient 取消请求或资源暂时不可用

在 Angular httpclient 拦截器中处理取消的 http 请求

为啥使用 HttpClient 而不是 HttpWebRequest 进行同步请求

使用 HttpClient 的 HTTP Post 请求需要 2 秒,为啥?

为啥 HttpClient.PostAsync 似乎将请求作为 GET 而不是 POST 发送?