异步任务方法WaitingForActivation

Mar*_*und 5 c# asynchronous task task-parallel-library async-await

我发现了一种非常混乱的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;
    });
}
Run Code Online (Sandbox Code Playgroud)

正如预期的那样,在它发布之后,它首先打印出一堆0,然后是一些,两个等等.

相反,请考虑以下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());
    }
}
Run Code Online (Sandbox Code Playgroud)

虽然这两者在一个小细节上有所不同,但UWP片段只是保持打印0并且if语句中的任务状态保持不变Waitingforactivation.此外,该问题可被固定通过去除asyncawaitCalculateNextAsync:

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

现在一切都与Console应用程序中的相同.

有人可以解释控制台中的行为与UWP应用程序不同的原因吗?为什么任务保持c在UWP应用程序的情况下?

更新

我有回到这个问题了,但发现了一个问题,最初接受的答案不包括-在UWP代码永远达不到.Result,它只是不断检查IsCompleted返回false,因此_lastResult被返回.是什么让它成为一个应该完成TaskAwaitingActivation状态?

我发现原因是主动等待while循环阻止await继续再次占用UI线程,从而导致类似"死锁"的情况.

Nko*_*osi 7

基于UWP应用程序中的代码,无需持有_calculateTask.只是await任务.

这是更新的代码

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

原始答案

您正在混合async/await和阻塞调用,就像.Result在UWP应用程序中一样,由于它的一个块SynchronizationContext导致死锁.控制台应用程序是该规则的一个例外,这就是它在那里工作而不是在UWP应用程序中工作的原因.

此死锁的根本原因是await处理上下文的方式.默认情况下,当等待未完成的任务时,捕获当前的"上下文"并用于在任务完成时恢复该方法.这个"上下文"是当前的SynchronizationContext,除非它是null,在这种情况下它是当前的TaskScheduler.GUI和ASP.NET应用程序具有SynchronizationContext,一次只允许运行一个代码块.当await完成时,它会尝试在捕获的上下文中执行异步方法的剩余部分.但是该上下文已经有一个线程,它(同步)等待异步方法完成.他们每个人都在等着对方,造成僵局.

请注意,控制台应用程序不会导致此死锁.它们有一个线程池SynchronizationContext而不是一次一个块的SynchronizationContext,因此当await完成时,它会在线程池线程上调度async方法的其余部分.该方法能够完成,完成其返回的任务,并且没有死锁.当程序员编写测试控制台程序,观察部分异步代码按预期工作,然后将相同的代码移动到GUI或ASP.NET应用程序中时,这种行为差异可能会令人困惑.

参考Async/Await - 异步编程的最佳实践