Um exemplo assíncrono / espera que causa um deadlock

94

Me deparei com algumas práticas recomendadas para programação assíncrona usando c # 's async/ awaitkeywords (eu sou novo no c # 5.0).

Um dos conselhos dados foi o seguinte:

Estabilidade: Conheça seus contextos de sincronização

... Alguns contextos de sincronização são não reentrantes e de thread único. Isso significa que apenas uma unidade de trabalho pode ser executada no contexto em um determinado momento. Um exemplo disso é o thread da IU do Windows ou o contexto de solicitação ASP.NET. Nesses contextos de sincronização de thread único, é fácil travar você mesmo. Se você gerar uma tarefa de um contexto de thread único e esperar por essa tarefa no contexto, seu código de espera pode estar bloqueando a tarefa em segundo plano.

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

Se eu tentar dissecá-lo sozinho, o thread principal gerará um novo em MyWebService.GetDataAsync();, mas, como o thread principal aguarda lá, ele espera o resultado em GetDataAsync().Result. Enquanto isso, digamos que os dados estejam prontos. Por que o thread principal não continua sua lógica de continuação e retorna um resultado de string GetDataAsync()?

Alguém pode me explicar por que há um impasse no exemplo acima? Estou completamente sem noção sobre qual é o problema ...

Dror Weiss
fonte
Tem certeza de que GetDataAsync finaliza suas coisas? Ou fica preso causando apenas bloqueio e não deadlock?
Andrey
Este é o exemplo fornecido. No meu entender, deve terminar o que está acontecendo e ter algum tipo de resultado pronto ...
Dror Weiss
4
Por que você está esperando pela tarefa? Em vez disso, você deve esperar porque basicamente perdeu todos os benefícios do modelo assíncrono.
Toni Petrina
Para adicionar ao ponto de @ToniPetrina, mesmo sem o problema de deadlock, var data = GetDataAsync().Result;é uma linha de código que nunca deve ser feita em um contexto que você não deve bloquear (solicitação de UI ou ASP.NET). Mesmo que isso não ocorra, ele está bloqueando o thread por um período de tempo indeterminado. Então, basicamente, é um exemplo terrível. [Você precisa sair do thread da IU antes de executar um código como esse, ou usar awaitlá também, como Toni sugere.]
ToolmakerSteve

Respostas:

81

Dê uma olhada neste exemplo , Stephen tem uma resposta clara para você:

Então é isso que acontece, começando com o método de nível superior ( Button1_Clickpara UI / MyController.Getpara ASP.NET):

  1. As chamadas de método de nível superior GetJsonAsync(dentro do contexto de UI / ASP.NET).

  2. GetJsonAsyncinicia a solicitação REST chamando HttpClient.GetStringAsync(ainda dentro do contexto).

  3. GetStringAsyncretorna um incompleto Task, indicando que a solicitação REST não foi concluída.

  4. GetJsonAsyncaguarda o Taskdevolvido por GetStringAsync. O contexto é capturado e será usado para continuar executando o GetJsonAsyncmétodo posteriormente. GetJsonAsyncretorna um incompleto Task, indicando que o GetJsonAsyncmétodo não está completo.

  5. O método de nível superior bloqueia de forma síncrona no Taskretornado por GetJsonAsync. Isso bloqueia o segmento de contexto.

  6. ... Eventualmente, a solicitação REST será concluída. Isso completa o Taskque foi retornado por GetStringAsync.

  7. A continuação de GetJsonAsyncagora está pronta para ser executada e aguarda que o contexto esteja disponível para que possa ser executada no contexto.

  8. Impasse . O método de nível superior está bloqueando o encadeamento de contexto, aguardando GetJsonAsynca conclusão e GetJsonAsyncaguardando que o contexto seja liberado para que possa ser concluído. Para o exemplo da IU, o "contexto" é o contexto da IU; para o exemplo ASP.NET, o "contexto" é o contexto da solicitação ASP.NET. Este tipo de impasse pode ser causado por qualquer "contexto".

Outro link que você deve ler: Await, e UI e deadlocks! Oh meu!

cuongle
fonte
20
  • Fato 1: GetDataAsync().Result;será executado quando a tarefa retornada por for GetDataAsync()concluída, enquanto isso bloqueia o thread de interface do usuário
  • Fato 2: a continuação de await ( return result.ToString()) é enfileirada no thread de IU para execução
  • Fato 3: A tarefa retornada por GetDataAsync()será concluída quando sua continuação na fila for executada
  • Fato 4: a continuação da fila nunca é executada, porque o thread de IU está bloqueado (Fato 1)

Impasse!

O impasse pode ser resolvido por alternativas fornecidas para evitar o Fato 1 ou o Fato 2.

  • Evite 1,4. Em vez de bloquear o UI thread, use var data = await GetDataAsync(), que permite que o UI thread continue em execução
  • Evite 2,3. Enfileire a continuação de await para um thread diferente que não está bloqueado, por exemplo var data = Task.Run(GetDataAsync).Result, use , que postará a continuação para o contexto de sincronização de um thread de pool de threads. Isso permite que a tarefa retornada por GetDataAsync()seja concluída.

Isso é muito bem explicado em um artigo de Stephen Toub , no meio do caminho, onde ele usa o exemplo de DelayAsync().

Phillip Ngan
fonte
Quanto a, var data = Task.Run(GetDataAsync).Resultisso é novo para mim. Sempre pensei que o exterior .Resultestaria disponível assim que a primeira espera GetDataAsyncfosse atingida, e datasempre estará default. Interessante.
nawfal
19

Eu estava apenas brincando com esse problema novamente em um projeto ASP.NET MVC. Quando você deseja chamar asyncmétodos de a PartialView, não tem permissão para fazer o PartialView async. Você receberá uma exceção se o fizer.

Você pode usar a seguinte solução alternativa simples no cenário em que deseja chamar um asyncmétodo de um método de sincronização:

  1. Antes da chamada, limpe o SynchronizationContext
  2. Faça a ligação, não vai haver mais impasse aqui, espere terminar
  3. Restaure o SynchronizationContext

Exemplo:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}
Herre Kuijpers
fonte
3

Outro ponto principal é que você não deve bloquear no Tasks e usar async até o fim para evitar deadlocks. Então, tudo será bloqueio assíncrono e não síncrono.

public async Task<ActionResult> ActionAsync()
{

    var data = await GetDataAsync();

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}
MarvelTracker
fonte
6
E se eu quiser que o thread principal (UI) seja bloqueado até que a tarefa seja concluída? Ou em um aplicativo de console, por exemplo? Digamos que eu queira usar HttpClient, que oferece suporte assíncrono ... Como faço para usá-lo de forma síncrona sem o risco de deadlock ? Isso deve ser possível. Se o WebClient pode ser usado dessa forma (por ter métodos de sincronização) e funciona perfeitamente, então por que não poderia ser feito com o HttpClient também?
Dexter
Veja a resposta de Philip Ngan acima (eu sei que isso foi postado após este comentário): Enfileirar a continuação de await para um thread diferente que não está bloqueado, por exemplo, use var data = Task.Run (GetDataAsync) .Result
Jeroen
@Dexter - re " E se eu quiser que o thread principal (UI) seja bloqueado até que a tarefa termine? " - você realmente deseja que o thread da interface do usuário seja bloqueado, o que significa que o usuário não pode fazer nada, não pode nem mesmo cancelar - ou é é que você não quer continuar o método em que está? "await" ou "Task.ContinueWith" tratam do último caso.
Toolmaker Steve
@ToolmakerSteve, é claro que não quero continuar o método. Mas eu simplesmente não posso usar await porque também não posso usar assíncrono - HttpClient é invocado em main , que obviamente não pode ser assíncrono. E então eu mencionei fazendo tudo isso em um aplicativo Console - neste caso eu quero exatamente o ex - Eu não quero que meu aplicativo para ainda ser multi-threaded. Bloqueie tudo .
Dexter
-1

Uma solução alternativa é usar um Joinmétodo de extensão na tarefa antes de solicitar o resultado.

O código é parecido com este:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

Onde o método de junção é:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

Não sou o suficiente no domínio para ver as desvantagens desta solução (se houver)

Orace
fonte