为什么我与ContinueWith死锁异步?

时间:2018-10-23 11:57:03

标签: c# async-await asp.net-web-api2 deadlock dapper

我这里没有解决方案,更多的是对发生的情况的解释。我已经重构了此代码以防止出现此问题,但我对为什么此调用死锁感到好奇。基本上,我有一个头对象列表,我需要从数据库存储库对象(使用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
        });
    

1 个答案:

答案 0 :(得分:3)

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

关于为什么同步等待导致死锁的原因,在于等待如何进行。这完全取决于您要拨打的电话。等待实际上就是通过任何作用域可见的限定GetAwaiter方法检索等待者,并在工作完成时注册立即在等待者上调用GetResult的回调。合格的GetAwaiter方法可以是实例或扩展方法,该方法可返回具有IsCompleted属性的对象,无参数的GetResult方法(任何返回类型,包括void-等待结果),以及INotifyCompletionICriticalNotifyCompletion接口。这两个接口都具有OnComplete方法来注册回调。这里有ContinueWith令人难以置信的链条,并且在这里等待调用,而这很大程度上取决于运行时环境。从Task<T>获得等待的默认行为是使用SynchronizationContext.Current(我想通过TaskScheduler.Current)来调用回调,或者如果使用null则使用线程池(我认为通过TaskScheduler.Default)来调用回调。包含{a {1}}类的方法将包含await的方法包装为Task(忘记了名称),从而为方法的调用者提供上述行为,以包装您正在等待的任何实现。

CompilerServices也可以自定义,但是通常每个上下文都在其自己的单个线程上调用。如果在SynchronizationContext上调用SynchronizationContext.Current时在await上存在这样的实现,并且您同步等待Task(它本身取决于等待中的调用)线程),则出现死锁。

另一方面,如果您将原样的方法分解为另一个线程,或者在任何任务上调用Result,或者为您的ConfigureAwait调用隐藏了当前调度程序,或者进行了设置您自己的ContinueWith(不建议使用),请更改以上所有内容。