Lia*_*iam 7 c# deadlock async-await dapper asp.net-web-api2
我不是在这里解决问题,更多是对正在发生的事情的解释.我已经重构了这段代码来防止这个问题,但我很好奇为什么这个调用会死锁.基本上我有一个头对象列表,我需要从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);
Run Code Online (Sandbox Code Playgroud)
有人可以解释导致这种僵局的原因吗?我试图了解这里发生的事情,但我不确定.我认为这与打电话.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 });
}
}
Run Code Online (Sandbox Code Playgroud)我已经用以下代码解决了这个问题:
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
});
Run Code Online (Sandbox Code Playgroud)小智 4
这个问题实际上是以上问题的综合。创建任务的枚举,每次迭代该枚举时,都会进行一次新的GetDetails调用。对此 Select 的调用ToList将修复死锁。在不固化可枚举的结果(将它们放入列表中)的情况下,调用WhenAll会评估可枚举并异步等待结果任务,不会出现任何问题,但是当返回的 Select 语句评估时,它会迭代并同步等待由新鲜的GetDetails和ContinueWith尚未完成的呼叫。所有这些同步等待都可能在尝试序列化响应时发生。
至于为什么同步等待会导致死锁,谜团在于await 是如何做事的。这完全取决于你打电话的内容。等待实际上只是通过任何范围可见的限定GetAwaiter方法检索等待者并注册回调,该GetResult回调在工作完成时立即调用等待者。限定GetAwaiter方法可以是返回具有属性的对象的实例或扩展方法IsCompleted、无参数GetResult方法(任何返回类型,包括 void - wait 的结果)以及INotifyCompletion接口ICriticalNotifyCompletion。这两个接口都有OnComplete注册回调的方法。这里有一个令人难以置信的调用链ContinueWith和等待调用,其中大部分取决于运行时环境。从 a 获得的等待的默认行为Task<T>是使用SynchronizationContext.Current(我认为是 via TaskScheduler.Current)来调用回调,或者如果为 null 则使用线程池(我认为是 via TaskScheduler.Default)来调用回调。包含等待的方法被某个CompilerServices类包装为任务(忘记了名称),为该方法的调用者提供上述行为,包装您正在等待的任何实现。
ASynchronizationContext也可以自定义它,但通常每个上下文都在它自己的单个线程上调用。如果在a上调用SynchronizationContext.Currentwhen 时存在这样的实现,并且您同步等待(这本身取决于对等待线程的调用),则会出现死锁。awaitTaskResult
另一方面,如果您将原样方法分解到另一个线程,或者调用ConfigureAwait任何任务,或者隐藏调用的当前调度程序ContinueWith,或者设置您自己的SynchronizationContext.Current(不推荐),则您可以更改上述所有内容。
| 归档时间: |
|
| 查看次数: |
279 次 |
| 最近记录: |