我不是在这里解决问题,更多是对正在发生的事情的解释。我已经重构了这段代码来防止这个问题,但我很好奇为什么这个调用会死锁。基本上我有一个头对象列表,我需要从DB存储库对象(使用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
有关
async
上下文中调用的webapi应用程序回购电话一直都是这样的:
public async Task<IEnumerable<ItemChangeLog>> GetDetails(int headId)
{
using(SqlConnection connection = new SqlConnection(_connectionString))
{
return await connection.QueryAsync<ItemChangeLog>(@"SELECT [Id]
,[Description]
,[HeadId]
FROM [dbo].[ItemChangeLog]
WHERE HeadId = @headId", new { headId });
}
}
我已经用以下代码解决了这个问题:
List<headObj> heads = await _repo.GetHeadObjects();
Dictionary<int, Task<IEnumerable<ItemChangeLog>>> tasks = new Dictionary<int, Task<IEnumerable<ItemChangeLog>>>();
//get details for each head and build the vm
foreach(ItemChangeHead head in heads)
{
tasks.Add(head.Id, _changeLogRepo.GetDetails(head.Id));
}
await Task.WhenAll(tasks.Values);
return heads.Select(s => new ChangeLogViewModel() {
Head = s,
Details = tasks[s.Id].Result
});
问题实际上是上述的组合。每次迭代枚举时都会创建一个任务枚举,这是一个新的GetDetails
调用。此Select上的ToList
调用将修复死锁。如果没有巩固可枚举的结果(将它们放入列表中),则WhenAll
调用会计算可枚举并异步等待生成的任务,但是当返回的Select语句进行求值时,它会迭代并同步等待任务结果来自尚未完成的新GetDetails
和ContinueWith
调用。尝试序列化响应时,可能会发生所有这种同步等待。
至于为什么同步等待导致死锁,神秘之处在于如何等待事情。这完全取决于你的呼唤。 await实际上只是通过任何范围可见的限定GetAwaiter
方法检索GetAwaiter
并注册在工作完成时立即调用awaiter上的GetResult
的回调。合格的GetAwaiter
方法可以是返回具有IsCompleted
属性的对象的实例或扩展方法,无参数GetResult
方法(任何返回类型,包括void - await的结果),以及INotifyCompletion
或ICriticalNotifyCompletion
接口。接口都有OnComplete
方法来注册回调。有一个令人难以置信的ContinueWith
链和等待调用在这里进行,其中大部分取决于运行时环境。从Task<T>
获取的await的默认行为是使用SynchronizationContext.Current
(我认为通过TaskScheduler.Current
)来调用回调,或者如果为null则使用线程池(我认为通过TaskScheduler.Default
)调用回调。包含await的方法被某些CompilerServices
类包装为Task(忘记名称),为方法的调用者提供上述行为包装您正在等待的任何实现。
SynchronizationContext
也可以自定义它,但通常每个上下文都在它自己的单个线程上调用。如果在Task
上调用await
时在SynchronizationContext.Current
上存在这样的实现,并且您同步等待Result
(它本身取决于对等待线程的调用),则会出现死锁。
另一方面,如果你将as-is方法分解为另一个线程,或者在任何任务上调用ConfigureAwait
,或者为ContinueWith
调用隐藏当前调度程序,或者设置自己的SynchronizationContext.Current
(不推荐),改变以上所有。