我这里没有解决方案,更多的是对发生的情况的解释。我已经重构了此代码以防止出现此问题,但我对为什么此调用死锁感到好奇。基本上,我有一个头对象列表,我需要从数据库存储库对象(使用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);
有人可以解释造成这种僵局的原因吗?我试图弄清这里发生的事情,但我不确定。我想这与在.Result
ContinueWith
有关
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
});
答案 0 :(得分:3)
问题实际上是上述情况的组合。创建了一个任务枚举,每次迭代该枚举时,都会进行一次新的GetDetails
调用。对此Select进行ToList
调用将解决死锁。在不巩固可枚举结果(将它们放入列表中)的情况下,WhenAll
调用将评估可枚举并异步地等待结果任务,而不会出现问题,但是当返回的Select语句求值时,它将迭代并同步等待由尚未完成的新GetDetails
和ContinueWith
调用产生的任务结果。尝试序列化响应时,很可能会发生所有这些同步等待。
关于为什么同步等待导致死锁的原因,在于等待如何进行。这完全取决于您要拨打的电话。等待实际上就是通过任何作用域可见的限定GetAwaiter
方法检索等待者,并在工作完成时注册立即在等待者上调用GetResult
的回调。合格的GetAwaiter
方法可以是实例或扩展方法,该方法可返回具有IsCompleted
属性的对象,无参数的GetResult
方法(任何返回类型,包括void-等待结果),以及INotifyCompletion
或ICriticalNotifyCompletion
接口。这两个接口都具有OnComplete
方法来注册回调。这里有ContinueWith
令人难以置信的链条,并且在这里等待调用,而这很大程度上取决于运行时环境。从Task<T>
获得等待的默认行为是使用SynchronizationContext.Current
(我想通过TaskScheduler.Current
)来调用回调,或者如果使用null则使用线程池(我认为通过TaskScheduler.Default
)来调用回调。包含{a {1}}类的方法将包含await的方法包装为Task(忘记了名称),从而为方法的调用者提供上述行为,以包装您正在等待的任何实现。
CompilerServices
也可以自定义,但是通常每个上下文都在其自己的单个线程上调用。如果在SynchronizationContext
上调用SynchronizationContext.Current
时在await
上存在这样的实现,并且您同步等待Task
(它本身取决于等待中的调用)线程),则出现死锁。
另一方面,如果您将原样的方法分解为另一个线程,或者在任何任务上调用Result
,或者为您的ConfigureAwait
调用隐藏了当前调度程序,或者进行了设置您自己的ContinueWith
(不建议使用),请更改以上所有内容。