aguardar vs Task.Wait - Deadlock?

194

Não entendo bem a diferença entre Task.Waite await.

Eu tenho algo semelhante às seguintes funções em um serviço ASP.NET WebAPI:

public class TestController : ApiController
{
    public static async Task<string> Foo()
    {
        await Task.Delay(1).ConfigureAwait(false);
        return "";
    }

    public async static Task<string> Bar()
    {
        return await Foo();
    }

    public async static Task<string> Ros()
    {
        return await Bar();
    }

    // GET api/test
    public IEnumerable<string> Get()
    {
        Task.WaitAll(Enumerable.Range(0, 10).Select(x => Ros()).ToArray());

        return new string[] { "value1", "value2" }; // This will never execute
    }
}

Onde Getserá um impasse.

O que poderia causar isso? Por que isso não causa um problema quando eu uso uma espera de bloqueio em vez de await Task.Delay?

ronag
fonte
@ Servy: voltarei com um repo assim que tiver tempo. Por enquanto, ele trabalha com o Task.Delay(1).Wait()que é bom o suficiente.
ronag
2
Task.Delay(1).Wait()é basicamente a mesma coisa que Thread.Sleep(1000). No código de produção real, raramente é apropriado.
Servy
@ronag: Você WaitAllestá causando o impasse. Veja o link para o meu blog na minha resposta para mais detalhes. Você deve usar em seu await Task.WhenAlllugar.
Stephen Cleary
6
@ronag Como você tem ConfigureAwait(false)uma chamada únicaBar ou Rosnão entra em conflito, mas porque possui um enumerável que está criando mais de uma e aguardando todas, a primeira barra entra em impasse na segunda. Se você, em await Task.WhenAllvez de esperar em todas as tarefas, para não bloquear o contexto ASP, verá o método retornar normalmente.
Servy
2
@ronag Sua outra opção seria adicionar a .ConfigureAwait(false) todo o caminho até a árvore até que você bloquear, dessa forma nada está sempre tentando voltar para o contexto principal; Isso funcionaria. Outra opção seria ativar um contexto de sincronização interno. Link . Se você colocar o item Task.WhenAllem, AsyncPump.Runele efetivamente bloqueará a coisa toda sem a necessidade de ir a ConfigureAwaitqualquer lugar, mas provavelmente é uma solução excessivamente complexa.
Servy

Respostas:

268

Waite await- embora parecidos conceitualmente - são realmente completamente diferentes.

Waitserá bloqueado de forma síncrona até que a tarefa seja concluída. Portanto, o encadeamento atual é literalmente bloqueado, aguardando a conclusão da tarefa. Como regra geral, você deve usar " asynctodo o caminho"; isto é, não bloqueie o asynccódigo. No meu blog, mostro os detalhes de como o bloqueio de código assíncrono causa um conflito .

awaitaguardará assincronamente até que a tarefa seja concluída. Isso significa que o método atual está "pausado" (seu estado é capturado) e o método retorna uma tarefa incompleta para o responsável pela chamada. Posteriormente, quando a awaitexpressão for concluída, o restante do método será agendado como uma continuação.

Você também mencionou um "bloco cooperativo", pelo qual suponho que você quer dizer que uma tarefa na qual você está Waitexecutando pode ser executada no thread em espera. Há situações em que isso pode acontecer, mas é uma otimização. Há muitas situações em que isso não pode acontecer, como se a tarefa é para outro agendador, ou se já foi iniciada ou se é uma tarefa que não é de código (como no seu exemplo de código: Waitnão é possível executar a Delaytarefa em linha porque não há código por isso).

Você pode achar minha async/ awaitintrodução útil.

Stephen Cleary
fonte
4
Eu acho que há um mal-entendido, Waitfunciona bem awaitimpasses.
ronag
5
Não, o agendador de tarefas não fará isso. Waitbloqueia o encadeamento e não pode ser usado para outras coisas.
Stephen Cleary
8
@ronag Meu palpite é que você acabou de misturar os nomes dos métodos e o seu conflito foi realmente causado pelo código de bloqueio e funcionou com o awaitcódigo. Ou isso, ou o impasse não estava relacionado a nenhum deles e você diagnosticou mal o problema.
Servy
3
@hexterminator: Isso ocorre por design - funciona muito bem para aplicativos de interface do usuário, mas tende a atrapalhar os aplicativos ASP.NET. O ASP.NET Core corrigiu isso removendo o SynchronizationContext, portanto, o bloqueio em uma solicitação do ASP.NET Core não causa mais conflitos.
Stephen Cleary
1
no msdn aqui, diz que a espera é executada em thread separado de forma assíncrona. Estou esquecendo de algo? msdn.microsoft.com/en-us/library/hh195051(v=vs.110).aspx
batmaci
6

Com base no que li de diferentes fontes:

Uma awaitexpressão não bloqueia o encadeamento no qual está executando. Em vez disso, faz com que o compilador inscreva o restante do asyncmétodo como uma continuação da tarefa esperada. O controle retorna ao chamador do asyncmétodo. Quando a tarefa é concluída, ela invoca sua continuação e a execução do asyncmétodo continua de onde parou.

Para aguardar taska conclusão de um único , você pode chamar seu Task.Waitmétodo. Uma chamada para o Waitmétodo bloqueia o encadeamento de chamada até que a instância de classe única conclua a execução. O Wait()método sem parâmetros é usado para esperar incondicionalmente até que uma tarefa seja concluída. A tarefa simula o trabalho chamando o Thread.Sleepmétodo para dormir por dois segundos.

Este artigo também é uma boa leitura.

Ayushmati
fonte
3
"Isso não é tecnicamente incorreto, então? Alguém pode esclarecer?" - posso esclarecer; você está fazendo isso como uma pergunta? (Eu só quero deixar claro se você está perguntando versus respondendo). Se você está perguntando: pode funcionar melhor como uma pergunta separada; é improvável que você obtenha novas respostas aqui como resposta
Marc Gravell
1
Respondi à pergunta e fiz uma pergunta separada para a dúvida que eu tinha aqui stackoverflow.com/questions/53654006/… Obrigado @MarcGravell. Você pode remover seu voto de exclusão para a resposta agora?
Ayushmati
"Você pode remover seu voto de exclusão para a resposta agora?" - isso não é meu; graças à ♦, qualquer votação por mim entraria em vigor imediatamente. No entanto, não acho que isso responda aos pontos principais da pergunta, que é sobre o comportamento do impasse.
Marc Gravell
-2

Alguns fatos importantes não foram dados em outras respostas:

O "async waiting" é mais complexo no nível CIL e, portanto, custa tempo de memória e CPU.

Qualquer tarefa pode ser cancelada se o tempo de espera for inaceitável.

No caso de "async aguardar", não temos um manipulador para essa tarefa cancelá-la ou monitorá-la.

O uso do Task é mais flexível que o "assíncrono aguarda".

Qualquer funcionalidade de sincronização pode ser agrupada por assíncrona.

public async Task<ActionResult> DoAsync(long id) 
{ 
    return await Task.Run(() => { return DoSync(id); } ); 
} 

Não vejo por que devo viver com a duplicação de código para o método de sincronização e assíncrona ou usando hacks.

user1785960
fonte