代码之家  ›  专栏  ›  技术社区  ›  Liam Joshua

为什么我的异步与死锁继续?

  •  7
  • Liam Joshua  · 技术社区  · 7 年前

    我已经重构了这个代码来防止这个问题,但是我很好奇为什么这个调用会死锁。基本上我有一个head对象的列表,我需要从DB repository对象(使用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 继续

    附加信息

    • 这是一个在 async 上下文
    • 回购通知的内容包括:

      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 回复  |  直到 7 年前
        1
  •  4
  •   Liam Joshua    7 年前

    这个问题实际上是上述问题的结合。创建了一个任务枚举,每次迭代枚举时,都会有一个新的 GetDetails 打电话。A ToList WhenAll call对可枚举项求值并异步等待生成的任务,而不会出现问题,但是当返回的Select语句求值时,它将迭代并同步等待fresh语句生成的任务的结果 ContinueWith 尚未完成的呼叫。所有这些同步等待都可能是在尝试序列化响应时发生的。

    至于为什么同步等待会导致死锁,谜团在于等待是如何做事情的。这完全取决于你打什么电话。等待实际上只是通过任何可见范围检索等待者 GetAwaiter 方法和立即调用的回调的注册 GetResult 当工作完成时,在等待者身上。资格赛 IsCompleted 属性,无参数 方法(任何返回类型,包括wait的void-result),以及 INotifyCompletion ICriticalNotifyCompletion 接口。两个接口都有 OnComplete 方法来注册回调。有一个令人难以置信的 继续 等待调用在这里进行,这在很大程度上取决于运行时环境。等待的默认行为是从 Task<T> 是用来 SynchronizationContext.Current (我想通过 TaskScheduler.Current )调用回调,或者,如果为null,则使用线程池(我认为是通过 TaskScheduler.Default )调用回调。包含await的方法被某些人包装为任务 CompilerServices 类(忘记名称),为方法的调用方提供上述行为,包装您正在等待的任何实现。

    A SynchronizationContext 什么时候 await 在一个 Task Result (这本身取决于对等待线程的调用),会出现死锁。

    另一方面,如果您将as-is方法中断到另一个线程,或者调用 ConfigureAwait 在任何任务上,或隐藏当前的计划程序 打电话,或者自己设定 SynchronizationContext.当前 (不推荐),您可以更改以上所有内容。