在已取消的CancellationToken传递导致的HttpClient挂

nic*_*k_w 8 c# task-parallel-library cancellationtokensource dotnet-httpclient

我想用一个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.
}
Run Code Online (Sandbox Code Playgroud)

请注意,我传递一个已经取消了CancellationTokenSource-我有同样的问题,如果我取消使用令牌Task.Delay有短暂的延迟.

我知道我可以简单地检查是否该令牌之前已经电话取消,但即便如此,我也有同样的问题,如果令牌被短暂延迟后取消,也就是说,它不是在方法调用之前取消,但因此后不久变得它.

所以我的问题是,是什么原因造成这一点,我可以做些什么来解决什么/解决这个问题?

编辑

对于那些寻找一个变通方法,通过@Darrel米勒的回答启发,我想出了下面的扩展方法:

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);
}
Run Code Online (Sandbox Code Playgroud)

Dar*_*ler 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.
}
Run Code Online (Sandbox Code Playgroud)

content.LoadIntoBufferAsync在PostAsync之前调用强制反序列化并且似乎避免了死锁.


Mat*_*ith 6

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

问题是使用了内部a TaskCompletionSource,但是当由于在这种特定情况下取消而抛出异常时,它没有被捕获,并且TaskCompletionSource永远不会被设置为一个已完成的状态(因此,等待它TaskCompletionSourceTask意志永远不会回来

使用ILSpy,看着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;
}
Run Code Online (Sandbox Code Playgroud)

稍后,通过该行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);
    }
}
Run Code Online (Sandbox Code Playgroud)

您会注意到调用中的委托在委托中ContinueWithStandard没有异常处理,并且没有人保留返回的任务(因此当此任务抛出异常时,它将被忽略).调用this.StartGettingRequestStream(state);确实抛出异常:

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: 
Run Code Online (Sandbox Code Playgroud)

这是异常时的完整callstack:

>   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]  
Run Code Online (Sandbox Code Playgroud)

我认为目的是不要忽略它,并且在异常的情况下调用将HandleAsyncException其设置TaskCompletionSource为最终状态的方法.