Non sto cercando una soluzione qui, più una spiegazione di cosa sta succedendo. Ho refactored questo codice per prevenire questo problema, ma sono incuriosito perché questa chiamata si blocca. Fondamentalmente ho un elenco di oggetti head e ho bisogno di caricare ogni singolo dettaglio da un oggetto repository DB (usando Dapper). Ho provato a farlo usando ContinueWith
ma non è riuscito:
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);
Qualcuno può spiegare cosa ha causato questo stallo? Ho cercato di capire cosa è successo qui, ma non ne sono sicuro. Presumo che abbia qualcosa a che fare con la chiamata. .Result
il ContinueWith
async
Le chiamate repo sono tutte sulla falsariga di:
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 });
}
}
Da allora ho risolto questo problema con il seguente codice:
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
});
Il problema è in realtà una combinazione di quanto sopra. È stata creata un'enumerazione di attività in cui ogni volta che viene enumerata l'enumerazione, una nuova chiamata GetDetails
. Una chiamata ToList
su questa selezione risolverà il deadlock. Senza consolidare i risultati dell'enumerazione (inserendoli in un elenco), la chiamata WhenAll
valuta l'enumerabile e attende le attività risultanti in modo asincrono senza problemi, ma quando l'istruzione Select restituita viene valutata, iterating e in attesa sincrona sui risultati delle attività risultanti da Fresh GetDetails
e ContinueWith
chiamate che non sono ancora state completate. È probabile che tutta questa attesa sincrona si verifichi durante il tentativo di serializzare la risposta.
Per quanto riguarda il motivo per cui quell'attesa sincrona provoca una situazione di stallo, il mistero sta nell'attesa delle cose. Dipende completamente da ciò che stai chiamando. Un'attesa è in realtà solo il recupero di un cameriere tramite qualsiasi metodo GetAwaiter
qualificante per l'ambito visibile e la registrazione di una richiamata che immediatamente chiama GetResult
sull'attenditore quando il lavoro è completo. Un metodo GetAwaiter
qualificante può essere un'istanza o un metodo di estensione che restituisce un oggetto con una proprietà IsCompleted
, un metodo GetResult
parametri (qualsiasi tipo restituito, incluso void - result of await) e interfacce INotifyCompletion
o ICriticalNotifyCompletion
. Le interfacce hanno entrambi i metodi OnComplete
per registrare il callback. C'è una catena da capogiro di ContinueWith
e attendono le chiamate in corso qui e in gran parte dipende dall'ambiente di runtime. Il comportamento predefinito dell'attesa che ottieni da un'attività Task<T>
è di utilizzare SynchronizationContext.Current
(penso tramite TaskScheduler.Current
) per richiamare la richiamata o, se è null, utilizzare il pool di thread (penso tramite TaskScheduler.Default
) per richiamare la richiamata. Un metodo che contiene un'attesa viene incapsulato come compito da qualche classe CompilerServices
(ha dimenticato il nome), dando ai chiamanti del metodo il comportamento sopra descritto che avvolge qualsiasi implementazione che si sta aspettando.
Un SynchronizationContext
può anche personalizzare questo, ma in genere ogni contesto richiama sul proprio thread singolo. Se un tale implementazione è presente sul SynchronizationContext.Current
quando await
che viene chiamato su un Task
, e in modo sincrono di attendere il Result
(che di per sé è contingente su un Invoke per il thread in attesa), si ottiene una situazione di stallo.
D'altra parte, se si interrompe il metodo as-is su un altro thread, o si chiama ConfigureAwait
su una delle attività, o si nasconde lo scheduler corrente per le chiamate ContinueWith
, o si imposta il proprio SynchronizationContext.Current
(non consigliato), si cambia tutto quanto sopra.