Ich bin nicht auf der Suche nach einer Lösung, sondern eher einer Erklärung dessen, was los ist. Ich habe diesen Code überarbeitet, um dieses Problem zu vermeiden, aber ich bin fasziniert, warum dieser Anruf blockiert ist. Grundsätzlich habe ich eine Liste von Kopfobjekten, und ich muss die Details aus einem DB-Repository-Objekt laden (mit Dapper). Ich habe dies mit ContinueWith
versucht, aber es ist fehlgeschlagen:
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);
Kann jemand erklären, was diesen Stillstand verursacht hat? Ich habe versucht, den Kopf herumzudrehen, was hier passiert ist, aber ich bin mir nicht sicher. Ich gehe davon aus, dass es etwas mit dem Aufruf von. .Result
tun hat. .Result
im ContinueWith
async
Kontext aufgerufen wird Die Repo-Aufrufe haben alle die folgenden Linien:
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 });
}
}
Ich habe dieses Problem seitdem mit dem folgenden Code behoben:
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
});
Das Problem ist eigentlich eine Kombination der oben genannten. Es wurde eine Aufzählung von Aufgaben erstellt, bei der bei jeder GetDetails
der Aufzählung ein GetDetails
Aufruf von GetDetails
. Ein ToList
Aufruf für diese Auswahl würde den Deadlock beheben. Ohne die Ergebnisse der Aufzählungsliste zu verfestigen (sie in eine Liste zu setzen), wertet der WhenAll
Aufruf die WhenAll
und wartet asynchron ohne Ausgabe auf die resultierenden Aufgaben. Wenn die zurückgegebene Select-Anweisung eine Auswertung durchführt, iteriert sie und wartet auf die Ergebnisse der resultierenden Aufgaben von frischen GetDetails
und ContinueWith
Aufrufen, die noch nicht abgeschlossen sind. Dieses synchrone Warten tritt wahrscheinlich auf, wenn versucht wird, die Antwort zu serialisieren.
Warum dieses synchrone Warten einen Stillstand verursacht, liegt das Geheimnis darin, wie abgewartet wird. Es hängt völlig davon ab, was Sie anrufen. Ein Erwarten ist eigentlich nur das Abrufen eines Erwarters über eine für jeden Bereich sichtbare, qualifizierende GetAwaiter
Methode und die Registrierung eines Rückrufs, der sofort GetResult
für den Erwarter aufruft, wenn die Arbeit abgeschlossen ist. Eine qualifizierte GetAwaiter
Methode kann eine GetAwaiter
oder Erweiterungsmethode sein, die ein Objekt mit einer IsCompleted
Eigenschaft, eine parameterlose GetResult
Methode (einen beliebigen Rückgabetyp, einschließlich void - result of await) und ICriticalNotifyCompletion
Schnittstellen INotifyCompletion
oder ICriticalNotifyCompletion
. Beide Schnittstellen verfügen über OnComplete
Methoden, um den Callback zu registrieren. Es gibt eine unübersehbare Kette von ContinueWith
und wartet auf Anrufe, die hier abgehalten werden. Ein großer Teil davon hängt von der Laufzeitumgebung ab. Das Standardverhalten des TaskScheduler.Current
von Task<T>
besteht im Verwenden von SynchronizationContext.Current
(ich denke über TaskScheduler.Current
), um den Rückruf aufzurufen, oder, falls dies null ist, um den Thread-Pool zu verwenden (denke ich über TaskScheduler.Default
). den Rückruf aufrufen. Eine Methode, die ein waitit enthält, wird von einer CompilerServices
Klasse als Task verpackt (den Namen vergessen). Dies gibt den Aufrufern der Methode das oben beschriebene Verhalten, das die Implementierung umgibt, auf die Sie warten.
Ein SynchronizationContext
kann dies auch anpassen, normalerweise wird jedoch jeder Kontext in seinem eigenen einzelnen Thread aufgerufen. Wenn eine solche Implementierung in SynchronizationContext.Current
wenn await
für eine Task
aufgerufen wird und Sie synchron auf das Result
warten (das selbst von einem Aufruf des wartenden Threads abhängig ist), wird ein Deadlock angezeigt.
Wenn Sie dagegen Ihre Ist-Methode in einen anderen Thread aufgelöst haben oder ConfigureAwait
für eine der Aufgaben aufrufen oder den aktuellen Scheduler für Ihre ContinueWith
Aufrufe ausblenden oder einen eigenen SynchronizationContext.Current
(nicht empfohlen) festlegen, werden Sie Ändern Sie alle oben genannten.