Por que essa ação assíncrona trava?

102

Eu tenho um aplicativo .Net 4.5 multicamadas chamando um método usando C #'s new asynce awaitpalavras - chave que simplesmente trava e não consigo ver o porquê.

Na parte inferior, tenho um método assíncrono que estende nosso utilitário de banco de dados OurDBConn(basicamente um wrapper para os objetos DBConnectione subjacentes DBCommand):

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

Então eu tenho um método assíncrono de nível médio que chama isso para obter alguns totais de execução lenta:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

Finalmente, tenho um método de IU (uma ação MVC) que é executado de forma síncrona:

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

O problema é que essa última linha fica para sempre. Ele faz a mesma coisa se eu ligar asyncTask.Wait(). Se eu executar o método SQL lento diretamente, leva cerca de 4 segundos.

O comportamento que estou esperando é que, quando chegar ao fim asyncTask.Result, se não estiver concluído, espere até que termine e, assim que terminar, retorne o resultado.

Se eu avançar com um depurador, a instrução SQL será concluída e a função lambda será concluída, mas a return result;linha de GetTotalAsyncnunca será alcançada.

Alguma ideia do que estou fazendo de errado?

Alguma sugestão de onde eu preciso investigar para corrigir isso?

Isso poderia ser um impasse em algum lugar e, se for o caso, existe alguma maneira direta de encontrá-lo?

Keith
fonte

Respostas:

150

Sim, isso é um impasse, certo. E um erro comum com o TPL, então não se sinta mal.

Quando você escreve await foo, o tempo de execução, por padrão, agenda a continuação da função no mesmo SynchronizationContext em que o método foi iniciado. Em inglês, digamos que você tenha chamado seu a ExecuteAsyncpartir do thread da interface do usuário. Sua consulta é executada no thread de pool de threads (porque você a chamou Task.Run), mas você aguarda o resultado. Isso significa que o tempo de execução agendará sua return result;linha para " " retornar ao thread de UI, em vez de agendá-la de volta para o threadpool.

Então, como esse impasse? Imagine que você acabou de ter este código:

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

Portanto, a primeira linha dá início ao trabalho assíncrono. A segunda linha bloqueia o thread da IU . Portanto, quando o tempo de execução deseja executar a linha de "resultado de retorno" de volta no thread de interface do usuário, ele não pode fazer isso até que seja Resultconcluído. Mas é claro, o Resultado não pode ser dado até que aconteça o retorno. Impasse.

Isso ilustra uma regra fundamental de uso do TPL: ao usar .Resultem um thread de UI (ou algum outro contexto de sincronização sofisticado), você deve ter cuidado para garantir que nada do qual Task dependa seja agendado para o thread de UI. Ou então a maldade acontece.

Então, o que você faz? A opção nº 1 é usar e esperar em qualquer lugar, mas como você disse, essa já não é uma opção. A segunda opção disponível para você é simplesmente parar de usar o await. Você pode reescrever suas duas funções para:

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

Qual é a diferença? Agora não há espera em lugar nenhum, então nada sendo agendado implicitamente para o thread de interface do usuário. Para métodos simples como esses que têm um único retorno, não há nenhum ponto em fazer um var result = await...; return resultpadrão " "; apenas remova o modificador assíncrono e passe o objeto de tarefa diretamente. É menos sobrecarga, se nada mais.

A opção nº 3 é especificar que você não deseja que suas esperas sejam agendadas de volta para o thread de IU, mas apenas para o pool de threads. Você faz isso com o ConfigureAwaitmétodo, assim:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

Esperar uma tarefa normalmente seria agendado para o thread de IU se você estiver nele; aguardar o resultado de ContinueAwaitirá ignorar qualquer contexto em que você esteja e sempre agendar para o threadpool. A desvantagem desta situação é que você tem que regar esta em todos os lugares , em todas as funções de seu .Result depende, porque qualquer falta .ConfigureAwaitpode ser a causa de outro impasse.

Jason Malinowski
fonte
6
BTW, a questão é sobre ASP.NET, então não há thread de interface do usuário. Mas o problema com deadlocks é exatamente o mesmo, por causa do ASP.NET SynchronizationContext.
svick
Isso explicava muito, pois eu tinha um código .Net 4 semelhante que não tinha o problema, mas usava o TPL sem as palavras-chave async/ await.
Keith
2
TPL = Biblioteca Paralela de Tarefas msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx
Jamie Ide
Se alguém estiver procurando pelo código VB.net (como eu), ele será explicado aqui: docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/…
MichaelDarkBlue
Você pode me ajudar em stackoverflow.com/questions/54360300/…
Jitendra Pancholi
36

Este é o asynccenário clássico de deadlock misto , conforme descrevo em meu blog . Jason descreveu bem: por padrão, um "contexto" é salvo a cada awaite usado para continuar o asyncmétodo. Este "contexto" é o atual a SynchronizationContextmenos que seja null, neste caso é o atual TaskScheduler. Quando o asyncmétodo tenta continuar, ele primeiro entra novamente no "contexto" capturado (neste caso, um ASP.NET SynchronizationContext). O ASP.NET SynchronizationContextpermite apenas um thread no contexto por vez, e já existe um thread no contexto - o thread está bloqueado Task.Result.

Existem duas diretrizes que evitarão esse impasse:

  1. Use asynctodo o caminho para baixo. Você mencionou que "não pode" fazer isso, mas não tenho certeza do motivo. A ASP.NET MVC no .NET 4.5 certamente pode oferecer suporte a asyncações, e não é uma mudança difícil de fazer.
  2. Use ConfigureAwait(continueOnCapturedContext: false)o máximo possível. Isso substitui o comportamento padrão de retomar no contexto capturado.
Stephen Cleary
fonte
Faz ConfigureAwait(false)garantia de que a função atual recomeça em um contexto diferente?
chue x
A estrutura MVC oferece suporte, mas faz parte de um aplicativo MVC existente com muitos JS do lado do cliente já presentes. Não consigo mudar facilmente para uma asyncação sem interromper a maneira como isso funciona do lado do cliente. Eu certamente planejo investigar essa opção a longo prazo.
Keith
Só para esclarecer meu comentário - estava curioso para saber se o uso ConfigureAwait(false)da árvore de chamadas teria resolvido o problema do OP.
chue x
3
@Keith: Fazer uma ação MVC asyncnão afeta o lado do cliente de forma alguma. Explico isso em outra postagem do blog, asyncNão altera o protocolo HTTP .
Stephen Cleary
1
@Keith: É normal async"crescer" por meio da base de código. Se o seu método de controlador pode depender de operações assíncronas, o método da classe base deve retornar Task<ActionResult>. Fazer a transição de um grande projeto para o qual asyncé sempre complicado porque misturar asynce sincronizar o código é difícil e complicado. O asynccódigo puro é muito mais simples.
Stephen Cleary
12

Eu estava na mesma situação de impasse, mas, no meu caso, chamando um método assíncrono de um método de sincronização, o que funciona para mim foi:

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=> 
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

essa é uma boa abordagem, alguma ideia?

Danilow
fonte
Essa solução está funcionando para mim também, mas não tenho certeza se é uma boa solução ou pode falhar em algum lugar. Qualquer um pode explicar isso
Konstantin Vdovkin
bem, finalmente eu
escolhi
1
Acho que você está tendo um impacto no desempenho ao usar Task.Run. Em meus testes, o Task.Run está quase dobrando o tempo de execução de uma solicitação http de 100 ms.
Timothy Gonzalez,
1
faz sentido, você está criando uma nova tarefa para encerrar uma chamada assíncrona, o desempenho é a compensação
Danilow,
Fantástico, isso funcionou para mim também, meu caso também foi causado por um método síncrono chamando um método assíncrono. Obrigado!
Leonardo Spina
4

Apenas para adicionar à resposta aceita (não há representante suficiente para comentar), tive esse problema ao bloquear o uso de task.Result, embora todos os eventos awaitabaixo tivessem ConfigureAwait(false), como neste exemplo:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

O problema realmente estava no código da biblioteca externa. O método de biblioteca assíncrona tentou continuar no contexto de sincronização de chamada, independentemente de como eu configurei o await, levando ao deadlock.

Portanto, a resposta foi lançar minha própria versão do código da biblioteca externa ExternalLibraryStringAsync, para que ela tivesse as propriedades de continuação desejadas.


resposta errada para fins históricos

Depois de muita dor e angústia, encontrei a solução enterrada nesta postagem do blog (Ctrl-f para 'impasse'). Ele gira em torno do uso task.ContinueWith, em vez do básico task.Result.

Exemplo de deadlock anterior:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

Evite o impasse como este:

public Foo GetFooSynchronous
{
    var foo = new Foo();
    GetInfoAsync()  // ContinueWith doesn't run until the task is complete
        .ContinueWith(task => foo.Info = task.Result);
    return foo;
}

private async Task<string> GetInfoAsync
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}
Cameron Jeffers
fonte
Qual é o downvote para? Esta solução está funcionando para mim.
Cameron Jeffers
Você está retornando o objeto antes de Taskser concluído e não fornecendo ao chamador nenhum meio de determinar quando a mutação do objeto retornado realmente ocorre.
Servy
hmm sim, entendo. Portanto, devo expor algum tipo de método de "esperar até que a tarefa seja concluída" que usa um loop while de bloqueio manual (ou algo parecido)? Ou empacotar tal bloqueio no GetFooSynchronousmétodo?
Cameron Jeffers
1
Se você fizer isso, será um impasse. Você precisa assíncronizar todo o caminho retornando um em Taskvez de bloqueio.
Servy
Infelizmente, essa não é uma opção, a classe implementa uma interface síncrona que não posso alterar.
Cameron Jeffers
0

resposta rápida: mude esta linha

ResultClass slowTotal = asyncTask.Result;

para

ResultClass slowTotal = await asyncTask;

porque? você não deve usar .result para obter o resultado das tarefas dentro da maioria dos aplicativos, exceto aplicativos de console, se você fizer isso o seu programa irá travar quando chegar lá

você também pode tentar o código abaixo se quiser usar .Result

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;
Ramin
fonte