我有一些展示奇怪行为的异步代码的最小示例。这是沙盒代码,更多地用于尝试更好地理解异步-
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
属于这一类别。
如果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.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
不能正确处理异步问题。我将尝试使用较新的版本,以查看是否可以解决问题。
答案 0 :(得分:0)
我猜想您正在GUI应用程序(或类似应用程序)中调用ExhibitStrangeBehaviorAsync().Wait()
,因为这是我认为可能导致死锁的唯一情况。这个答案就是基于这种假设而写的。
死锁为this one,这是由于您在安装了await
的线程上运行SynchronizationContext
,然后在更高的位置阻塞了该线程而导致的调用.Wait()
的调用堆栈。
当您运行TaskA()
和TaskB()
时,这两个方法都会将一些工作发布到ThreadPool,这需要花费可变的时间。当ThreadPool实际执行throw
语句时,这会使从Task
/ TaskA
返回的TaskB
发生异常。
tasksTask
将在TaskA
和TaskB
返回的两个任务完成时完成。
比赛源于这一行被执行的事实:
await tasksTask;
任务tasksTask
可能已经完成或可能尚未完成。如果tasksTask
和TaskA
返回的任务已经完成,TaskB
将已经完成,因此这与主线程向await tasksTask
行的进展,线程池的运行速度进行了竞争可以同时运行这两个throw
语句。
如果tasksTask
完成,则await
同步发生(足够聪明地检查等待的Task
是否已经完成),并且没有死锁的机会。如果tasksTask
尚未完成,那么我的猜测是您遇到了here所述的僵局。
这也与您观察到的删除Task.Run
的调用会删除死锁的观点一致。在这种情况下,从TaskA
和TaskB
返回的任务也会同步完成,因此不会出现竞争。
这个故事的士气经常重复出现,不要混用异步和同步代码。请勿在{{1}}会以任何方式影响的.Wait()
上呼叫.Result
或Task
。还应考虑在战术上使用await
来防止其他人在您完成的任务上打电话给.ConfigureAwait(false)
。