传入已取消的CancellationToken会导致HttpClient挂起

时间:2014-04-15 05:23:56

标签: c# task-parallel-library dotnet-httpclient cancellationtokensource

我想使用CancellationToken取消对HttpClient.PostAsJsonAsync的通话。但是,通过以下设置,对PostAsJsonAsync的调用将无限期挂起(我将其保持运行几分钟)。

CancellationTokenSource source = new CancellationTokenSource();
source.Cancel();
HttpClient client = new HttpClient();

try
{
    var task = client.PostAsJsonAsync<MyObject>("http://server-address.com",
        new MyObject(), source.Token);

    task.Wait();
}
catch (Exception ex)
{
    //Never gets hit.
}

请注意,我正在传递已取消的CancellationTokenSource - 如果我使用Task.Delay短暂延迟取消令牌,我会遇到同样的问题。

我意识到我可以简单地检查一下令牌是否在通话之前被取消了,但即便如此,如果令牌在短暂延迟后被取消,我也会遇到同样的问题,即它在取消之前没有取消方法调用但在它之后不久就变得很快。

所以我的问题是,导致这种情况的原因是什么,我该怎么做才能解决它?

修改

对于那些寻求解决方法的人,受到@Darrel Miller的回答的启发,我提出了以下扩展方法:

public static async Task<HttpResponseMessage> PostAsJsonAsync2<T>(this HttpClient client, string requestUri, T value, CancellationToken token)
{
    var content = new ObjectContent(typeof(T), value, new JsonMediaTypeFormatter());
    await content.LoadIntoBufferAsync();

    return await client.PostAsync(requestUri, content, token);
}

2 个答案:

答案 0 :(得分:7)

它肯定是你遇到的一个错误你可以通过自己构建HttpContent / ObjectContent对象来解决它,就像这样。

CancellationTokenSource source = new CancellationTokenSource();
source.Cancel();
HttpClient client = new HttpClient();

var content = new ObjectContent(typeof (MyObject), new MyObject(), new JsonMediaTypeFormatter());
content.LoadIntoBufferAsync().Wait();
try
{
    var task = client.PostAsync("http://server-address.com",content, source.Token);

    task.Wait();
}
catch (Exception ex)
{
    //This will get hit now with an AggregateException containing a TaskCancelledException.
}

调用content.LoadIntoBufferAsync强制在PostAsync之前发生反序列化,似乎可以避免死锁。

答案 1 :(得分:6)

同意@Darrel Miller的回答。这是一个错误。只需添加错误报告的更多细节。

问题是在内部使用TaskCompletionSource,但是当由于在这种特定情况下取消而引发异常时,它没有被捕获,并且TaskCompletionSource永远不会被设置为已完成的状态(因此等待TaskCompletionSource的{​​{1}}将永远不会返回。

使用ILSpy,查看Task,您可以看到HttpClientHandler.SendAsync

TaskCompletionSource

稍后,通过行// System.Net.Http.HttpClientHandler /// <summary>Creates an instance of <see cref="T:System.Net.Http.HttpResponseMessage" /> based on the information provided in the <see cref="T:System.Net.Http.HttpRequestMessage" /> as an operation that will not block.</summary> /// <returns>Returns <see cref="T:System.Threading.Tasks.Task`1" />.The task object representing the asynchronous operation.</returns> /// <param name="request">The HTTP request message.</param> /// <param name="cancellationToken">A cancellation token to cancel the operation.</param> /// <exception cref="T:System.ArgumentNullException">The <paramref name="request" /> was null.</exception> protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request == null) { throw new ArgumentNullException("request", SR.net_http_handler_norequest); } this.CheckDisposed(); if (Logging.On) { Logging.Enter(Logging.Http, this, "SendAsync", request); } this.SetOperationStarted(); TaskCompletionSource<HttpResponseMessage> taskCompletionSource = new TaskCompletionSource<HttpResponseMessage>(); HttpClientHandler.RequestState requestState = new HttpClientHandler.RequestState(); requestState.tcs = taskCompletionSource; requestState.cancellationToken = cancellationToken; requestState.requestMessage = request; this.lastUsedRequestUri = request.RequestUri; try { HttpWebRequest httpWebRequest = this.CreateAndPrepareWebRequest(request); requestState.webRequest = httpWebRequest; cancellationToken.Register(HttpClientHandler.onCancel, httpWebRequest); if (ExecutionContext.IsFlowSuppressed()) { IWebProxy webProxy = null; if (this.useProxy) { webProxy = (this.proxy ?? WebRequest.DefaultWebProxy); } if (this.UseDefaultCredentials || this.Credentials != null || (webProxy != null && webProxy.Credentials != null)) { this.SafeCaptureIdenity(requestState); } } Task.Factory.StartNew(this.startRequest, requestState); } catch (Exception e) { this.HandleAsyncException(requestState, e); } if (Logging.On) { Logging.Exit(Logging.Http, this, "SendAsync", taskCompletionSource.Task); } return taskCompletionSource.Task; } ,我们可以使用以下方法:

Task.Factory.StartNew(this.startRequest, requestState);

您会注意到// System.Net.Http.HttpClientHandler private void PrepareAndStartContentUpload(HttpClientHandler.RequestState state) { HttpContent requestContent = state.requestMessage.Content; try { if (state.requestMessage.Headers.TransferEncodingChunked == true) { state.webRequest.SendChunked = true; this.StartGettingRequestStream(state); } else { long? contentLength = requestContent.Headers.ContentLength; if (contentLength.HasValue) { state.webRequest.ContentLength = contentLength.Value; this.StartGettingRequestStream(state); } else { if (this.maxRequestContentBufferSize == 0L) { throw new HttpRequestException(SR.net_http_handler_nocontentlength); } requestContent.LoadIntoBufferAsync(this.maxRequestContentBufferSize).ContinueWithStandard(delegate(Task task) { if (task.IsFaulted) { this.HandleAsyncException(state, task.Exception.GetBaseException()); return; } contentLength = requestContent.Headers.ContentLength; state.webRequest.ContentLength = contentLength.Value; this.StartGettingRequestStream(state); }); } } } catch (Exception e) { this.HandleAsyncException(state, e); } } 调用中的委托在委托中没有异常处理,并且没有人保留返回的任务(因此当此任务抛出异常时,它将被忽略) 。对ContinueWithStandard的调用确实会引发异常:

this.StartGettingRequestStream(state);

以下是异常时的完整callstack:

System.Net.WebException occurred
  HResult=-2146233079
  Message=The request was aborted: The request was canceled.
  Source=System
  StackTrace:
       at System.Net.HttpWebRequest.BeginGetRequestStream(AsyncCallback callback, Object state)
  InnerException: 

我认为目的是不要忽略它,并且在异常的情况下调用> System.dll!System.Net.HttpWebRequest.BeginGetRequestStream(System.AsyncCallback callback, object state) Line 1370 C# System.Net.Http.dll!System.Net.Http.HttpClientHandler.StartGettingRequestStream(System.Net.Http.HttpClientHandler.RequestState state) + 0x82 bytes System.Net.Http.dll!System.Net.Http.HttpClientHandler.PrepareAndStartContentUpload.AnonymousMethod__0(System.Threading.Tasks.Task task) + 0x92 bytes mscorlib.dll!System.Threading.Tasks.ContinuationTaskFromTask.InnerInvoke() Line 59 + 0xc bytes C# mscorlib.dll!System.Threading.Tasks.Task.Execute() Line 2459 + 0xb bytes C# mscorlib.dll!System.Threading.Tasks.Task.ExecutionContextCallback(object obj) Line 2815 + 0x9 bytes C# mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 581 + 0xd bytes C# mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 530 + 0xd bytes C# mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot) Line 2785 C# mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) Line 2728 C# mscorlib.dll!System.Threading.Tasks.ThreadPoolTaskScheduler.TryExecuteTaskInline(System.Threading.Tasks.Task task, bool taskWasPreviouslyQueued) Line 91 + 0xb bytes C# mscorlib.dll!System.Threading.Tasks.TaskScheduler.TryRunInline(System.Threading.Tasks.Task task, bool taskWasPreviouslyQueued) Line 221 + 0x12 bytes C# mscorlib.dll!System.Threading.Tasks.TaskContinuation.InlineIfPossibleOrElseQueue(System.Threading.Tasks.Task task, bool needsProtection) Line 259 + 0xe bytes C# mscorlib.dll!System.Threading.Tasks.StandardTaskContinuation.Run(System.Threading.Tasks.Task completedTask, bool bCanInlineContinuationTask) Line 334 + 0xc bytes C# mscorlib.dll!System.Threading.Tasks.Task.ContinueWithCore(System.Threading.Tasks.Task continuationTask, System.Threading.Tasks.TaskScheduler scheduler, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskContinuationOptions options) Line 4626 + 0x12 bytes C# mscorlib.dll!System.Threading.Tasks.Task.ContinueWith(System.Action<System.Threading.Tasks.Task> continuationAction, System.Threading.Tasks.TaskScheduler scheduler, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskContinuationOptions continuationOptions, ref System.Threading.StackCrawlMark stackMark) Line 3840 C# mscorlib.dll!System.Threading.Tasks.Task.ContinueWith(System.Action<System.Threading.Tasks.Task> continuationAction, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskContinuationOptions continuationOptions, System.Threading.Tasks.TaskScheduler scheduler) Line 3805 + 0x1b bytes C# System.Net.Http.dll!System.Net.Http.HttpUtilities.ContinueWithStandard(System.Threading.Tasks.Task task, System.Action<System.Threading.Tasks.Task> continuation) + 0x2c bytes System.Net.Http.dll!System.Net.Http.HttpClientHandler.PrepareAndStartContentUpload(System.Net.Http.HttpClientHandler.RequestState state) + 0x16b bytes System.Net.Http.dll!System.Net.Http.HttpClientHandler.StartRequest(object obj) + 0x5a bytes mscorlib.dll!System.Threading.Tasks.Task.InnerInvoke() Line 2835 + 0xd bytes C# mscorlib.dll!System.Threading.Tasks.Task.Execute() Line 2459 + 0xb bytes C# mscorlib.dll!System.Threading.Tasks.Task.ExecutionContextCallback(object obj) Line 2815 + 0x9 bytes C# mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 581 + 0xd bytes C# mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 530 + 0xd bytes C# mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot) Line 2785 C# mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) Line 2728 C# mscorlib.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() Line 2664 + 0x7 bytes C# mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch() Line 829 C# mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() Line 1170 + 0x5 bytes C# [Native to Managed Transition] 方法,它将HandleAsyncException设置为最终状态。