“Wait Task.Yield ()” et ses alternatives

Si je dois différer l’exécution du code après une itération future de la boucle de messages du thread d’interface utilisateur, je peux le faire à peu près de la manière suivante:

await Task.Factory.StartNew( () => { MessageBox.Show("Hello!"); }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()); 

Ce serait similaire à await Task.Yield(); MessageBox.Show("Hello!"); await Task.Yield(); MessageBox.Show("Hello!"); De plus, j’aurais la possibilité d’annuler la tâche si je le voulais.

Dans le cas du contexte de synchronisation par défaut, je pourrais également utiliser await Task.Run pour continuer sur un thread de pool.

En fait, j’aime bien Task.Factory.StartNew et Task.Run plus que Task.Yield , car ils définissent tous deux explicitement la scope du code de continuation.

Alors, dans quelles situations await Task.Yield() est réellement utile?

Considérez le cas où vous souhaitez que votre tâche asynchrone retourne une valeur.

Méthode synchrone existante:

 public int DoSomething() { return SomeMethodThatReturnsAnInt(); } 

Pour rendre asynchrone, ajoutez un mot clé async et changez le type de retour:

 public async Task DoSomething() 

Pour utiliser Task.Factory.StartNew (), remplacez le corps d’une ligne de la méthode par:

 // start new task var task = Task.Factory.StartNew( () => { return SomeMethodThatReturnsAnInt(); }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext() ); // await task, return control to calling method await task; // return task result return task.Result; 

vs append une seule ligne si vous utilisez await Task.Yield()

 // this returns control to the calling method await Task.Yield(); // otherwise synchronous method scheduled for async execution by the // TaskScheduler of the calling thread return SomeMethodThatReturnsAnInt(); 

Ce dernier est beaucoup plus concis, lisible et ne change pas beaucoup la méthode existante.

Task.Yield() est idéal pour “percer un trou” dans une partie par ailleurs synchrone d’une méthode async .

Personnellement, je l’ai trouvé utile dans les cas où j’ai une méthode async annulation automatique (une méthode qui gère son propre CancellationTokenSource correspondante et annule l’instance créée précédemment à chaque appel suivant) et qui peut être appelée plusieurs fois dans un délai extrêmement court ( c’est-à-dire par des gestionnaires d’événements d’éléments d’interface utilisateur interdépendants). Dans une telle situation, l’utilisation de Task.Yield() suivie d’un contrôle IsCancellationRequested dès que le CancellationTokenSource est échangé peut empêcher le travail potentiellement coûteux, dont les résultats seront de toute façon ignorés.

Voici un exemple où seul le dernier appel en queue de SelfCancellingAsync peut effectuer un travail coûteux et aboutir.

 using System; using System.Threading; using System.Threading.Tasks; namespace TaskYieldExample { class Program { private static CancellationTokenSource CancellationTokenSource; static void Main(ssortingng[] args) { SelfCancellingAsync(); SelfCancellingAsync(); SelfCancellingAsync(); Console.ReadLine(); } private static async void SelfCancellingAsync() { Console.WriteLine("SelfCancellingAsync starting."); var cts = new CancellationTokenSource(); var oldCts = Interlocked.Exchange(ref CancellationTokenSource, cts); if (oldCts != null) { oldCts.Cancel(); } // Allow quick cancellation. await Task.Yield(); if (cts.IsCancellationRequested) { return; } // Do the "meaty" work. Console.WriteLine("Performing intensive work."); var answer = await Task .Delay(TimeSpan.FromSeconds(1)) .ContinueWith(_ => 42, TaskContinuationOptions.ExecuteSynchronously); if (cts.IsCancellationRequested) { return; } // Do something with the result. Console.WriteLine("SelfCancellingAsync completed. Answer: {0}.", answer); } } } 

Le but ici est d’autoriser le code qui s’exécute de manière synchrone sur le même SynchronizationContext immédiatement après le retour de l’appel non attendu de la méthode async (lorsqu’il atteint son premier await ) de changer l’état qui affecte l’exécution de la méthode async. Cette limitation est similaire à celle obtenue par Task.Delay (je parle ici d’un délai non nul), mais sans le retard réel et potentiellement perceptible, ce qui peut être indésirable dans certaines situations.

Une situation dans laquelle Task.Yield() est réellement utile est lorsque vous await une Task appelée de manière récursive et terminée de manière synchrone . Parce que async / wait de csharp await «publication de Zalgo» en exécutant les continuations de manière synchrone, la stack dans un scénario de récursion entièrement synchrone peut devenir assez volumineuse pour que votre processus se termine. Je pense que cela est également dû en partie au fait que les appels en queue ne peuvent pas être pris en charge à cause de l’indirection de Task . await Task.Yield() planifie la poursuite de l’exécution du planificateur par le planificateur plutôt qu’en ligne, ce qui permet d’éviter la croissance de la stack et de résoudre ce problème.

De plus, Task.Yield() peut être utilisé pour couper la partie synchrone d’une méthode. Si l’appelant doit recevoir la Task votre méthode avant que celle-ci Task.Yield() une action, vous pouvez utiliser Task.Yield() pour forcer le renvoi de la Task plus tôt que Task.Yield() . Par exemple, dans le scénario de méthode locale suivant, la méthode async peut obtenir une référence à sa propre Task toute sécurité (en supposant que vous l’ AsyncContext.Run() sur un SynchronizationContext à une seule concurrence, tel que dans winforms ou via AsyncContext.Run() ) de nito :

 using Nito.AsyncEx; using System; using System.Threading.Tasks; class Program { // Use a single-threaded SynchronizationContext similar to winforms/WPF static void Main(ssortingng[] args) => AsyncContext.Run(() => RunAsync()); static async Task RunAsync() { Task task = null; task = getOwnTaskAsync(); var foundTask = await task; Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}"); async Task getOwnTaskAsync() { // Cause this method to return and let the 「task」 local be assigned. await Task.Yield(); return task; } } } 

sortie:

 3 == 3: True 

Je suis désolé de ne pouvoir concevoir aucun scénario réel dans lequel le meilleur moyen de faire quelque chose consiste à couper de force la partie synchrone d’une méthode async . Savoir que vous pouvez faire un tour, comme je viens de le montrer, peut être utile parfois, mais il a aussi tendance à être plus dangereux. Vous pouvez souvent transmettre des données de manière plus lisible et plus sécurisée pour les threads. Par exemple, vous pouvez transmettre à la méthode locale une référence à sa propre Task aide de TaskCompletionSource :

 using System; using System.Threading.Tasks; class Program { // Fully free-threaded! Works in more environments! static void Main(ssortingng[] args) => RunAsync().Wait(); static async Task RunAsync() { var ownTaskSource = new TaskCompletionSource(); var task = getOwnTaskAsync(ownTaskSource.Task); ownTaskSource.SetResult(task); var foundTask = await task; Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}"); async Task getOwnTaskAsync( Task ownTaskTask) { // This might be clearer. return await ownTaskTask; } } } 

sortie:

 2 == 2: True 

Task.Yield n’est pas une alternative à Task.Factory.StartNew ou Task.Run . Ils sont totalement différents. Lorsque vous await Task.Yield vous autorisez l’ Task.Yield d’un autre code sur le thread actuel sans le bloquer. Pensez-y comme si vous Task.Yield Task.Delay , excepté Task.Yield attend que les tâches soient terminées, plutôt que d’attendre une heure spécifique.

Remarque: N’utilisez pas Task.Yield sur le thread d’interface utilisateur et supposez que l’interface utilisateur restra toujours réactive. Ce n’est pas toujours le cas.