为什么我的异步与 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
有关
附加信息
- 这是一个在
async
上下文中调用的 webapi 应用程序
repo 调用都是这样的:
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
调用评估可枚举并异步等待结果任务而不会出现问题,但是当 returned Select 语句评估,它正在迭代并同步等待由尚未完成的新 GetDetails
和 ContinueWith
调用产生的任务结果。所有这些同步等待都可能在尝试序列化响应时发生。
至于为什么同步等待会导致死锁,谜团在于 await 是如何做的。这完全取决于你打电话的内容。 await 实际上只是通过任何范围可见的限定 GetAwaiter
方法检索等待者,并注册一个回调,当工作完成时立即调用等待者的 GetResult
。限定 GetAwaiter
方法可以是 return 具有 IsCompleted
属性 的对象的实例或扩展方法,无参数 GetResult
方法(任何 return 类型,包括 void - await 的结果),以及 INotifyCompletion
或 ICriticalNotifyCompletion
接口。这两个接口都有 OnComplete
方法来注册回调。这里有一个令人难以置信的 ContinueWith
和 await 调用链,其中大部分取决于运行时环境。从 Task<T>
获得的 await 的默认行为是使用 SynchronizationContext.Current
(我认为是通过 TaskScheduler.Current
)调用回调,或者,如果它为 null 则使用线程池(我认为通过 TaskScheduler.Default
) 调用回调。包含 await 的方法被某些 CompilerServices
class(忘记名字)包装为任务,为方法的调用者提供上述行为包装您正在等待的任何实现。
A SynchronizationContext
也可以对此进行自定义,但通常每个上下文都在其自己的单线程上调用。如果在 Task
上调用 await
时 SynchronizationContext.Current
上存在这样的实现,并且您同步等待 Result
(它本身取决于对等待的调用线程),你会遇到死锁。
另一方面,如果您将原样方法分解到另一个线程,或对任何任务调用 ConfigureAwait
,或隐藏当前调用的 ContinueWith
调度程序,或者自己设置SynchronizationContext.Current
(不推荐),你把上面的都改了。
我不是在这里寻求解决方案,更多的是对发生了什么的解释。我重构了这段代码以防止这个问题,但我很好奇为什么这个电话陷入僵局。基本上我有一个头对象列表,我需要从数据库存储库对象(使用 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 应用程序 repo 调用都是这样的:
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
调用评估可枚举并异步等待结果任务而不会出现问题,但是当 returned Select 语句评估,它正在迭代并同步等待由尚未完成的新 GetDetails
和 ContinueWith
调用产生的任务结果。所有这些同步等待都可能在尝试序列化响应时发生。
至于为什么同步等待会导致死锁,谜团在于 await 是如何做的。这完全取决于你打电话的内容。 await 实际上只是通过任何范围可见的限定 GetAwaiter
方法检索等待者,并注册一个回调,当工作完成时立即调用等待者的 GetResult
。限定 GetAwaiter
方法可以是 return 具有 IsCompleted
属性 的对象的实例或扩展方法,无参数 GetResult
方法(任何 return 类型,包括 void - await 的结果),以及 INotifyCompletion
或 ICriticalNotifyCompletion
接口。这两个接口都有 OnComplete
方法来注册回调。这里有一个令人难以置信的 ContinueWith
和 await 调用链,其中大部分取决于运行时环境。从 Task<T>
获得的 await 的默认行为是使用 SynchronizationContext.Current
(我认为是通过 TaskScheduler.Current
)调用回调,或者,如果它为 null 则使用线程池(我认为通过 TaskScheduler.Default
) 调用回调。包含 await 的方法被某些 CompilerServices
class(忘记名字)包装为任务,为方法的调用者提供上述行为包装您正在等待的任何实现。
A SynchronizationContext
也可以对此进行自定义,但通常每个上下文都在其自己的单线程上调用。如果在 Task
上调用 await
时 SynchronizationContext.Current
上存在这样的实现,并且您同步等待 Result
(它本身取决于对等待的调用线程),你会遇到死锁。
另一方面,如果您将原样方法分解到另一个线程,或对任何任务调用 ConfigureAwait
,或隐藏当前调用的 ContinueWith
调度程序,或者自己设置SynchronizationContext.Current
(不推荐),你把上面的都改了。