Quando dovrei usare Task.Yield ()?


219

Sto usando asincrono / wait e Taskmolto, ma non ho mai usato Task.Yield()e ad essere sincero anche con tutte le spiegazioni non capisco perché avrei bisogno di questo metodo.

Qualcuno può dare un buon esempio dove Yield()è richiesto?

Risposte:


240

Quando si utilizza async/ await, non vi è alcuna garanzia che il metodo che si chiama quando lo si esegue await FooAsync()effettivamente verrà eseguito in modo asincrono. L'implementazione interna può essere restituita gratuitamente utilizzando un percorso completamente sincrono.

Se stai creando un'API in cui è fondamentale che non blocchi e esegui un po 'di codice in modo asincrono e c'è una possibilità che il metodo chiamato venga eseguito in modo sincrono (blocco efficace), l'utilizzo await Task.Yield()forzerà il tuo metodo ad essere asincrono e restituirà controllo a quel punto. Il resto del codice verrà eseguito in un secondo momento (a quel punto, potrebbe ancora essere eseguito in modo sincrono) sul contesto corrente.

Ciò può essere utile anche se si esegue un metodo asincrono che richiede un'inizializzazione "long running", ovvero:

 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);
 }

Senza la Task.Yield()chiamata, il metodo verrà eseguito in modo sincrono fino alla prima chiamata a await.


28
Mi sento come se stessi interpretando male qualcosa qui. Se await Task.Yield()impone che il metodo sia asincrono, perché dovremmo preoccuparci di scrivere un codice asincrono "reale"? Immagina un metodo di sincronizzazione pesante. Per renderlo asincrono, basta aggiungere asynce await Task.Yield()all'inizio e magicamente, sarà asincrono? Sarebbe praticamente come racchiudere tutto il codice di sincronizzazione Task.Run()e creare un metodo asincrono falso.
Krumelur,

14
@Krumelur C'è una grande differenza: guarda il mio esempio. Se si utilizza a Task.Runper implementarlo, ExecuteFooOnUIThreadverrà eseguito sul pool di thread, non sul thread dell'interfaccia utente. Con await Task.Yield(), lo forzate ad essere asincrono in modo che il codice successivo sia ancora eseguito sul contesto corrente (solo in un momento successivo). Non è qualcosa che faresti normalmente, ma è bello che ci sia l'opzione se è richiesta per qualche strano motivo.
Reed Copsey,

7
Un'altra domanda: se ExecuteFooOnUIThread()durasse molto a lungo, bloccherebbe comunque a lungo il thread dell'interfaccia utente e renderebbe l'IU non rispondente, è corretto?
Krumelur,

7
@Krumelur Sì, lo sarebbe. Solo non immediatamente - sarebbe successo in un secondo momento.
Reed Copsey,

34
Sebbene questa risposta sia tecnicamente corretta, l'affermazione secondo cui "il resto del codice verrà eseguito in un secondo momento" è troppo astratta e potrebbe essere fuorviante. La pianificazione dell'esecuzione del codice dopo Task.Yield () dipende in larga misura da SynchronisationContext concreto. E la documentazione MSDN afferma chiaramente che "Il contesto di sincronizzazione che è presente su un thread dell'interfaccia utente nella maggior parte degli ambienti dell'interfaccia utente spesso darà la priorità al lavoro pubblicato nel contesto più in alto rispetto all'input e al rendering del lavoro. Per questo motivo, non fare affidamento su waitit Task.Yield () ; per mantenere reattiva un'interfaccia utente. "
Vitaliy Tsvayer,

36

Internamente, await Task.Yield()semplicemente accoda la continuazione sul contesto di sincronizzazione corrente o su un thread di pool casuale, se lo SynchronizationContext.Currentè null.

È implementato in modo efficiente come cameriere personalizzato. Un codice meno efficiente che produce l'effetto identico potrebbe essere semplice come questo:

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()può essere usato come scorciatoia per alcune strane alterazioni del flusso di esecuzione. Per esempio:

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  
}

Detto questo, non riesco a pensare ad alcun caso in cui Task.Yield()non possa essere sostituito con Task.Factory.StartNewun pianificatore di attività adeguato.

Guarda anche:


Nel tuo esempio, qual è la differenza tra cosa c'è e var dialogTask = await showAsync();?
Erik Philips

@ErikPhilips, var dialogTask = await showAsync()non verrà compilato perché l' await showAsync()espressione non restituisce un Task(diversamente da come fa senza await). Detto questo, se lo fai await showAsync(), l'esecuzione dopo verrà ripresa solo dopo la chiusura della finestra di dialogo, ecco come è diversa. Questo perché window.ShowDialogè un'API sincrona (nonostante continui a pompare messaggi). In quel codice, volevo continuare mentre la finestra di dialogo è ancora visualizzata.
noseratio,

5

Un uso di Task.Yield()è per evitare un overflow dello stack durante la ricorsione asincrona. Task.Yield()impedisce la continuazione sincrona. Si noti, tuttavia, che ciò può causare un'eccezione OutOfMemory (come notato da Triynko). La ricorsione senza fine non è ancora sicura e probabilmente stai meglio riscrivendo la ricorsione come un ciclo.

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();
    }

4
Ciò potrebbe impedire un overflow dello stack, ma ciò finirà per esaurire la memoria di sistema se lo si lascia funzionare abbastanza a lungo. Ogni iterazione creerebbe un nuovo Task che non si completa mai, poiché il Task esterno è in attesa di un Task interno, che è in attesa di un altro Task interno e così via. Questo non va bene. In alternativa, potresti semplicemente avere un'attività più esterna che non viene mai completata e semplicemente eseguirla in loop anziché ricorrere. Il compito non sarebbe mai completo, ma ce ne sarebbe solo uno. All'interno del ciclo, potrebbe cedere o attendere qualsiasi cosa tu voglia.
Triynko,

Non riesco a riprodurre l'overflow dello stack. Sembra che await Task.Delay(1)sia abbastanza per impedirlo. (App console, .NET Core 3.1, C # 8)
Theodor Zoulias

-8

Task.Yield() può essere utilizzato in simulazioni di metodi asincroni.


4
Dovresti fornire alcuni dettagli.
PJProudhon,

3
A tale scopo, preferirei utilizzare Task.CompletedTask - vedere la sezione Task.CompletedTask in questo post di blog msdn per ulteriori considerazioni.
Grzegorz Smulko,

2
Il problema con l'utilizzo di Task.CompletedTask o Task.FromResult è che potresti perdere bug che compaiono solo quando il metodo viene eseguito in modo asincrono.
Joakim MH,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.