所有任务时.发生异常时运行任务挂起



我有一个展示奇怪行为的异步代码的最小示例。这是沙盒代码,更倾向于更好地理解异步——

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

此代码将间歇性挂起。我想更好地理解为什么。我目前的猜测是,间歇性是由于聚合任务的无序执行,和/或异步编程中的这句话:

LINQ中的Lambda表达式使用延迟执行,这意味着代码最终可能会在您意想不到的时候执行。

TaskA属于这一类。

如果TaskB也是Task.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.FormsOnAppearing生命周期方法中运行出来的变体。我不得不稍微修改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);
}
}

这可能与我遇到的另一个问题有关——我使用的是Xamarin.Forms的旧版本,该版本的OnAppearing无法正确处理异步。我将尝试使用一个新版本,看看它是否能解决这个问题。

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

死锁就是这种情况,原因是您在安装了SynchronizationContext的线程上运行await,然后通过对.Wait()的调用阻塞调用堆栈更高级别的同一线程。

当您运行TaskA()TaskB()时,这两个方法都会将一些工作发布到ThreadPool,这需要可变的时间。当ThreadPool能够实际执行throw语句时,这将导致从TaskA/TaskB返回的Task完成,但出现异常。

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

比赛源于这样一个事实,即在执行这条线时:

await tasksTask;

任务CCD_ 23可能已经完成或者可能还没有完成。如果从TaskATaskB返回的Tasks已经完成,那么tasksTask就已经完成了,所以这是一场与主线程向await tasksTask行的进度以及ThreadPool运行这两个throw语句的速度的竞赛。

如果tasksTask完成,则await同步发生(它足够智能,可以检查正在等待的Task是否已经完成(,并且不存在死锁的机会。如果tasksTask还没有完成,那么我的猜测是您遇到了这里描述的死锁。

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


正如经常重复的那样,故事的主旨是不要混合异步和同步代码。不要在受await影响的Task上调用.Wait().Result。还要考虑.ConfigureAwait(false)的战术使用,以防止其他人在您执行的任务中调用.Wait()

最新更新