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?
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?
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.
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());
}
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With