193 votes

5 c# async CTP : Pourquoi est définie sur 0 dans « état » interne généré code avant l’appel de EndAwait ?

Hier j'ai donné une conférence sur le nouveau C# "async", en particulier à fouiller dans ce que le code généré ressemblait, et the GetAwaiter() / BeginAwait() / EndAwait() des appels.

Nous avons examiné en détail l'état de la machine généré par le compilateur C#, et il y avait deux aspects nous ne pouvions pas comprendre:

  • Pourquoi la classe générée contient un Dispose() méthode et un $__disposing variable, qui n'apparaissent jamais à être utilisé (et la classe n'implémente IDisposable).
  • Pourquoi l'interne state variable est définie à 0, avant tout appel à l' EndAwait(), lorsque 0 apparaît normalement à dire "c'est le premier point d'entrée".

Je soupçonne le premier point pourrait être répondu en faisant quelque chose de plus intéressant à l'intérieur de la méthode asynchrone, bien que si quelqu'un a des informations je serais heureux de l'entendre. Cette question est plus sur le deuxième point, cependant.

Voici un simple exemple de code:

using System.Threading.Tasks;

class Test
{
    static async Task<int> Sum(Task<int> t1, Task<int> t2)
    {
        return await t1 + await t2;
    }
}

... et voici le code qui est généré pour l' MoveNext() méthode qui implémente l'état de la machine. C'est copié directement à partir de Réflecteur - je n'ai pas fixé jusqu'à l'indicible, les noms de variables:

public void MoveNext()
{
    try
    {
        this.$__doFinallyBodies = true;
        switch (this.<>1__state)
        {
            case 1:
                break;

            case 2:
                goto Label_00DA;

            case -1:
                return;

            default:
                this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
                this.<>1__state = 1;
                this.$__doFinallyBodies = false;
                if (this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate))
                {
                    return;
                }
                this.$__doFinallyBodies = true;
                break;
        }
        this.<>1__state = 0;
        this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
        this.<a2>t__$await4 = this.t2.GetAwaiter<int>();
        this.<>1__state = 2;
        this.$__doFinallyBodies = false;
        if (this.<a2>t__$await4.BeginAwait(this.MoveNextDelegate))
        {
            return;
        }
        this.$__doFinallyBodies = true;
    Label_00DA:
        this.<>1__state = 0;
        this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
        this.<>1__state = -1;
        this.$builder.SetResult(this.<1>t__$await1 + this.<2>t__$await3);
    }
    catch (Exception exception)
    {
        this.<>1__state = -1;
        this.$builder.SetException(exception);
    }
}

Il est long, mais les lignes importants pour cette question sont les suivants:

// End of awaiting t1
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

// End of awaiting t2
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();

Dans les deux cas, l'état est modifié à nouveau par la suite avant la prochaine évidemment observé... alors pourquoi le mettre à 0, à tous? Si MoveNext() ont été appelés de nouveau à ce point (que ce soit directement ou par l'intermédiaire de Dispose), il serait un moyen efficace de commencer la méthode asynchrone de nouveau, ce qui serait totalement inapproprié pour autant que je peux dire... si et MoveNext() n'est pas appelée, le changement d'état n'est pas pertinent.

Est-ce simplement un effet secondaire de l'compilateur réutilisation itérateur bloc de génération de code asynchrone, où il peut avoir un plus évident explication?

Avertissement Important

Évidemment, c'est juste un CTP compilateur. Je m'attends à ce que les choses changent avant la version finale et peut-être même avant la prochaine version CTP. Cette question est en aucun cas tenter de prétendre c'est une faille dans le compilateur C# ou quelque chose comme ça. Je suis juste en train de travailler si il y a une subtile raison de ce que j'ai manqué :)

70voto

Jon Skeet Points 692016

Bon, j'ai enfin une vraie réponse. J'ai travaillé sur moi-même, mais seulement après Lucian Wischik de la VB partie de l'équipe a confirmé qu'il y a vraiment une bonne raison pour cela. Un grand merci à lui et merci de visiter son blog, qui rochers.

La valeur 0 n'est ici qu'spécial parce que c'est pas un état valide qui vous aurez peut-être juste avant la await dans un cas normal. En particulier, il n'est pas un état de la machine d'état peut en fin de test pour ailleurs. Je crois que l'utilisation de tout non-valeur positive pourrait fonctionner tout aussi bien: -1 n'est pas utilisé pour ce qu'il est logiquement incorrect, -1 signifie normalement "fini". Je pourrait dire que nous sommes de donner un supplément de sens à l'état 0 à l'instant, mais finalement il n'a pas vraiment d'importance. Le but de cette question est de trouver la raison de l'état est en train d'être définie.

La valeur est pertinente si l'attendent se termine dans une exception qui est capturé. On peut finir par revenir à la même attendre la déclaration de nouveau, mais nous ne devons pas être dans l'état signifie "je suis sur le point de revenir de qui l'attendent" sinon tous les types de code sera sauté. Il est plus simple de montrer un exemple. Notez que je suis maintenant à l'aide de la deuxième CTP, de sorte que le code généré est un peu différente de celle de la question.

Voici la méthode async:

static async Task<int> FooAsync()
{
    var t = new SimpleAwaitable();

    for (int i = 0; i < 3; i++)
    {
        try
        {
            Console.WriteLine("In Try");
            return await t;
        }                
        catch (Exception)
        {
            Console.WriteLine("Trying again...");
        }
    }
    return 0;
}

Sur le plan conceptuel, l' SimpleAwaitable peut être tout awaitable - peut-être une tâche, peut-être quelque chose d'autre. Pour les fins de mes tests, il renvoie toujours false pour IsCompleted, et lève une exception en GetResult.

Voici le code généré pour l' MoveNext:

public void MoveNext()
{
    int returnValue;
    try
    {
        int num3 = state;
        if (num3 == 1)
        {
            goto Label_ContinuationPoint;
        }
        if (state == -1)
        {
            return;
        }
        t = new SimpleAwaitable();
        i = 0;
      Label_ContinuationPoint:
        while (i < 3)
        {
            // Label_ContinuationPoint: should be here
            try
            {
                num3 = state;
                if (num3 != 1)
                {
                    Console.WriteLine("In Try");
                    awaiter = t.GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        state = 1;
                        awaiter.OnCompleted(MoveNextDelegate);
                        return;
                    }
                }
                else
                {
                    state = 0;
                }
                int result = awaiter.GetResult();
                awaiter = null;
                returnValue = result;
                goto Label_ReturnStatement;
            }
            catch (Exception)
            {
                Console.WriteLine("Trying again...");
            }
            i++;
        }
        returnValue = 0;
    }
    catch (Exception exception)
    {
        state = -1;
        Builder.SetException(exception);
        return;
    }
  Label_ReturnStatement:
    state = -1;
    Builder.SetResult(returnValue);
}

J'ai dû déplacer Label_ContinuationPoint pour la rendre valide code - sinon il n'est pas dans le champ d'application de l' goto de l'instruction, mais cela n'affecte pas la réponse.

Pensez à ce qui arrive lorsqu' GetResult lance son exception. Nous allons passer par le bloc catch, incrémenter i, puis boucle ronde à nouveau (en supposant i est toujours inférieur à 3). Nous sommes encore dans quel état nous étions avant la GetResult appel... mais quand nous arrivons à l'intérieur de l' try bloc, nous devons d'impression "À Essayer" et appelez GetAwaiter nouveau... et nous allons le faire que si l'état n'est pas 1. Sans l' state = 0 affectation, il va utiliser le awaiter et de passer l' Console.WriteLine appel.

C'est un assez tortueux peu de code à travers le travail, mais qui va juste pour montrer le genre de chose que l'équipe a à penser. Je suis content que je ne suis pas responsable de la mise en œuvre de cette :)

5voto

Rune FS Points 13350

si il a été maintenu à 1 (premier cas), vous recevez un appel à l' EndAwait sans un appel à l' BeginAwait. Si elle est maintenue à 2 (deuxième cas), vous obtiendrez le même résultat, juste de l'autre awaiter.

Je suppose que l'appel de la BeginAwait renvoie false si elle a déjà commencé (une supposition de ma part) et conserve la valeur d'origine pour retourner à la EndAwait. Si c'est le cas, il ne fonctionnera pas correctement alors que si vous le réglez à -1, vous pouvez avoir un non initialisée this.<1>t__$await1 pour le premier cas.

Toutefois, cela suppose que BeginAwaiter ne commencent réellement à l'action sur tous les appels après la première et qu'elle retourne la valeur false dans ces cas. Départ serait évidemment inacceptable, car il pourrait avoir des effets secondaires ou tout simplement donner un résultat différent. Il a également assumpes que le EndAwaiter retourne toujours la même valeur, peu importe combien de fois il est appelé et qui est peut être appelée lorsque BeginAwait retourne false (conformément à l'hypothèse ci-dessus)

Il semblerait que ce soit une protection contre des conditions de course Si nous inline les états où movenext est appelé par un autre thread, après que l'état = 0 dans les questions qu'il woule ressembler à quelque chose comme ci-dessous

this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)
this.<>1__state = 0;

//second thread
this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)
this.$__doFinallyBodies = true;
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

//other thread
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

Si les hypothèses ci-dessus sont corrects, la il y a un peu inutiles travail effectué comme sawiater et réattribution de la même valeur à <1>t__$await1. Si l'état a été maintenu à 1 alors la dernière partie serait en place:

//second thread
//I suppose this un matched call to EndAwait will fail
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

encore si elle a été fixée à 2 la machine d'état suppose qu'il avait déjà obtenu la valeur de la première action qui serait faux et (potentiellement) non affecté variable serait utilisée pour calculer le résultat

1voto

GaryMcAllister Points 106

Serait-ce quelque chose à voir avec async empilés/imbriqué appels ?...

C’est à dire :

Le délégué movenext est appelé plusieurs fois dans cette situation ?

Juste une botté de dégagement vraiment ?

0voto

fix_likes_coding Points 3586

Explication des états réels:

états possibles:

  • 0 Initialisé (je pense que oui) ou en attente pour la fin de l'opération
  • >0 juste appelé MoveNext, le choix du prochain état
  • -1 terminé

Est-il possible que cette mise en œuvre veut juste s'assurer que, si un autre Appel à la méthode MoveNext à partir de là où il se passe (en attendant) il faudra réévaluer l'ensemble de l'état de la chaîne depuis le début, de réévaluer les résultats qui pourraient être dans le temps de le dire déjà obsolète?

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