web-dev-qa-db-fra.com

Task.Yield - des usages réels?

J'ai lu sur Task.Yield, et en tant que développeur Javascript, je peux dire que c'est son travail exactement la même chose que setTimeout(function (){...},0); en termes de laisser le seul thread 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 < int > 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 lui ouvrir une tâche?

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

Question

Avec Js, je ne peux pas ouvrir une tâche (oui je sais que je peux réellement avec les travailleurs web ). Mais avec c # je peux .

Si oui - 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:

De icienter image description here

De ici (un autre livre électronique):

enter image description here

36
Royi Namir

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 de manière 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 se produira non sur le même cadre de pile.

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 poursuite aura lieu lors d'une prochaine itération 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 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 tri-niveau. Tout les messages postés ont une priorité plus élevée que les messages d'entrée de l'utilisateur, car ils résident dans différentes files d'attente. Et tous les messages d'entrée d'utilisateur sont priorité plus élevée que les messages WM_Paint et WM_TIMER.

Ainsi, si vous utilisez await Task.Yield() pour céder la boucle de message en essayant de garder l'interface utilisateur réactive, vous courez réellement le 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 faites await Task.Yield() sur une boucle serrée, vous pouvez toujours bloquer l’UI.

C'est en quoi cela diffère de l'analogie setTimer de JavaScript que vous avez mentionnée dans la question. Un callback setTimer sera appelé after _ tous les messages entrés par l'utilisateur ont été traités par la pompe à messages du navigateur.

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

Par exemple, avec WPF, vous pourriez faire 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<bool>();
    // 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 en arrière-plan exécutée sur le thread d'interface utilisateur.

Pour un thread non-UI sans contexte de synchronisation, await Task.Yield() bascule simplement la continuation sur un thread de pool aléatoire. Il n'y a aucune garantie qu'il s'agisse d'un thread {différent} _ du thread actuel, il ne peut s'agir que d'une continuation asynchrone. Si ThreadPool est affamé, il peut programmer la suite sur le même thread.

Dans ASP.NET, exécuter await Task.Yield() n'a aucun sens, à l'exception de 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.

Alors, await Task.Yield() est-il utile? IMO, pas grand chose. Il peut être utilisé comme raccourci pour exécuter la poursuite via SynchronizationContext.Post ou ThreadPool.QueueUserWorkItem, si vous devez réellement imposer l'asynchronisme à une partie de votre méthode.

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

Mis à jour pour répondre au commentaire:

... comment peut-il s'agir d'une opération asynchrone et rester 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é l'é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 file d'attente avec WinFormsSynchronizationContext.Post, qui publie en interne un message Windows privé dans la boucle de messages 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é ci-dessus.

52
noseratio

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 résoudre un problème obscur ASP.NET dans lequel le code d'identité ne peut pas terminer de manière synchrone .
17
Stephen Cleary

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 étant laissée toujours comme setTimeout a toujours une pause minimale de quelques millisecondes, et le travail en attente de l'interface utilisateur est prioritaire sur les minuteries, mais await Task.Yield(); ne le fait pas.

Il n'y a aucune garantie que le rendement laissera un travail dans le thread principal, au contraire, le code qui appelle 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 Au travail d'entrée et de rendu. Pour cette raison, ne vous fiez pas à wait Task. Rendement (); pour garder une interface utilisateur sensible. "

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

6
Guffa

Tout d’abord, permettez-moi de préciser: Yield n’est pas exactement la même chose 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. Type 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 gouvernés par des machines à états. Donc, si vous omettez Yield de votre code, il ne bloquera que le thread et le tour est joué. Si vous en faites une tâche normale et appelez simplement start (ou un fil), il se contentera de faire son travail en parallèle et bloquera plus tard le fil appelant lorsque task.Result sera appelé. Qu'est-ce qui se passe quand vous faites 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'au premier Task.Yield et ensuite, il est autonome. Les appels ultérieurs à Task.Yield ne font apparemment rien.

Démonstration simple:

class MainClass
{
    //Just to reduce amont of log itmes
    static HashSet<Tuple<string, int>> cache = new HashSet<Tuple<string, int>>();
    public static void LogThread(string 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<int> 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, il sera asynchrone depuis le début et ne bloquera pas le thread appelant.

Conclusion: Task.Yield peut permettre de mélanger le code synchrone et asynchrone. Un scénario plus ou moins réaliste: vous avez une opération de calcul lourde, un cache local et une tâche CalcThing. Dans cette méthode, vous vérifiez si l’élément est dans le cache, si oui - renvoie l’élément, s’il n’y existe pas Yield et 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 celui-ci: "ne vous fiez pas à wait Task.Yield (); pour garder une interface utilisateur sensible ".

2
Andrey

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. Les durées d'exécution doivent donc ê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 cas, 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 tâches.

0
user743382

Je pense que personne n’a fourni la vraie réponse quand utiliser Task.Yield . Il est surtout nécessaire si une tâche utilise une boucle sans fin (ou un long travail synchrone) et peut potentiellement contenir un thread threadpool exclusivement tâches à utiliser ce fil). Cela peut arriver si, dans la boucle, le code est exécuté de manière synchrone. le Task.Yield replanifie la tâche dans la file d'attente du pool de threads et les autres tâches qui attendaient le thread peuvent être exécutées.

L'exemple:

  CancellationTokenSource cts;
  void Start()
  {
        cts = new CancellationTokenSource();

        // run async operation
        var task = Task.Run(() => SomeWork(cts.Token), cts.Token);
        // wait for completion
        // after the completion handle the result/ cancellation/ errors
    }

    async Task<int> SomeWork(CancellationToken cancellationToken)
    {
        int result = 0;

        bool loopAgain = true;
        while (loopAgain)
        {
            // do something ... means a substantial work or a micro batch here - not processing a single byte

            loopAgain = /* check for loop end && */  cancellationToken.IsCancellationRequested;
            if (loopAgain) {
                // reschedule  the task to the threadpool and free this thread for other waiting tasks
                await Task.Yield();
            }
        }
        cancellationToken.ThrowIfCancellationRequested();
        return result;
    }

    void Cancel()
    {
        // request cancelation
        cts.Cancel();
    }
0
Maxim T