为什么我的异步与 ContinueWith 死锁?

Why did my async with ContinueWith deadlock?

我不是在这里寻求解决方案,更多的是对发生了什么的解释。我重构了这段代码以防止这个问题,但我很好奇为什么这个电话陷入僵局。基本上我有一个头对象列表,我需要从数据库存储库对象(使用 Dapper)加载每个细节。我尝试使用 ContinueWith 执行此操作,但失败了:

List<headObj> heads = await _repo.GetHeadObjects();
var detailTasks = heads.Select(s => _changeLogRepo.GetDetails(s.Id)
    .ContinueWith(c => new ChangeLogViewModel() {
         Head = s,
         Details = c.Result
 }, TaskContinuationOptions.OnlyOnRanToCompletion));

await Task.WhenAll(detailTasks);

//deadlock here
return detailTasks.Select(s => s.Result);

有人可以解释是什么导致了这个僵局吗? 我试图弄清楚这里发生了什么,但我不确定。我假设这与在 ContinueWith

中调用 .Result 有关

附加信息

该问题实际上是上述问题的组合。创建了一个任务枚举,每次迭代枚举时,都会有一个新的 GetDetails 调用。对此 Select 的 ToList 调用将修复死锁。在不巩固可枚举的结果(将它们放在列表中)的情况下,WhenAll 调用评估可枚举并异步等待结果任务而不会出现问题,但是当 returned Select 语句评估,它正在迭代并同步等待由尚未完成的新 GetDetailsContinueWith 调用产生的任务结果。所有这些同步等待都可能在尝试序列化响应时发生。

至于为什么同步等待会导致死锁,谜团在于 await 是如何做的。这完全取决于你打电话的内容。 await 实际上只是通过任何范围可见的限定 GetAwaiter 方法检索等待者,并注册一个回调,当工作完成时立即调用等待者的 GetResult 。限定 GetAwaiter 方法可以是 return 具有 IsCompleted 属性 的对象的实例或扩展方法,无参数 GetResult 方法(任何 return 类型,包括 void - await 的结果),以及 INotifyCompletionICriticalNotifyCompletion 接口。这两个接口都有 OnComplete 方法来注册回调。这里有一个令人难以置信的 ContinueWith 和 await 调用链,其中大部分取决于运行时环境。从 Task<T> 获得的 await 的默认行为是使用 SynchronizationContext.Current(我认为是通过 TaskScheduler.Current)调用回调,或者,如果它为 null 则使用线程池(我认为通过 TaskScheduler.Default) 调用回调。包含 await 的方法被某些 CompilerServices class(忘记名字)包装为任务,为方法的调用者提供上述行为包装您正在等待的任何实现。

A SynchronizationContext 也可以对此进行自定义,但通常每个上下文都在其自己的单线程上调用。如果在 Task 上调用 awaitSynchronizationContext.Current 上存在这样的实现,并且您同步等待 Result(它本身取决于对等待的调用线程),你会遇到死锁。

另一方面,如果您将原样方法分解到另一个线程,或对任何任务调用 ConfigureAwait,或隐藏当前调用的 ContinueWith 调度程序,或者自己设置SynchronizationContext.Current(不推荐),你把上面的都改了。