取消 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 为假?的主要内容,如果未能解决你的问题,请参考以下文章
HTTP 请求 PostAsync 与 HttpClient 取消请求或资源暂时不可用
在 Angular httpclient 拦截器中处理取消的 http 请求
为啥使用 HttpClient 而不是 HttpWebRequest 进行同步请求