Je ne suis pas après une solution ici, plus une explication de ce qui se passe. J'ai refactoré ce code pour éviter ce problème, mais je suis intrigué par la raison pour laquelle cet appel est dans l'impasse. En gros, j'ai une liste d'objets head et je dois charger les détails de chacun d'eux à partir d'un objet de référentiel de base de données (à l'aide de Dapper). J'ai essayé de faire cela en utilisant ContinueWith
mais cela a échoué:
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);
Quelqu'un peut-il expliquer la cause de cette impasse? J'ai essayé de comprendre ce qui s'est passé ici mais je ne suis pas sûr. Je suppose que c'est quelque chose à voir avec appeler .Result
dans le ContinueWith
async
Les appels de repo sont tout le long des lignes de:
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 });
}
}
J'ai depuis résolu ce problème avec le code suivant:
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
});
Le problème est en réalité une combinaison de ce qui précède. Une énumération de tâches a été créée et chaque fois que l'énumération est itérée, un nouvel appel GetDetails
. Un appel ToList
sur cette sélection ToList
le blocage. Sans solidifier les résultats de l'énumérable (en les plaçant dans une liste), l'appel WhenAll
évalue l'énumérable et attend les tâches résultantes de manière asynchrone, sans problème, mais lorsque l'instruction Select renvoyée est évaluée, elle itère et attend de manière synchrone les résultats des tâches résultantes. à partir de nouveaux GetDetails
et ContinueWith
qui ne se sont pas encore terminés. Toute cette attente synchrone est susceptible de se produire lors de la tentative de sérialisation de la réponse.
Quant à savoir pourquoi cette attente synchrone provoque une impasse, le mystère est de savoir comment attendre fait les choses. Cela dépend complètement de ce que vous appelez. Une attente est en fait juste la récupération d'un attente via n'importe GetAwaiter
méthode GetAwaiter
qualifiante visible dans l' GetAwaiter
et l'enregistrement d'un rappel qui appelle immédiatement GetResult
sur l'attendeur une fois le travail terminé. Une méthode GetAwaiter
qualifiante peut être une méthode d’instance ou d’extension qui renvoie un objet doté d’une propriété IsCompleted
, une méthode GetResult
paramètre (tout type de retour, y compris void - result of INotifyCompletion
et des interfaces INotifyCompletion
ou ICriticalNotifyCompletion
. Les deux interfaces ont des méthodes OnComplete
pour enregistrer le rappel. Il y a une chaîne époustouflante de ContinueWith
et d'attendre les appels qui se passent ici et cela dépend en grande partie de l'environnement d'exécution. Le comportement par défaut de l'attente que vous obtenez d'une Task<T>
consiste à utiliser SynchronizationContext.Current
(je pense via TaskScheduler.Current
) pour appeler le rappel ou, s'il est nul, à utiliser le pool de threads (je pense via TaskScheduler.Default
) invoquer le rappel. Une méthode contenant une attente est encapsulée en tant que tâche par une classe CompilerServices
(oubliée), donnant ainsi aux appelants de la méthode le comportement décrit ci-dessus qui encapsule l’implémentation que vous attendez.
Un SynchronizationContext
peut également personnaliser cela, mais généralement chaque contexte appelle sur son propre thread unique. Si une telle mise en œuvre est présent sur SynchronizationContext.Current
quand await
est fait appel à un Task
de , et vous synchroniquement attendre le Result
(qui est lui - même subordonné à un Invoke au fil d'attente), vous obtenez une impasse.
Par contre, si vous avez cassé votre méthode as-is en un autre thread, ou si vous appelez ConfigureAwait
pour l'une des tâches, ou si vous masquez le planificateur actuel de vos appels ContinueWith
, ou si vous définissez votre propre SynchronizationContext.Current
(non recommandé), changer tout ce qui précède.