【问题标题】:WhenAll Task.Run Tasks Hang when Exceptions Occur发生异常时,WhenAll Task.Run 任务挂起
【发布时间】:2020-11-05 04:49:42
【问题描述】:

我有一个显示奇怪行为的异步代码的最小示例。这是沙盒代码,更适合尝试更好地理解异步 --

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.Runs 是异步 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 正确处理异步存在问题。我将尝试使用更新的版本,看看它是否能解决问题。

【问题讨论】:

  • ExhibitStrangeBehaviorAsync 的运行情况如何?这是控制台还是 WPF/WinForms 应用程序?
  • 在控制台中对我来说很好。这段代码用在哪里?
  • 那里没有应该挂起的东西,我也无法在控制台应用程序中重现它。我猜你正在使用.Wait() 更进一步,这是一个 GUI 应用程序?但是一个 complete 复制器会很好,所以我们不是在猜测。如果你打电话给ExhibitStrangeBehaviorAsync().Wait(),有一场比赛可能会导致死锁

标签: c# xamarin asynchronous exception xamarin.forms


【解决方案1】:

我猜你是在 GUI 应用程序(或类似应用程序)中调用 ExhibitStrangeBehaviorAsync().Wait(),因为这是我认为可能导致死锁的唯一情况。这个答案是根据这个假设写的。

死锁是this one,原因是您在安装了SynchronizationContext 的线程上运行await,然后通过调用.Wait().

当您运行TaskA()TaskB() 时,这两种方法都会向线程池发布一些工作,这需要不同的时间。当 ThreadPool 开始实际执行 throw 语句时,这会导致从 TaskA / TaskB 返回的 Task 以异常完成。

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

比赛来自于这样一个事实,在这一行被执行时:

await tasksTask;

任务tasksTask 可能已完成,也可能未完成。如果从TaskATaskB 返回的任务已完成,tasksTask 将完成,因此这是与主线程向await tasksTask 行进展的竞赛,与线程池可以运行这两个throw 的速度竞争声明。

如果tasksTask 完成,await 同步发生(它足够聪明地检查正在等待的Task 是否已经完成),并且没有死锁的机会。如果tasksTask 还没有完成,那么我猜你遇到了here 描述的死锁。

这也与您的观察一致,即删除对Task.Run 的调用会消除死锁。在这种情况下,TaskATaskB 返回的任务也是同步完成的,所以不存在竞争。


故事的精神是,正如经常重复的那样,不要混合异步和同步代码。不要在受await 以任何方式影响的Task 上调用.Wait().Result。还可以考虑在战术上使用.ConfigureAwait(false) 来防止其他人在您生成的任务上调用.Wait()

【讨论】:

  • 感谢您的详细撰写。我同意某处存在竞争条件,并且进一步的测试证实了您的诊断。在 async TaskA 和 async TaskB 中长时间调用 Task.Delay 会导致死锁,而在这两个函数 async 之前没有挂起。但是,我没有(直接地、明显地)使用.Wait。我知道WhenAll 没有阻塞,但是Waits 内部有机会吗?我会觉得这很难相信。
  • 我只是重复了Task.Delay测试,第二次没有死锁。
  • 这就是上面的意图。但它在我的应用程序框架的内部。试图将一些直接来自OnAppearing 的东西放在一起,但我还没有走这么远。我会继续努力的。
  • 是的,我们可以运行它来重现问题。如果它只在应用程序的某些部分存在时重现,那么它们与了解正在发生的事情有关
  • 我发布了一个更简单的版本,它仍然会间歇性地死锁。但是,这似乎只是此版本的 Xamarin.Forms (4.2.0) 的问题。无论如何,我似乎无法在较新的版本上重现它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-10-05
  • 1970-01-01
  • 2016-06-21
  • 1970-01-01
  • 2014-09-27
  • 2017-03-14
  • 2021-05-04
相关资源
最近更新 更多