我有一个展示奇怪行为的异步代码的最小示例。这是沙盒代码,更倾向于更好地理解异步——
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
完成,但出现异常。
当TaskA
和TaskB
返回的两个任务完成时,tasksTask
将完成。
比赛源于这样一个事实,即在执行这条线时:
await tasksTask;
任务CCD_ 23可能已经完成或者可能还没有完成。如果从TaskA
和TaskB
返回的Tasks已经完成,那么tasksTask
就已经完成了,所以这是一场与主线程向await tasksTask
行的进度以及ThreadPool运行这两个throw
语句的速度的竞赛。
如果tasksTask
完成,则await
同步发生(它足够智能,可以检查正在等待的Task
是否已经完成(,并且不存在死锁的机会。如果tasksTask
还没有完成,那么我的猜测是您遇到了这里描述的死锁。
这也与您的观察结果一致,即删除对Task.Run
的调用可以消除死锁。在这种情况下,从TaskA
和TaskB
返回的任务也同步完成,因此不存在竞争。
正如经常重复的那样,故事的主旨是不要混合异步和同步代码。不要在受await
影响的Task
上调用.Wait()
或.Result
。还要考虑.ConfigureAwait(false)
的战术使用,以防止其他人在您执行的任务中调用.Wait()
。