No estoy buscando una solución aquí, más una explicación de lo que está pasando. He refactorizado este código para evitar este problema, pero estoy intrigado por qué esta llamada está bloqueada. Básicamente tengo una lista de objetos principales y necesito cargar los detalles de cada uno desde un objeto de repositorio de bases de datos (usando Dapper). Intenté hacer esto usando ContinueWith
pero falló:
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);
¿Alguien puede explicar qué causó este punto muerto? Traté de entender lo que sucedió aquí, pero no estoy seguro. Supongo que es algo que tiene que ver con las llamadas .Result
en el ContinueWith
async
Las llamadas de repo están a lo largo de las líneas 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 });
}
}
Desde entonces he solucionado este problema con el siguiente código:
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
});
El problema es en realidad una combinación de lo anterior. Se creó una enumeración de tareas donde cada vez que se enumera la enumeración, una nueva llamada GetDetails
. Una llamada ToList
en este Select solucionaría el interbloqueo. Sin consolidar los resultados de los enumerables (colocándolos en una lista), la llamada WhenAll
evalúa los enumerables y espera las tareas resultantes de forma asíncrona sin problemas, pero cuando la declaración Select devuelta evalúa, está iterando y sincronizando los resultados de las tareas resultantes. desde las llamadas GetDetails
y ContinueWith
nuevas que aún no se han completado. Es probable que toda esta espera síncrona se produzca al intentar serializar la respuesta.
En cuanto a por qué esa espera sincrónica causa un punto muerto, el misterio está en cómo esperar las cosas. Depende completamente de lo que estés llamando. Una espera es en realidad la recuperación de un esperante a través de cualquier método GetAwaiter
calificado de alcance visible y el registro de una devolución de llamada que inmediatamente llama a GetResult
en el esperador cuando el trabajo está completo. Un método GetAwaiter
califica puede ser un método de instancia o extensión que devuelve un objeto que tiene una propiedad IsCompleted
, un método GetResult
parámetros (cualquier tipo de devolución, incluyendo void - result of INotifyCompletion
), e interfaces INotifyCompletion
o ICriticalNotifyCompletion
. Ambas interfaces tienen métodos OnComplete
para registrar la devolución de llamada. Hay una cadena alucinante de ContinueWith
y aguardan las llamadas que están ocurriendo aquí y gran parte depende del entorno de ejecución. El comportamiento predeterminado de la espera que obtienes de una Task<T>
es usar SynchronizationContext.Current
(creo que a través de TaskScheduler.Current
) para invocar la devolución de llamada o, si eso es nulo para usar el grupo de hilos (creo que a través de TaskScheduler.Default
) para invocar la devolución de llamada. Un método que contiene una espera se envuelve como una tarea por parte de alguna clase de CompilerServices
(olvidó el nombre), dando a los llamadores del método el comportamiento descrito anteriormente envolviendo cualquier implementación que esté esperando.
Un SynchronizationContext
también puede personalizar esto, pero generalmente cada contexto invoca en su propio hilo único. Si dicha aplicación está presente en SynchronizationContext.Current
cuando await
que se llama en una Task
, y esperas de forma sincrónica para el Result
(que a su vez depende de una invocación a la rosca de espera), se obtiene un punto muerto.
Por otro lado, si rompió su método tal como está en otro hilo, o llama a ConfigureAwait
en cualquiera de las tareas, u oculta el programador actual para sus llamadas ContinueWith
, o configura su propio SynchronizationContext.Current
(no recomendado), Cambia todo lo anterior.