Task.Yield – des usages réels?

J’ai lu sur Task.Yield , et en tant que développeur Javascript, je peux dire que son travail est exactement le même que setTimeout(function (){...},0); en termes de laisser le fil unique principal traiter avec d’autres trucs aka:

“Ne prenez pas tout le pouvoir, libérez-vous du temps – pour que d’autres en aient aussi …”

En js cela fonctionne particulièrement dans les longues boucles. ( ne faites pas geler le navigateur … )

Mais j’ai vu cet exemple ici :

 public static async Task  FindSeriesSum(int i1) { int sum = 0; for (int i = 0; i < i1; i++) { sum += i; if (i % 1000 == 0) ( after a bulk , release power to main thread) await Task.Yield(); } return sum; } 

En tant que programmeur JS, je peux comprendre ce qu’ils ont fait ici.

MAIS en tant que programmeur C #, je me pose la question suivante: pourquoi ne pas ouvrir une tâche à cette tâche?

  public static async Task  FindSeriesSum(int i1) { //do something.... return await MyLongCalculationTask(); //do something } 

Question

Avec Js, je ne peux pas ouvrir de tâche ( oui, je sais que je peux le faire avec des travailleurs Web ). Mais avec c # je peux .

Si c’est le cas – pourquoi même s’embarrasser de relâcher de temps en temps pendant que je peux le relâcher?

modifier

Ajout de références:

À partir d’ ici : entrez la description de l'image ici

De là (un autre ebook):

entrez la description de l'image ici

Quand tu vois:

 await Task.Yield(); 

vous pouvez penser de cette façon:

 await Task.Factory.StartNew( () => {}, CancellationToken.None, TaskCreationOptions.None, SynchronizationContext.Current != null? TaskScheduler.FromCurrentSynchronizationContext(): TaskScheduler.Current); 

Tout cela garantit que la suite se produira de manière asynchrone dans le futur. Par asynchrone, je veux dire que le contrôle d’exécution retournera à l’appelant de la méthode async et que le rappel de continuation ne se produira pas sur le même cadre de stack.

Quand exactement et sur quel thread cela va se produire dépend complètement du contexte de synchronisation du thread appelant.

Pour un thread d’interface utilisateur , la continuation se produira lors d’une itération future de la boucle de message, exécutée par Application.Run ( WinForms ) ou Dispatcher.Run ( WPF ). En interne, cela revient à l’API Win32 PostMessage , qui poste un message personnalisé dans la file de messages du thread d’interface utilisateur. Le rappel de continuation en await sera appelé lorsque ce message sera pompé et traité. Vous êtes complètement hors de contrôle à propos de quand exactement cela va se produire.

En outre, Windows a ses propres priorités pour le pompage des messages: INFO: Priorités des messages de fenêtre . La partie la plus pertinente:

Dans ce schéma, la priorisation peut être considérée comme un sorting-niveau. Tous les messages postés ont une priorité plus élevée que les messages d’entrée de l’utilisateur car ils résident dans des files d’attente différentes. Et tous les messages d’entrée d’utilisateur ont une priorité plus élevée que les messages WM_PAINT et WM_TIMER.

Ainsi, si vous utilisez await Task.Yield() pour await Task.Yield() la boucle de message afin que l’interface utilisateur rest sensible, vous await Task.Yield() risque d’obstruer la boucle de message du thread d’interface utilisateur. Certains messages d’entrée utilisateur en attente, ainsi que WM_PAINT et WM_TIMER , ont une priorité inférieure à celle du message de continuation publié. Ainsi, si vous await Task.Yield() sur une boucle étroite, vous pouvez toujours bloquer l’interface utilisateur.

C’est en quoi cela diffère de l’analogie setTimer de JavaScript que vous avez mentionnée dans la question. Un rappel setTimer sera appelé une fois que tous les messages d’entrée de l’utilisateur auront été traités par la pompe à messages du navigateur.

Donc, await Task.Yield() n’est pas bon pour faire du travail de fond sur le thread d’interface utilisateur. En fait, il est très rarement nécessaire d’exécuter un processus d’arrière-plan sur le thread d’interface utilisateur, mais parfois, par exemple, la coloration syntaxique de l’éditeur, la vérification orthographique, etc. Dans ce cas, utilisez l’infrastructure inactive du framework.

Par exemple, avec WPF, vous pouvez await Dispatcher.Yield(DispatcherPriority.ApplicationIdle) :

 async Task DoUIThreadWorkAsync(CancellationToken token) { var i = 0; while (true) { token.ThrowIfCancellationRequested(); await Dispatcher.Yield(DispatcherPriority.ApplicationIdle); // do the UI-related work item this.TextBlock.Text = "iteration " + i++; } } 

Pour WinForms, vous pouvez utiliser l’événement Application.Idle :

 // await IdleYield(); public static Task IdleYield() { var idleTcs = new TaskCompletionSource(); // subscribe to Application.Idle EventHandler handler = null; handler = (s, e) => { Application.Idle -= handler; idleTcs.SetResult(true); }; Application.Idle += handler; return idleTcs.Task; } 

Il est recommandé de ne pas dépasser 50 ms pour chaque itération d’une telle opération d’arrière-plan exécutée sur le thread d’interface utilisateur.

Pour un thread non-UI sans contexte de synchronisation, await Task.Yield() commute simplement la continuation sur un thread de pool aléatoire. Il n’y a aucune garantie que ce sera un thread différent du thread actuel, c’est seulement une continuation asynchrone . Si ThreadPool est affamé, il peut planifier la suite sur le même thread.

await Task.Yield() dans ASP.NET n’a aucun sens, sauf pour la solution de contournement mentionnée dans la réponse de @ StephenCleary . Sinon, les performances de l’application Web ne seront affectées que par un commutateur de thread redondant.

Est-ce await Task.Yield() utile? OMI, pas grand chose. Il peut être utilisé comme raccourci pour exécuter la suite via SynchronizationContext.Post ou ThreadPool.QueueUserWorkItem , si vous devez réellement imposer l’asynchronie à une partie de votre méthode.

En ce qui concerne les livres que vous avez cités , à mon avis, ces Task.Yield utilisation de Task.Yield sont fausses. J’ai expliqué pourquoi ils ont tort pour un thread d’interface utilisateur, ci-dessus. Pour un thread de pool non-UI, il n’y a tout simplement pas “d’autres tâches dans le thread à exécuter” , sauf si vous exécutez une pompe de tâches personnalisée comme AsyncPump Stephen Toub .

Mis à jour pour répondre au commentaire:

… comment cela peut-il être une opération asynchrone et restr dans le même thread? ..

Comme exemple simple: application WinForms:

 async void Form_Load(object s, object e) { await Task.Yield(); MessageBox.Show("Async message!"); } 

Form_Load retournera à l’appelant (le code de structure WinFroms qui a déclenché Load événement Load ), puis la boîte de message s’affichera de manière asynchrone lors d’une prochaine itération de la boucle de message exécutée par Application.Run() . Le rappel de continuation est mis en queue avec WinFormsSynchronizationContext.Post , qui publie en interne un message Windows privé dans la boucle de message du thread d’interface utilisateur. Le rappel sera exécuté lorsque ce message sera pompé, toujours sur le même fil.

Dans une application de console, vous pouvez exécuter une boucle de sérialisation similaire avec AsyncPump mentionnée ci-dessus.

J’ai seulement trouvé Task.Yield utile dans deux scénarios:

  1. Tests unitaires pour vérifier que le code testé fonctionne correctement en présence d’asynchronisme.
  2. Pour contourner un problème ASP.NET obscur dans lequel le code d’identité ne peut pas se terminer de manière synchrone .

Non, ce n’est pas exactement comme utiliser setTimeout pour rendre le contrôle à l’interface utilisateur. En Javascript, la mise à jour de l’interface utilisateur en tant que setTimeout toujours une pause minimale de quelques millisecondes. Le travail en attente de l’interface utilisateur est prioritaire sur les timers, mais await Task.Yield(); ne fait pas ça.

Il n’y a aucune garantie que le rendement laissera un travail dans le thread principal, bien au contraire, le code appelé le rendement sera souvent prioritaire par rapport au travail de l’interface utilisateur.

“Le contexte de synchronisation présent sur un thread d’interface utilisateur dans la plupart des environnements d’interface utilisateur donne souvent la priorité au travail posté dans le contexte supérieur au travail d’entrée et de rendu. Pour cette raison, n’utilisez pas Task.Yield (); attendez que l’interface utilisateur soit réactive. . ”

Réf: MSDN: méthode Task.Yield

Tout d’abord, permettez-moi de préciser: le Yield n’est pas exactement le même que setTimeout(function (){...},0); . JS est exécuté dans un environnement à thread unique, c’est donc le seul moyen de laisser d’autres activités se dérouler. Genre de multitâche coopératif . .net est exécuté dans un environnement multitâche préemptif avec multithreading explicite.

Revenons maintenant à Thread.Yield . Comme je l’ai dit, .net vit dans un monde préemptif, mais c’est un peu plus compliqué que cela. C # await/async crée un mélange intéressant de ces modes multitâches dirigés par des machines à états. Donc, si vous omettez Yield de votre code, il ne bloquera que le fil et le tour est joué. Si vous en faites une tâche normale et que vous appelez simplement start (ou un fil), il se contentera de faire son travail en parallèle et bloquera plus tard le thread appelant lorsque task.Result sera appelé. Que se passe-t-il lorsque vous await Task.Yield(); est plus compliqué. Logiquement, cela débloque le code appelant (similaire à JS) et l’exécution se poursuit. Ce qu’il fait réellement – il sélectionne un autre thread et continue son exécution dans un environnement préemptif avec un thread appelant. Donc, il est dans l’appel du thread jusqu’à la première Task.Yield et ensuite, il est autonome. Les appels suivants à Task.Yield ne font apparemment rien.

Démonstration simple:

 class MainClass { //Just to reduce amont of log itmes static HashSet> cache = new HashSet>(); public static void LogThread(ssortingng msg, bool clear=false) { if (clear) cache.Clear (); var val = Tuple.Create(msg, Thread.CurrentThread.ManagedThreadId); if (cache.Add (val)) Console.WriteLine ("{0}\t:{1}", val.Item1, val.Item2); } public static async Task FindSeriesSum(int i1) { LogThread ("Task enter"); int sum = 0; for (int i = 0; i < i1; i++) { sum += i; if (i % 1000 == 0) { LogThread ("Before yield"); await Task.Yield (); LogThread ("After yield"); } } LogThread ("Task done"); return sum; } public static void Main (string[] args) { LogThread ("Before task"); var task = FindSeriesSum(1000000); LogThread ("While task", true); Console.WriteLine ("Sum = {0}", task.Result); LogThread ("After task"); } } 

Voici les résultats:

 Before task :1 Task enter :1 Before yield :1 After yield :5 Before yield :5 While task :1 Before yield :5 After yield :5 Task done :5 Sum = 1783293664 After task :1 
  • Sortie produite sur mono 4.5 sur Mac OS X, les résultats peuvent varier sur d’autres configurations

Si vous déplacez Task.Yield en haut de la méthode, elle sera asynchrone depuis le début et ne bloquera pas le thread appelant.

Conclusion: Task.Yield peut permettre de mélanger du code synchrone et asynchrone. Un scénario plus ou moins réaliste: vous avez une opération de calcul lourde, une mémoire cache locale et la tâche CalcThing . Dans cette méthode, vous vérifiez si l'élément est en cache, si oui - renvoie l'élément, s'il n'y est pas Yield puis procédez au calcul. En fait, l'échantillon de votre livre n'a pas de sens car rien d'utile n'y est réalisé. Leur remarque concernant l'interactivité de l'interface graphique est tout simplement mauvaise et incorrecte (le thread d'interface utilisateur sera verrouillé jusqu'au premier appel à Yield , vous ne devriez jamais le faire, MSDN est clair (et correct) sur ce point: "ne vous fiez pas à wait Task.Yield (); garder une interface utilisateur sensible ".

Vous supposez que la fonction de longue durée est une fonction pouvant s’exécuter sur un thread d’arrière-plan. Si ce n’est pas le cas, par exemple, en raison de son interaction avec l’interface utilisateur, il n’existe aucun moyen d’empêcher le blocage de l’interface utilisateur pendant son exécution. Par conséquent, les durées d’exécution doivent être suffisamment courtes pour ne pas causer de problèmes aux utilisateurs.

Une autre possibilité est que vos fonctions soient plus longues que vos threads d’arrière-plan. Dans ce scénario, il peut être préférable (ou cela n’a pas d’importance, cela dépend) d’empêcher certaines de ces fonctions de prendre toutes vos discussions.