异步任务方法等待激活

5

我发现了async方法非常令人困惑的行为。请看以下控制台应用程序:

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;
    });
}

如预期的那样,在启动后,它首先会打印出一堆0,然后是1、2等。

相比之下,考虑以下UWP应用程序片段:

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());
    }
}

尽管这两者只有一个小细节的不同,但UWP片段仅打印出0,且任务状态在if语句中仍保持为Waitingforactivation。此外,可以通过从CalculateNextAsync中移除asyncawait来解决问题。
private static Task<int> CalculateNextAsync()
{
    return Task.Run(async () =>
    {
        await Task.Delay(2000);
        return ++_i;
    });
}

现在一切的工作方式与控制台应用程序相同。
有人能解释一下为什么控制台和UWP应用程序的行为不同吗?为什么在UWP应用程序中任务保持为“c”?
更新
我又回到了这个问题,但发现了一个原先被接受的答案没有涵盖的问题 - UWP上的代码从来没有达到“.Result”,它只是不断检查“IsCompleted”,返回“false”,因此返回“_lastResult”。什么导致了“Task”在应该已经完成时具有“AwaitingActivation”状态?
解决方案
我发现问题的原因是活动等待“while”循环阻止了“await”继续占用UI线程,因此导致了类似于“死锁”的情况。
1个回答

8
基于 UWP 应用程序中的代码,不需要保持 _calculateTask。只需等待任务即可。
以下是更新后的代码:
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());
    }
}

原始回答

您在UWP应用程序中混合使用异步/等待和阻塞调用,例如.Result,这会导致死锁,因为它具有一个块同步上下文。控制台应用程序是这个规则的例外,这就是为什么它在控制台应用程序中可以工作而在UWP应用程序中不能工作的原因。

这个僵局的根本原因在于await处理上下文的方式。默认情况下,当等待一个未完成的任务时,当前的“上下文”会被捕获并用于在任务完成后恢复方法。这个“上下文”是当前的同步上下文,除非它为null,在这种情况下,它是当前的任务调度程序。GUI和ASP.NET应用程序有一个同步上下文,只允许一次运行一个代码块。当await完成时,它尝试在捕获的上下文中执行异步方法的剩余部分。但是该上下文已经有一个线程在其中,该线程(同步地)等待异步方法完成。它们互相等待,导致死锁。
需要注意的是,控制台应用程序不会引起这种死锁。它们使用线程池同步上下文而不是一次运行一个代码块的同步上下文,因此当await完成时,它会在线程池线程上安排异步方法的剩余部分。该方法能够完成,完成其返回的任务,并且没有死锁。这种行为上的差异可能会使程序员感到困惑,当他们编写一个测试控制台程序时,观察部分异步代码按预期工作,然后将相同的代码移动到GUI或ASP.NET应用程序中时,它会出现死锁。
参考:

异步/等待 - 异步编程中的最佳实践


太棒了,非常感谢您清晰的解释! - Martin Zikmund
我又回到了这个问题,但发现一个问题——在UWP上的代码永远不会到达.Result,它只是不断地检查IsCompleted。为什么会这样? - Martin Zikmund
哦,现在我理解了,原因是while循环永远不会允许继续占用UI线程,这使其“基本上”无法继续执行。 - Martin Zikmund

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接