Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async Task method WaitingForActivation

I have found a very confusing behavior of async methods. Consider the following Console app:

private static int _i = 0;
private static Task<int> _calculateTask = Task.FromResult(0);
private static int _lastResult = 0;

static void Main(string[] args)
{
    while (true)
    {
        Console.WriteLine(Calculate());
    }
}

private static int Calculate()
{
    if (!_calculateTask.IsCompleted)
    {
        return _lastResult;
    }

    _lastResult = _calculateTask.Result;
    _calculateTask = CalculateNextAsync();
    return _lastResult;
}

private static async Task<int> CalculateNextAsync()
{
    return await Task.Run(() =>
    {
        Thread.Sleep(2000);
        return ++_i;
    });
}

As expected, after it is launched, it first prints out a bunch of 0s, then ones, twos and so on.

In contrast consider the following UWP app snippet:

private static int _i = 0;
private static Task<int> _calculateTask = Task.FromResult(0);
private static int _lastResult = 0;
public int Calculate()
{
    if (!_calculateTask.IsCompleted)
    {
        return _lastResult;
    }

    _lastResult = _calculateTask.Result;
    _calculateTask = CalculateNextAsync();
    return _lastResult;
}

private static async Task<int> CalculateNextAsync()
{
    return await Task.Run( async() =>
    {
        await Task.Delay(2000);
        return ++_i;
    });
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    while( true)
    {
        Debug.WriteLine(Calculate());
    }
}

Although these two differ only in one small detail, the UWP snippet just keeps printing out 0 and task state in the if statement just stays Waitingforactivation. Furthermore the problem can be fixed by removing async and await from CalculateNextAsync:

private static Task<int> CalculateNextAsync()
{
    return Task.Run(async () =>
    {
        await Task.Delay(2000);
        return ++_i;
    });
}

Now everything works the same way as in the Console app.

Can someone explain the reason why the behavior in Console differs from UWP app? And why the task stays as c in case of UWP app?

Update

I have got back to this question again, but found out an issue that the originally accepted answer does not cover - the code on UWP never reaches the .Result, it just keeps checking for IsCompleted which returns false, hence the _lastResult is returned. What makes the Task have a AwaitingActivation state when it should have completed?

Solution

I figured out that the reason is that the active waiting while loop prevents the await continuation from ever seizing the UI thread again, hence causing a "deadlock"-like situation.

like image 264
Martin Zikmund Avatar asked Oct 27 '17 02:10

Martin Zikmund


1 Answers

Based on the code in the UWP app there is no need for holding on to the _calculateTask. Just await the task.

Here is the updated code

private static int _i = 0;
private static int _lastResult = 0;

public async Task<int> Calculate() {    
    _lastResult = await CalculateNextAsync();
    return _lastResult;
}

//No need to wrap the code in a Task.Run. Just await the async code
private static async Task<int> CalculateNextAsync()  
    await Task.Delay(2000);
    return ++_i;
}

//Event Handlers allow for async void
private async void Button_Click(object sender, RoutedEventArgs e) {
    while( true) {
        var result = await Calculate();
        Debug.WriteLine(result.ToString());
    }
}

ORIGINAL ANSWER

You are mixing async/await and blocking calls like .Result in the UWP app which is causing a deadlock because of its one chunk SynchronizationContext. Console applications are an exception to that rule, which is why it works there and not in the UWP app.

The root cause of this deadlock is due to the way await handles contexts. By default, when an incomplete Task is awaited, the current “context” is captured and used to resume the method when the Task completes. This “context” is the current SynchronizationContext unless it’s null, in which case it’s the current TaskScheduler. GUI and ASP.NET applications have a SynchronizationContext that permits only one chunk of code to run at a time. When the await completes, it attempts to execute the remainder of the async method within the captured context. But that context already has a thread in it, which is (synchronously) waiting for the async method to complete. They’re each waiting for the other, causing a deadlock.

Note that console applications don’t cause this deadlock. They have a thread pool SynchronizationContext instead of a one-chunk-at-a-time SynchronizationContext, so when the await completes, it schedules the remainder of the async method on a thread pool thread. The method is able to complete, which completes its returned task, and there’s no deadlock. This difference in behavior can be confusing when programmers write a test console program, observe the partially async code work as expected, and then move the same code into a GUI or ASP.NET application, where it deadlocks.

Reference Async/Await - Best Practices in Asynchronous Programming

like image 187
Nkosi Avatar answered Oct 31 '22 23:10

Nkosi