问题
Given the following code:
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");
}
I would expect ex.CancellationToken.IsCancellationRequested
to be true
inside the catch block, but it is not. Am I misunderstanding something?
回答1:
That's the case because HttpClient
internally (in SendAsync
) is using a TaskCompletionSource
to represent the async
operation. It returns TaskCompletionSource.Task
and that's the task you await
on.
It then calls base.SendAsync
and registers a continuation on the returned task that cancels/completes/faults the TaskCompletionSource
's task accordingly.
In the case of cancellation it uses TaskCompletionSource.TrySetCanceled which associates the canceled task with a new CancellationToken
(default(CancellationToken)
).
You can see that by looking at the TaskCanceledException
. On top of ex.CancellationToken.IsCancellationRequested
being false
ex.CancellationToken.CanBeCanceled
is also false
, meaning that this CancellationToken
can never be canceled as it wasn't created using a CancellationTokenSource
.
IMO it should be using TaskCompletionSource.TrySetCanceled(CancellationToken) instead. That way the TaskCompletionSource
will be associated with the CancellationToken
passed in by the consumer and not simply the default CancellationToken
. I think it's a bug (though a minor one) and I submitted an issue on connect about it.
回答2:
@Bengie This didn't work for me. I had to alter it a little. IsCancellationRequested always returned true so i couldn't rely on that.
This worked for me:
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:
I set the timeout to infinite to disable it, then I pass in my own cancellation token.
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;
}
来源:https://stackoverflow.com/questions/29319086/cancelling-an-httpclient-request-why-is-taskcanceledexception-cancellationtoke