Quando eu usaria Task.Yield ()?

218

Estou usando async / waitit e Taskmuito, mas nunca usei Task.Yield()e para ser honesto, mesmo com todas as explicações, não entendo por que precisaria desse método.

Alguém pode dar um bom exemplo onde Yield()é necessário?

Krumelur
fonte

Respostas:

241

Quando você usa async/ await, não há garantia de que o método que você chama ao await FooAsync()executar seja executado de forma assíncrona. A implementação interna é livre para retornar usando um caminho completamente síncrono.

Se você estiver criando uma API em que é fundamental que você não bloqueie e execute algum código de forma assíncrona, e há uma chance de o método chamado ser executado de forma síncrona (efetivamente bloqueada), usar await Task.Yield()forçará seu método a ser assíncrono e retornará controle nesse ponto. O restante do código será executado posteriormente (nesse ponto, ele ainda poderá ser executado de forma síncrona) no contexto atual.

Isso também pode ser útil se você criar um método assíncrono que requer alguma inicialização "de longa execução", ou seja:

 private async void button_Click(object sender, EventArgs e)
 {
      await Task.Yield(); // Make us async right away

      var data = ExecuteFooOnUIThread(); // This will run on the UI thread at some point later

      await UseDataAsync(data);
 }

Sem a Task.Yield()chamada, o método será executado de forma síncrona até a primeira chamada para await.

Reed Copsey
fonte
26
Sinto que estou interpretando mal algo aqui. Se await Task.Yield()força o método a ser assíncrono, por que nos preocuparíamos em escrever código assíncrono "real"? Imagine um método de sincronização pesada. Para torná-lo assíncrono, basta adicionar asynce await Task.Yield()no início e magicamente, será assíncrono? Isso seria como envolver todo o código de sincronização Task.Run()e criar um método assíncrono falso.
Krumelur 25/03
14
@ Krumelur Há uma grande diferença - veja meu exemplo. Se você usar um Task.Runpara implementá-lo, ExecuteFooOnUIThreadserá executado no pool de threads, não no thread da interface do usuário. Com await Task.Yield(), você o força a ser assíncrono, de forma que o código subsequente ainda seja executado no contexto atual (apenas posteriormente). Não é algo que você normalmente faria, mas é bom que exista a opção se for necessária por algum motivo estranho.
Reed Copsey
7
Mais uma pergunta: se a ExecuteFooOnUIThread()execução fosse muito longa, ele ainda bloqueará o thread da interface do usuário por um longo período de tempo e deixaria a interface sem resposta, está correto?
Krumelur
7
@ Krumelur Sim, seria. Apenas não imediatamente - isso aconteceria mais tarde.
Reed Copsey
33
Embora essa resposta seja tecnicamente correta, a declaração de que "o restante do código será executado posteriormente" é muito abstrata e pode ser enganosa. O agendamento de execução do código após Task.Yield () depende muito do SynchronisationContext concreto. E a documentação do MSDN afirma claramente que "o contexto de sincronização presente em um encadeamento da interface do usuário na maioria dos ambientes de interface do usuário geralmente prioriza o trabalho publicado no contexto mais alto do que o trabalho de entrada e renderização. Por esse motivo, não conte com a espera de Task.Yield () ; para manter uma interface do usuário responsiva. "
Vitaliy Tsvayer /
36

Internamente, await Task.Yield()basta enfileirar a continuação no contexto de sincronização atual ou em um encadeamento de pool aleatório, se SynchronizationContext.Currentfornull .

É implementado eficientemente como garçom personalizado. Um código menos eficiente que produz o efeito idêntico pode ser tão simples quanto isto:

var tcs = new TaskCompletionSource<bool>();
var sc = SynchronizationContext.Current;
if (sc != null)
    sc.Post(_ => tcs.SetResult(true), null);
else
    ThreadPool.QueueUserWorkItem(_ => tcs.SetResult(true));
await tcs.Task;

Task.Yield()pode ser usado como atalho para algumas alterações estranhas no fluxo de execução. Por exemplo:

async Task DoDialogAsync()
{
    var dialog = new Form();

    Func<Task> showAsync = async () => 
    {
        await Task.Yield();
        dialog.ShowDialog();
    }

    var dialogTask = showAsync();
    await Task.Yield();

    // now we're on the dialog's nested message loop started by dialog.ShowDialog 
    MessageBox.Show("The dialog is visible, click OK to close");
    dialog.Close();

    await dialogTask;
    // we're back to the main message loop  
}

Dito isto, não consigo pensar em nenhum caso em Task.Yield()que não possa ser substituído porTask.Factory.StartNew um agendador de tarefas adequado.

Veja também:

noseratio
fonte
No seu exemplo, qual é a diferença entre o que existe e o que var dialogTask = await showAsync();?
Erik Philips
@ErikPhilips, var dialogTask = await showAsync()não será compilado porque a await showAsync()expressão não retorna a Task(ao contrário de sem await). Dito isto, se você fizer await showAsync(), a execução depois que ela será retomada somente após o fechamento do diálogo, é assim que é diferente. Isso window.ShowDialogocorre porque é uma API síncrona (apesar de ainda gerar mensagens). Nesse código, eu queria continuar enquanto a caixa de diálogo ainda é mostrada.
noseratio
5

Um dos usos Task.Yield()é evitar um estouro de pilha ao fazer recursão assíncrona. Task.Yield()impede a continuação síncrona. Observe, no entanto, que isso pode causar uma exceção OutOfMemory (conforme observado por Triynko). A recursão infinita ainda não é segura e é melhor reescrever a recursão como um loop.

private static void Main()
    {
        RecursiveMethod().Wait();
    }

    private static async Task RecursiveMethod()
    {
        await Task.Delay(1);
        //await Task.Yield(); // Uncomment this line to prevent stackoverlfow.
        await RecursiveMethod();
    }
Joakim MH
fonte
4
Isso pode impedir o estouro de uma pilha, mas isso acabará ficando sem memória do sistema se você deixá-lo funcionar por tempo suficiente. Cada iteração criaria uma nova tarefa que nunca é concluída, pois a tarefa externa aguarda uma tarefa interna, que aguarda mais uma tarefa interna e assim por diante. Isso não está bom. Como alternativa, você pode simplesmente ter uma tarefa mais externa que nunca seja concluída e fazer com que ela faça um loop em vez de recursar. A tarefa nunca seria concluída, mas haveria apenas uma delas. Dentro do loop, ele pode render ou aguardar o que você quiser.
Triynko 19/10/19
Não consigo reproduzir o estouro da pilha. Parece que o await Task.Delay(1)suficiente para impedi-lo. (Aplicativo para console, .NET Core 3.1, C # 8)
Theodor Zoulias
-8

Task.Yield() pode ser usado em implementações simuladas de métodos assíncronos.

mhsirig
fonte
4
Você deve fornecer alguns detalhes.
PJProudhon
3
Para esse propósito, prefiro usar Task.CompletedTask - consulte a seção Task.CompletedTask nesta postagem do blog msdn para obter mais considerações.
Grzegorz Smulko
2
O problema com o uso de Task.CompletedTask ou Task.FromResult é que você pode perder erros que só aparecem quando o método é executado de forma assíncrona.
Joakim MH