105 votes

Comment céder et attendre mettre en œuvre le flux de contrôle dans .NET?

Comme je comprends l' yield mot-clé, si utilisé à l'intérieur d'un bloc itérateur, il renvoie le flux de contrôle dans le code d'appel, et lorsque l'itérateur est appelée de nouveau, il reprend là où il l'avait laissé.

Aussi, await non seulement attend pour le destinataire de l'appel, mais elle retourne le contrôle à l'appelant, pour reprendre là où il a laissé lorsque l'appelant awaits la méthode.

En d'autres mots, il n'y a pas de fil, et la "simultanéité" de async et await est une illusion causée par des intelligent de flux de contrôle, dont les détails sont cachés par la syntaxe.

Maintenant, je suis un ancien de l'assemblée programmeur et je suis très familier avec l'instruction des pointeurs, des piles, etc. et je reçois la façon normale dont les flux de contrôle (sous-routine, la récursivité, les boucles, les branches). Mais ces nouvelles constructions-- je ne les recevez pas.

Lorsqu'un await est atteint, comment fonctionne le moteur d'exécution de savoir ce morceau de code doit s'exécuter prochaine? Comment savoir quand il peut la reprendre là où il l'a laissée, et comment faut-il rappeler où? Qu'advient-il de la pile d'appel en cours, fait-il sauvé en quelque sorte? Que faire si l'appel de la méthode rend les autres appels de méthode avant d' awaits-- pourquoi ne pas la pile écrasés? Et comment sur terre serait le moteur d'exécution travailler son chemin à travers tout cela, dans le cas d'une exception et une pile de vous détendre?

Lors de l' yield est atteint, comment fonctionne le moteur d'exécution de garder la trace du point où les choses doivent être ramassés? Comment est itérateur de l'état est-elle préservée?

113voto

Eric Lippert Points 300275

Je vais répondre à vos questions ci-dessous, mais vous faites bien de simplement lire mes nombreux articles sur la façon dont nous avons conçu le rendement et l'attendent.

https://blogs.msdn.microsoft.com/ericlippert/tag/continuation-passing-style/

https://blogs.msdn.microsoft.com/ericlippert/tag/iterators/

https://blogs.msdn.microsoft.com/ericlippert/tag/async/

Certains de ces articles ne sont pas à jour, le code généré est différent dans beaucoup de façons. Mais ce sera certainement vous donner une idée de comment cela fonctionne.

Aussi, si vous ne comprenez pas comment les lambdas sont générés de fermeture de classes, de comprendre que la première. Vous ne voulez pas faire des têtes ou queues de async si vous n'avez pas lambdas vers le bas.

Lorsqu'une attendent est atteint, comment fonctionne le moteur d'exécution de savoir ce morceau de code doit s'exécuter prochaine?

await est généré sous la forme:

if (the task is not completed)
  assign a delegate which executes the remainder of the method as the continuation of the task
  return to the caller
else
  execute the remainder of the method now

C'est principalement ça. Attendent est juste une fantaisie de retour.

Comment savoir quand il peut la reprendre là où il l'a laissée, et comment faut-il rappeler où?

Eh bien, comment faites-vous sans attendre? Lorsque la méthode foo appels de méthode bar, d'une certaine manière, nous nous souvenons comment obtenir de nouveau au milieu de foo, avec tous les habitants de l'activation de foo intact, n'importe quel bar.

Vous savez comment c'est fait en assembleur. Une activation de l'enregistrement pour les foo est poussé sur la pile; il contient les valeurs de la population locale. Au moment de l'appel, l'adresse de retour dans foo est poussé sur la pile. Lorsque la barre est fait, le pointeur de pile et le pointeur d'instruction sont remis à l'endroit où ils doivent être et toto continue à aller de l'endroit où il l'avait laissé.

La poursuite de l'attendent exactement la même, sauf que l'enregistrement est mis sur le tas, pour la raison évidente que la séquence d'activations ne forme pas une pile.

Le délégué qui attendent donne comme la continuation de la tâche contient (1) un nombre qui est l'entrée dans une table de recherche qui donne le pointeur d'instruction que vous avez besoin pour exécuter prochaine, et (2) toutes les valeurs de locaux et temporaires.

Il y a quelques autres engins de là; par exemple, dans .NET, il est illégal de direction dans le milieu d'un bloc try, de sorte que vous ne pouvez pas simplement coller l'adresse de code à l'intérieur d'un bloc try dans la table. Mais ce sont de comptabilité de détails. Sur le plan conceptuel, l'activation de l'enregistrement est simplement déplacé sur le tas.

Qu'advient-il de la pile d'appel en cours, fait-il sauvé en quelque sorte?

Les informations pertinentes dans le courant de l'activation n'est jamais mis sur la pile en premier lieu; il est alloué au large de la tas à partir de l'obtenir-aller. (Eh bien, les paramètres formels sont passés sur la pile ou dans les registres normalement et ensuite copié dans un segment de mémoire lorsque l'emplacement de la méthode commence.)

L'activation des dossiers d'appelants ne sont pas stockées; await va probablement revenir à eux, rappelez-vous, donc ils vont être traitées normalement.

Notez que c'est un germane différence entre la simplification de la continuation passing style de l'attendent, et la vraie call-with-current-poursuite des structures que vous voyez dans les langues comme le Régime. Dans ces langues, l'ensemble de la poursuite, y compris la poursuite de retour dans les appelants est capturé par appel-cc.

Que faire si l'appel de la méthode rend les autres appels de méthode avant qu'il attend, pourquoi ne pas la pile écrasés?

Ces appels de méthode de retour, et ainsi de leur activation, les enregistrements ne sont plus sur la pile, au point de l'attendre.

Et comment sur terre serait le moteur d'exécution travailler son chemin à travers tout cela, dans le cas d'une exception et une pile de vous détendre?

Dans le cas d'une exception non interceptée, l'exception est interceptée, stockée à l'intérieur de la tâche, et re-levée lorsque la tâche que le résultat de l'extraction.

Rappelez-vous tout ce que la comptabilité je l'ai mentionné précédemment? Obtenir exception de la sémantique à droite a été une énorme douleur, permettez-moi de vous le dire.

Lorsque le rendement est atteint, comment fonctionne le moteur d'exécution de garder la trace du point où les choses doivent être ramassés? Comment est itérateur de l'état est-elle préservée?

De la même manière. L'état des locaux est déplacé sur le tas, et un nombre représentant l'instruction à qui MoveNext devrait reprendre la prochaine fois, il est appelé est stocké avec les habitants.

Et encore, il y a un tas de matériel dans un itérateur bloc pour s'assurer que les exceptions sont traitées correctement.

37voto

Jon Hanna Points 40291

yield est la plus simple des deux, donc nous allons l'examiner.

Dire que nous avons:

public IEnumerable<int> CountToTen()
{
  for (int i = 1; i <= 10; ++i)
  {
    yield return i;
  }
}

Ce sera compilé un peu comme si nous avions écrit:

// Deliberately use name that isn't valid C# to not clash with anything
private class <CountToTen> : IEnumerator<int>, IEnumerable<int>
{
    private int _i;
    private int _current;
    private int _state;
    private int _initialThreadId = CurrentManagedThreadId;

    public IEnumerator<CountToTen> GetEnumerator()
    {
        // Use self if never ran and same thread (so safe)
        // otherwise create a new object.
        if (_state != 0 || _initialThreadId != CurrentManagedThreadId)
        {
            return new <CountToTen>();
        }

        _state = 1;
        return this;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public int Current => _current;

    object IEnumerator.Current => Current;

    public bool MoveNext()
    {
        switch(_state)
        {
            case 1:
                _i = 1;
                _current = i;
                _state = 2;
                return true;
            case 2:
                ++_i;
                if (_i <= 10)
                {
                    _current = _i;
                    return true;
                }
                break;
        }
        _state = -1;
        return false;
    }

    public void Dispose()
    {
      // if the yield-using method had a `using` it would
      // be translated into something happening here.
    }

    public void Reset()
    {
        throw new NotSupportedException();
    }
}

Donc, pas aussi efficace qu'un écrit à la main et de la mise en œuvre de l' IEnumerable<int> et IEnumerator<int> (par exemple, nous n'aurions probablement pas des déchets ayant un _state, _i et _current dans ce cas), mais pas mal (le truc de réutiliser lui-même lorsque les conditions sont sécuritaires plutôt que de créer un nouvel objet est bonne) et extensible pour traiter de très compliqué yield-à l'aide de méthodes.

Et bien sûr, depuis

foreach(var a in b)
{
  DoSomething(a);
}

Est le même que:

using(var en = b.GetEnumerator())
{
  while(en.MoveNext())
  {
     var a = en.Current;
     DoSomething(a);
  }
}

Alors générés MoveNext() est appelé à plusieurs reprises.

L' async de cas est à peu près le même principe, mais avec un peu plus de complexité. Pour réutiliser un exemple à partir d' une autre réponse Code comme:

private async Task LoopAsync()
{
    int count = 0;
    while(count < 5)
    {
       await SomeNetworkCallAsync();
       count++;
    }
}

Produit code comme:

private struct LoopAsyncStateMachine : IAsyncStateMachine
{
  public int _state;
  public AsyncTaskMethodBuilder _builder;
  public TestAsync _this;
  public int _count;
  private TaskAwaiter _awaiter;
  void IAsyncStateMachine.MoveNext()
  {
    try
    {
      if (_state != 0)
      {
        _count = 0;
        goto afterSetup;
      }
      TaskAwaiter awaiter = _awaiter;
      _awaiter = default(TaskAwaiter);
      _state = -1;
    loopBack:
      awaiter.GetResult();
      awaiter = default(TaskAwaiter);
      _count++;
    afterSetup:
      if (_count < 5)
      {
        awaiter = _this.SomeNetworkCallAsync().GetAwaiter();
        if (!awaiter.IsCompleted)
        {
          _state = 0;
          _awaiter = awaiter;
          _builder.AwaitUnsafeOnCompleted<TaskAwaiter, TestAsync.LoopAsyncStateMachine>(ref awaiter, ref this);
          return;
        }
        goto loopBack;
      }
      _state = -2;
      _builder.SetResult();
    }
    catch (Exception exception)
    {
      _state = -2;
      _builder.SetException(exception);
      return;
    }
  }
  [DebuggerHidden]
  void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
  {
    _builder.SetStateMachine(param0);
  }
}

public Task LoopAsync()
{
  LoopAsyncStateMachine stateMachine = new LoopAsyncStateMachine();
  stateMachine._this = this;
  AsyncTaskMethodBuilder builder = AsyncTaskMethodBuilder.Create();
  stateMachine._builder = builder;
  stateMachine._state = -1;
  builder.Start(ref stateMachine);
  return builder.Task;
}

C'est plus compliqué, mais très similaire principe de base. La principale complication supplémentaire est que, désormais, GetAwaiter() est utilisé. Si toute fois awaiter.IsCompleted est vérifiée il retourne true parce que la tâche awaited est déjà terminé (par exemple des cas où il pourrait revenir en mode synchrone), puis la méthode continue de se déplacer à travers les états, mais sinon, il définit lui-même comme un rappel à la awaiter.

Tout ce qui se passe avec qui dépend de la awaiter, en termes de ce qui déclenche le rappel (par exemple, e/S asynchrone à l'achèvement d'une tâche en cours d'exécution sur un thread d'achèvement des travaux) et quelles sont les exigences qu'il y a pour le regroupement pour un thread particulier ou en cours d'exécution sur un pool de threads thread, ce contexte de l'appel d'origine peut ou peut ne pas être nécessaire, et ainsi de suite. Quel que soit ce qu'elle est bien quelque chose dans ce awaiter s'appeler dans l' MoveNext et il va continuer avec le prochain morceau de travail (jusqu'à la prochaine await) ou de la finition et de retour, auquel cas l' Task que c'est la mise en œuvre devient terminée.

13voto

Stephen Cleary Points 91731

Il y a une tonne de réponses ici, je vais partager quelques points de vue qui peuvent aider à former un modèle mental.

Tout d'abord, une async méthode est cassé en plusieurs morceaux par le compilateur; l' await expressions sont les points de divergence. (C'est facile à concevoir pour des méthodes simples; des méthodes plus complexes avec des boucles et de la gestion des exceptions aussi être fragmenté, avec l'ajout de plus en plus complexes, de l'état de la machine).

Deuxièmement, await se traduit par une assez simple séquence; j'aime Lucian de la description, qui dans les mots est assez bien "si la awaitable est déjà complète, obtenir le résultat et poursuivre l'exécution de cette méthode; sinon, enregistrez cette méthode de l'état et de retour". (J'utilise très similaire terminologie dans mes async intro).

Lorsqu'une attendent est atteint, comment fonctionne le moteur d'exécution de savoir ce morceau de code doit s'exécuter prochaine?

Le reste de la méthode existe comme un rappel pour que awaitable (dans le cas de tâches, ces rappels sont la continuation). Lorsque le awaitable se termine, il invoque ses rappels.

Notez que la pile d'appels est pas sauvé et restauré; rappels sont directement invoquée. Dans le cas de overlapped I/O, ils sont invoquées directement à partir du pool de threads.

Ces rappels peuvent poursuivre l'exécution de la méthode directement, ou ils peuvent planifier l'exécution ailleurs (p. ex., si l' await capturé une INTERFACE utilisateur SynchronizationContext et les e/S est terminée sur le pool de threads).

Comment savoir quand il peut la reprendre là où il l'a laissée, et comment faut-il rappeler où?

C'est tous simplement des rappels. Lorsqu'un awaitable se termine, il invoque ses rappels, et tout async méthode qui avait déjà awaited il obtient repris. Le rappel des sauts dans le milieu de cette méthode et a ses variables locales dans le champ d'application.

Les rappels sont pas exécuter un thread particulier, et ils ne sont pas ont leur de la pile restauré.

Qu'advient-il de la pile d'appel en cours, fait-il sauvé en quelque sorte? Que faire si l'appel de la méthode rend les autres appels de méthode avant qu'il attend, pourquoi ne pas la pile écrasés? Et comment sur terre serait le moteur d'exécution travailler son chemin à travers tout cela, dans le cas d'une exception et une pile de vous détendre?

La pile des appels n'est pas enregistré dans la première place; il n'est pas nécessaire.

Avec code synchrone, vous pouvez vous retrouver avec une pile d'appel qui comprend tous vos appels, et le moteur d'exécution sait où pour revenir à l'aide que.

Avec le code asynchrone, vous pouvez vous retrouver avec un tas de rappel des pointeurs enracinée à un certain opération d'e/S qui termine sa tâche, qui peut reprendre une async méthode qui a terminé sa tâche, qui peut reprendre une async méthode qui a terminé sa tâche, etc.

Ainsi, avec code synchrone A appelant B appelant C, votre pile des appels peut ressembler à ceci:

A:B:C

alors que le code asynchrone utilise les rappels (pointeurs):

A <- B <- C <- (I/O operation)

Lorsque le rendement est atteint, comment fonctionne le moteur d'exécution de garder la trace du point où les choses doivent être ramassés? Comment est itérateur de l'état est-elle préservée?

Actuellement, inefficacement. :)

Il fonctionne comme n'importe quel autre lambda - variable de la durée de vie est prolongée et les références sont placés dans l'état d'un objet qui vit sur la pile. La meilleure ressource pour tous les profondes au niveau des détails est Jon Skeet est EduAsync de la série.

7voto

Chris Tavares Points 8818

yield et await sont, tout à la fois de traiter avec le contrôle de flux, de deux choses complètement différentes. Donc je vais les aborder séparément.

L'objectif de l' yield est de le rendre plus facile à construire paresseux séquences. Lorsque vous écrivez un agent recenseur-boucle avec un yield déclaration, le compilateur génère une tonne de nouveau code vous ne voyez pas. Sous le capot, il génère en fait une toute nouvelle classe. La classe contient des membres qui le suivi de l'état de la boucle, et une mise en œuvre de IEnumerable, de sorte que chaque fois que vous appelez MoveNext il suit une fois de plus à travers cette boucle. Donc, quand vous faites une boucle foreach, comme ceci:

foreach(var item in mything.items()) {
    dosomething(item);
}

le code généré ressemble à quelque chose comme:

var i = mything.items();
while(i.MoveNext()) {
    dosomething(i.Current);
}

À l'intérieur de la mise en œuvre de mything.les éléments() est un tas de machine d'état code qui permettra de faire une "étape" de la boucle de retour. Ainsi, alors que vous l'écrivez dans la source comme une simple boucle, sous le capot, ce n'est pas une simple boucle. Donc, compilateur de la ruse. Si vous voulez voir vous-même, sortez ILDASM ou ILSpy ou des outils similaires et voir ce que l'généré IL ressemble. Il devrait être instructif.

async et await, d'autre part, sont une toute autre paire de manches. Attendre est, dans l'abstrait, les primitives de synchronisation. C'est une façon de dire que le système "je ne peux pas continuer jusqu'à ce que ce soit fait." Mais, comme vous l'avez remarqué, il n'y a pas toujours un thread en question.

Ce qui est impliqué est quelque chose qui s'appelle un contexte de synchronisation. Il y en a toujours un qui traînent. Le travail de leur contexte de synchronisation est de planifier des tâches qui sont attendues et leurs suites.

Quand vous dites await thisThing(), un couple de choses arriver. Dans une méthode asynchrone, le compilateur fait côtelettes la méthode en petits morceaux, chaque morceau étant un "avant les attendent" et un "après une attente" (ou la poursuite) de la section. Lorsque l'attendent s'exécute, la tâche attendue, et la suite de " continuation - in d'autres termes, le reste de la fonction - qui est passé à la synchronisation du contexte. Le contexte prend soin de la planification de la tâche, et quand c'est fini le contexte, puis exécute la poursuite, en passant quelle que soit la valeur de retour qu'il veut.

La synchronisation contexte est libre de faire ce qu'il veut tant qu'il les horaires de trucs. Il pourrait utiliser le pool de threads. Il pourrait créer un thread par tâche. Il peut fonctionner de manière synchrone. Les différents environnements (ASP.NET vs WPF) proposent différentes sync contexte implémentations qui font des choses différentes en fonction sur ce qui est le mieux pour leurs environnements.

(Bonus: jamais demandé ce qu' .ConfigurateAwait(false) ? Il indique au système de ne pas utiliser la synchronisation actuel contexte (généralement selon votre type de projet - WPF vs ASP.NET par exemple) et au lieu d'utiliser la valeur par défaut, qui utilise le pool de threads).

Encore une fois, c'est beaucoup de compilateur de la ruse. Si vous regardez le code généré son compliquée, mais vous devriez être capable de voir ce qu'il fait. Ces types de transformations sont durs, mais déterministe et mathématique, qui est pourquoi il est grand temps que le compilateur fait pour nous.

P. S. Il y a une exception à l'existence d'un défaut de synchronisation des contextes - console apps n'ont pas de synchronisation par défaut le contexte. Vérifier Stephen Toub blog pour beaucoup plus d'informations. C'est un grand endroit pour chercher de l'information sur async et await en général.

4voto

IllidanS4 Points 811

Normalement, je serais recommande de regarder le CIL, mais dans le cas de ces derniers, c'est un gâchis.

Ces deux constructions de langage sont similaires dans le travail, mais mis en œuvre un peu différemment. Fondamentalement, c'est juste du sucre syntaxique pour un compilateur de la magie, il n'y a rien de fou/dangereux au niveau de l'assemblage. Examinons brièvement.

yield est un peu plus ancien et plus simple déclaration, et c'est un sucre syntaxique pour un état de base de la machine. Une méthode retournant IEnumerable<T> ou IEnumerator<T> peut contenir un yield, ce qui transforme la méthode de l'état de la machine de l'usine. Une chose que vous remarquerez est que pas de code dans la méthode est exécutée au moment où vous l'appelez, si il y a un yield à l'intérieur. La raison en est que le code que vous écrivez est transporté à l' IEnumerator<T>.MoveNext méthode, qui vérifie l'état il est et exécute la bonne partie du code. yield return x; est ensuite converti en quelque chose qui ressemble à l' this.Current = x; return true;

Si vous faites un peu de réflexion, vous pouvez facilement inspecter la construction de l'état de la machine et de ses champs (au moins une pour l'état et pour les habitants). Vous pouvez même le réinitialiser si vous modifiez les champs.

await nécessite un peu de soutien de la bibliothèque de type, et fonctionne un peu différemment. Il prend un Task ou Task<T> argument, puis les résultats de sa valeur si la tâche est terminée, ou registres de la continuation via Task.GetAwaiter().OnCompleted. La mise en œuvre complète de l' async/await système serait trop long à expliquer, mais c'est pas non plus que mystique. Il crée également un état de la machine et passe ainsi que la poursuite de OnCompleted. Si la tâche est terminée, il utilise ensuite son résultat dans la suite. La mise en œuvre de la awaiter décide de la façon d'invoquer la suite. En général, il utilise le thread appelant la synchronisation du contexte.

Les deux yield et await ont pour diviser la méthode basée sur leur occurrence pour former une machine d'état, auprès de chaque direction générale de la machine représentant de chaque partie de la méthode.

Vous ne devriez pas penser au sujet de ces concepts dans le "bas niveau" des termes tels que des piles, des threads, etc. Ce sont des abstractions, et leur fonctionnement interne n'a pas besoin de tout le soutien de la CLR, c'est le compilateur qui fait la magie. C'est très différent d'Lua de coroutines, qui n'ont à l'exécution soutien, ou C est longjmp, qui est juste de la magie noire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X