3

First, apologies -- I'm unable to reproduce this behaviour in a suitably simple sample application. This functionality was working before some refactoring of the calling code.

I'm trying to use a TaskCompletionSource to signal the end of an async operation (either a long-running process completing, or a timeout may signal the completion using TrySetResult() ).

My issue is that even though I can see Task is transitioning from "WaitingForActivation" to "RanToCompletion", the call to await never completes.

As a test, I created a Task Continuation, and this IS being called, and I added a Timer to show the Task states:

async Task<Foo> WaitForResultOrTimeoutAsync()
{
        //... [Create 'pendingReq' with its TaskCompletion property]

        TaskCompletionSource<Foo> myCompletion = pendingReq.TaskCompletion;

        Task<Foo> theTask = myCompletion.Task;

        var taskContinuation = theTask.ContinueWith(resp =>
        {
            Console.WriteLine("The task completed");
            return resp.Result;
        });

        new Timer(state =>
        {
            Console.WriteLine("theTask TaskCompletion state is {0}", theTask.Status);
            Console.WriteLine("taskContinuation TaskCompletion state is {0}", taskContinuation.Status);
        }, null, 0, 1000);

        //var result = await theTask;
        var result = await taskContinuation;
        Console.WriteLine("We're FINISHED");    // NEVER GETS HERE
        return result;
}

This results in the following output:

theTask TaskCompletion state is WaitingForActivation
taskContinuation TaskCompletion state is WaitingForActivation
theTask TaskCompletion state is WaitingForActivation
taskContinuation TaskCompletion state is WaitingForActivation
The task completed
theTask TaskCompletion state is RanToCompletion
taskContinuation TaskCompletion state is RanToCompletion
theTask TaskCompletion state is RanToCompletion
taskContinuation TaskCompletion state is RanToCompletion

Surely with the continuation being hit, directly awaiting the Task should also complete, no? What external (calling) factors could there be for this behaviour?

Ive
  • 457
  • 5
  • 19
  • 3
    Check whether `SynchronizationContext.Current != null` – i3arnon Oct 28 '15 at 10:59
  • Can you please edit your code so that we can run it to replicate your issue? – Enigmativity Oct 28 '15 at 11:23
  • Thanks @i3arnon -- this was indeed null, which pointed me on the right track; – Ive Oct 28 '15 at 13:38
  • 1
    @Ive You mean not `null`, right? – i3arnon Oct 28 '15 at 13:40
  • @i3arnon Before awaiting anything, it was set to WindowsFormsSynchronizationContext. I was referring to within the Continuation, where it was null. I suspect this due to Stephen's note about Continuations using the TaskScheduler...but haven't investigated further. – Ive Oct 28 '15 at 14:03

1 Answers1

3

I'm pretty sure that the calling code at some point further up the callstack is blocking on a task, and that this code is executed within a synchronization context (i.e., on a UI thread or from an ASP.NET request). This will cause a deadlock that I explain in full on my blog. The most correct solution is to replace the blocking (usually a Wait or Result call) with await.

The reason ContinueWith is not blocked is because it uses the current TaskScheduler instead of the current SynchronizationContext, so it probably ends up running on the thread pool in this case. If my guess about the calling code blocking is correct, then ContinueWith would also deadlock if you pass it a TaskScheduler.FromCurrentSynchronizationContext().

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Yup, you're quite right. Further up the call stack, I was working around calling some async code from synchronous, and had referenced a Task.Result to do so (I know -- naughty). That combined with using a WinForms app's Page_Load event to test this component caused the deadlock as described in your blog. I've subsequently used async all the way back to the original caller and the issue is resolved. Setting ConfigureAwait(false) on the deadlocked await also did the trick. – Ive Oct 28 '15 at 13:31