Quand utiliserais-je Task.Yield ()?

219

J'utilise async / wait et Taskbeaucoup mais je n'ai jamais utilisé Task.Yield()et pour être honnête, même avec toutes les explications, je ne comprends pas pourquoi j'aurais besoin de cette méthode.

Quelqu'un peut-il donner un bon exemple où cela Yield()est nécessaire?

Krumelur
la source

Réponses:

240

Lorsque vous utilisez async/ await, il n'y a aucune garantie que la méthode que vous appelez lorsque vous le await FooAsync()ferez s'exécutera réellement de manière asynchrone. L'implémentation interne est libre de revenir en utilisant un chemin complètement synchrone.

Si vous créez une API où il est essentiel de ne pas bloquer et d'exécuter du code de manière asynchrone, et qu'il y a une chance que la méthode appelée s'exécute de manière synchrone (blocage efficace), l'utilisation await Task.Yield()force votre méthode à être asynchrone et retourne contrôle à ce point. Le reste du code s'exécutera ultérieurement (à ce moment, il peut toujours s'exécuter de manière synchrone) sur le contexte actuel.

Cela peut également être utile si vous créez une méthode asynchrone qui nécessite une initialisation «longue», c'est-à-dire:

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

Sans l' Task.Yield()appel, la méthode s'exécutera de manière synchrone jusqu'au premier appel à await.

Reed Copsey
la source
28
J'ai l'impression de mal interpréter quelque chose ici. Si await Task.Yield()force la méthode à être asynchrone, pourquoi prendrions-nous la peine d'écrire du "vrai" code asynchrone? Imaginez une méthode de synchronisation lourde. Pour le rendre asynchrone, il suffit d'ajouter asyncet await Task.Yield()au début et comme par magie, ce sera asynchrone? Ce serait un peu comme encapsuler tout le code de synchronisation Task.Run()et créer une fausse méthode asynchrone.
Krumelur
14
@Krumelur Il y a une grande différence - regardez mon exemple. Si vous utilisez un Task.Runpour l'implémenter, ExecuteFooOnUIThreads'exécutera sur le pool de threads, pas sur le thread d'interface utilisateur. Avec await Task.Yield(), vous le forcez à être asynchrone de manière à ce que le code suivant soit toujours exécuté sur le contexte actuel (juste à un moment ultérieur). Ce n'est pas quelque chose que vous feriez normalement, mais c'est bien qu'il y ait l'option si c'est nécessaire pour une raison étrange.
Reed Copsey
7
Une autre question: si elle ExecuteFooOnUIThread()était très longue, elle bloquerait encore le thread d'interface utilisateur pendant un certain temps et rendrait l'interface utilisateur non réactive, est-ce correct?
Krumelur
7
@Krumelur Oui, ce serait le cas. Mais pas immédiatement - cela arriverait plus tard.
Reed Copsey
34
Bien que cette réponse soit techniquement correcte, l'affirmation selon laquelle «le reste du code s'exécutera ultérieurement» est trop abstraite et peut être trompeuse. Le calendrier d'exécution du code après Task.Yield () dépend beaucoup du SynchronisationContext concret. Et la documentation MSDN indique clairement que «Le contexte de synchronisation qui est présent sur un thread d'interface utilisateur dans la plupart des environnements d'interface utilisateur donnera souvent la priorité au travail publié dans le contexte supérieur au travail d'entrée et de rendu. Pour cette raison, ne vous fiez pas à attendre Task.Yield () ; pour garder une interface utilisateur réactive. "
Vitaliy Tsvayer
36

En interne, met await Task.Yield()simplement la suite en file d'attente sur le contexte de synchronisation actuel ou sur un thread de pool aléatoire, si tel SynchronizationContext.Currentest le cas null.

Il est efficacement mis en œuvre comme serveur personnalisé. Un code moins efficace produisant l'effet identique pourrait être aussi simple que cela:

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()peut être utilisé comme raccourci pour certaines modifications de flux d'exécution étranges. Par exemple:

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  
}

Cela dit, je ne peux penser à aucun cas où Task.Yield()ne peut pas être remplacé par un Task.Factory.StartNewbon planificateur de tâches.

Voir également:

noseratio
la source
Dans votre exemple, quelle est la différence entre ce qui est là et var dialogTask = await showAsync();?
Erik Philips
@ErikPhilips, var dialogTask = await showAsync()ne compilera pas car l' await showAsync()expression ne renvoie pas de Task(contrairement à ce qui se passe sans await). Cela dit, si vous le faites await showAsync(), l'exécution ne reprendra qu'après la fermeture de la boîte de dialogue, c'est différent. C'est parce que window.ShowDialogc'est une API synchrone (malgré qu'elle pompe toujours des messages). Dans ce code, je voulais continuer pendant que la boîte de dialogue est toujours affichée.
noseratio
5

Une utilisation de Task.Yield()est d'empêcher un débordement de pile lors de la récursion asynchrone. Task.Yield()empêche la poursuite syncrone. Notez cependant que cela peut provoquer une exception OutOfMemory (comme l'a noté Triynko). La récursion sans fin n'est toujours pas sûre et vous feriez probablement mieux de réécrire la récursivité en boucle.

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
la source
4
Cela peut empêcher un débordement de pile, mais cela finira par manquer de mémoire système si vous le laissez s'exécuter suffisamment longtemps. Chaque itération créerait une nouvelle tâche qui ne se termine jamais, car la tâche extérieure attend une tâche intérieure, qui attend encore une autre tâche intérieure, et ainsi de suite. Ce n'est pas OK. Alternativement, vous pouvez simplement avoir une tâche la plus externe qui ne se termine jamais et la faire simplement boucler au lieu de recurse. La tâche ne serait jamais terminée, mais il n'y en aurait qu'un seul. À l'intérieur de la boucle, il pourrait céder ou attendre tout ce que vous souhaitez.
Triynko
Je ne peux pas reproduire le débordement de pile. Il semble que le await Task.Delay(1)soit suffisant pour l'empêcher. (Application console, .NET Core 3.1, C # 8)
Theodor Zoulias
-8

Task.Yield() peut être utilisé dans des implémentations simulées de méthodes asynchrones.

mhsirig
la source
4
Vous devez fournir quelques détails.
PJProudhon
3
À cette fin, je préfère utiliser Task.CompletedTask - voir la section Task.CompletedTask dans ce billet de blog msdn pour plus de considérations.
Grzegorz Smulko
2
Le problème avec l'utilisation de Task.CompletedTask ou Task.FromResult est que vous pouvez manquer des bogues qui n'apparaissent que lorsque la méthode s'exécute de manière asynchrone.
Joakim MH