发生异常时,WhenAll Task.Run任务会挂起

时间:2020-07-15 16:05:33

标签: c# xamarin asynchronous exception xamarin.forms

我有一些展示奇怪行为的异步代码的最小示例。这是沙盒代码,更多地用于尝试更好地理解异步-

private async Task ExhibitStrangeBehaviorAsync()
{
    async Task TaskA()
    {
        await Task.Run(async () =>
        {
            throw new Exception(nameof(TaskA));
            await Task.Yield();
        });
    }

    async Task TaskB()
    {
        await Task.Run(() =>
        {
            throw new Exception(nameof(TaskB));
        });
    }

    var tasks = new List<Task>
    {
        TaskA(),
        TaskB(),
    };
    var tasksTask = Task.WhenAll(tasks);

    try
    {
        await tasksTask;
    }
    catch
    {
        Debug.WriteLine(tasksTask.Exception.Message);
    }
}

此代码会间歇性地挂起。我想更好地理解原因。我目前的猜测是间歇性的,这是由于聚集任务的执行顺序混乱和/或来自Asynchronous Programming的这一行:

LINQ中的Lambda表达式使用延迟执行,这意味着代码可能在您不期望的时候结束执行。

TaskA属于这一类别。

如果TaskBTask.Run也是一个异步lambda,或者本地Task的两个函数都不包含Task.Run,例如,

    async Task TaskA()
    {
        //await Task.Run(async () =>
        //{
             throw new Exception(nameof(TaskA));
             await Task.Yield();
        //});
    }

    async Task TaskB()
    {
        //await Task.Run(() =>
        //{
            throw new Exception(nameof(TaskB));
        //});
    }

任何人都可以对这里发生的事情有所了解吗?

编辑

这是在UI线程(特别是Xamarin.Forms应用程序)的上下文中执行的。

编辑2

这是Xamarin.Forms OnAppearing生命周期方法的另一种变体。我不得不稍微修改TaskA / B,尽管它们也与上面的原始设置一样打破了这种方式。

protected async override void OnAppearing()
{
    base.OnAppearing();

    async Task TaskA()
    {
        await Task.Run(async () =>
        {
            throw new InvalidOperationException();
            await Task.Delay(1).ConfigureAwait(false);
        }).ConfigureAwait(false);
    }

    async Task TaskB()
    {
        await Task.Run(() => throw new ArgumentException()).ConfigureAwait(false);
    }

    var tasks = new List<Task>
    {
        TaskA(),
        TaskB(),
    };
    var tasksTask = Task.WhenAll(tasks);

    try
    {
        await tasksTask;
    }
    catch
    {
        Debug.WriteLine(tasksTask.Exception.Message);
    }
}

这可能与another issue I have had有关-我使用的是Xamarin.Forms的旧版本,该版本的OnAppearing不能正确处理异步问题。我将尝试使用较新的版本,以查看是否可以解决问题。

1 个答案:

答案 0 :(得分:0)

我猜想您正在GUI应用程序(或类似应用程序)中调用ExhibitStrangeBehaviorAsync().Wait(),因为这是我认为可能导致死锁的唯一情况。这个答案就是基于这种假设而写的。

死锁为this one,这是由于您在安装了await的线程上运行SynchronizationContext,然后在更高的位置阻塞了该线程而导致的调用.Wait()的调用堆栈。

当您运行TaskA()TaskB()时,这两个方法都会将一些工作发布到ThreadPool,这需要花费可变的时间。当ThreadPool实际执行throw语句时,这会使从Task / TaskA返回的TaskB发生异常。

tasksTask将在TaskATaskB返回的两个任务完成时完成。

比赛源于这一行被执行的事实:

await tasksTask;

任务tasksTask可能已经完成或可能尚未完成。如果tasksTaskTaskA返回的任务已经完成,TaskB将已经完成,因此这与主线程向await tasksTask行的进展,线程池的运行速度进行了竞争可以同时运行这两个throw语句。

如果tasksTask完成,则await同步发生(足够聪明地检查等待的Task是否已经完成),并且没有死锁的机会。如果tasksTask尚未完成,那么我的猜测是您遇到了here所述的僵局。

这也与您观察到的删除Task.Run的调用会删除死锁的观点一致。在这种情况下,从TaskATaskB返回的任务也会同步完成,因此不会出现竞争。


这个故事的士气经常重复出现,不要混用异步和同步代码。请勿在{{1​​}}会以任何方式影响的.Wait()上呼叫.ResultTask。还应考虑在战术上使用await来防止其他人在您完成的任务上打电话给.ConfigureAwait(false)

相关问题