C # 5 asynchrone CTP: pourquoi l’état interne est-il défini sur 0 dans le code généré avant l’appel EndAwait?

Hier, je parlais de la nouvelle fonctionnalité “async” de C #, notamment en ce qui concerne le code généré et the GetAwaiter() / BeginAwait() / EndAwait() .

Nous avons examiné en détail la machine à états générée par le compilateur C #, et nous ne pouvions pas comprendre deux aspects:

  • Pourquoi la classe générée contient une méthode Dispose() et une variable de $__disposing , qui ne semble jamais être utilisée (et la classe IDisposable pas IDisposable ).
  • Pourquoi la variable d’ state interne est définie sur 0 avant tout appel à EndAwait() , alors que 0 semble normalement signifier “ceci est le point d’entrée initial”.

Je soupçonne que le premier point pourrait être répondu en faisant quelque chose de plus intéressant dans la méthode asynchrone, bien que si quelqu’un a d’autres informations, je serais heureux de l’entendre. Cette question concerne plutôt le deuxième point.

Voici un exemple de code très simple:

 using System.Threading.Tasks; class Test { static async Task Sum(Task t1, Task t2) { return await t1 + await t2; } } 

… et voici le code qui est généré pour la méthode MoveNext() qui implémente la machine à états. Ceci est copié directement à partir de Reflector – Je n’ai pas corrigé les noms de variables indicibles:

 public void MoveNext() { try { this.$__doFinallyBodies = true; switch (this.1__state) { case 1: break; case 2: goto Label_00DA; case -1: return; default: this.t__$await2 = this.t1.GetAwaiter(); this.1__state = 1; this.$__doFinallyBodies = false; if (this.t__$await2.BeginAwait(this.MoveNextDelegate)) { return; } this.$__doFinallyBodies = true; break; } this.1__state = 0; this.t__$await1 = this.t__$await2.EndAwait(); this.t__$await4 = this.t2.GetAwaiter(); this.1__state = 2; this.$__doFinallyBodies = false; if (this.t__$await4.BeginAwait(this.MoveNextDelegate)) { return; } this.$__doFinallyBodies = true; Label_00DA: this.1__state = 0; this.t__$await3 = this.t__$await4.EndAwait(); this.1__state = -1; this.$builder.SetResult(this.t__$await1 + this.t__$await3); } catch (Exception exception) { this.1__state = -1; this.$builder.SetException(exception); } } 

C’est long, mais les lignes importantes pour cette question sont les suivantes:

 // End of awaiting t1 this.1__state = 0; this.t__$await1 = this.t__$await2.EndAwait(); // End of awaiting t2 this.1__state = 0; this.t__$await3 = this.t__$await4.EndAwait(); 

Dans les deux cas, l’état est modifié à nouveau avant qu’il ne soit évident qu’il est suivi… alors pourquoi le mettre à 0? Si MoveNext() été appelé à nouveau à ce stade (directement ou via Dispose ), la méthode asynchrone serait à nouveau lancée, ce qui serait totalement inapproprié pour autant que je MoveNext() … if et MoveNext() ne sont pas appelés, le changement d’état n’est pas pertinent.

Est-ce simplement un effet secondaire du compilateur réutilisant le code de génération de blocs d’iterators pour async, où il pourrait avoir une explication plus évidente?

Avertissement important

Évidemment, il ne s’agit que d’un compilateur CTP. Je m’attends à ce que les choses changent avant la version finale – et peut-être même avant la prochaine version de CTP. Cette question n’essaie en aucune façon d’affirmer qu’il s’agit d’une faille dans le compilateur C # ou quelque chose du genre. J’essaie juste de déterminer s’il y a une raison subtile à cela.

Ok, j’ai enfin une vraie réponse. Je me suis en quelque sorte arrangé par moi-même, mais seulement après que Lucian Wischik de l’équipe VB a confirmé qu’il y avait une bonne raison. Un grand merci à lui – et s’il vous plaît visitez son blog , qui bascule.

La valeur 0 ici n’est spéciale que parce que ce n’est pas un état valide que vous pourriez être juste avant l’ await dans un cas normal. En particulier, ce n’est pas un état que la machine d’état peut tester ailleurs. Je crois que l’utilisation d’une valeur non positive fonctionnerait tout aussi bien: -1 n’est pas utilisé pour cela car il est logiquement incorrect, car -1 signifie normalement “fini”. Je pourrais soutenir que nous donnons un sens supplémentaire à l’état 0 pour le moment, mais finalement, cela n’a pas vraiment d’importance. Le but de cette question était de découvrir pourquoi l’état est en train de se définir.

La valeur est pertinente si l’attente se termine par une exception interceptée. Nous pouvons finir par revenir à la même déclaration d’attente, mais nous ne devons pas être dans l’état signifiant “Je suis sur le sharepoint revenir de cette attente”, sinon toutes sortes de codes seraient ignorés. Il est plus simple de montrer cela avec un exemple. Notez que j’utilise maintenant le deuxième CTP, donc le code généré est légèrement différent de celui de la question.

Voici la méthode asynchrone:

 static async Task 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; } 

D'un sharepoint vue conceptuel, SimpleAwaitable peut être très utile - peut-être une tâche, peut-être autre chose. Aux fins de mes tests, il renvoie toujours false pour IsCompleted et lève une exception dans GetResult .

Voici le code généré pour 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 en faire un code valide - sinon ce n'est pas dans la scope de l' goto - mais cela n'affecte pas la réponse.

Pensez à ce qui se passe lorsque GetResult lance son exception. Nous allons passer par le bloc catch, incrémenter i , puis boucler à nouveau (en supposant que i toujours inférieur à 3). Nous sums toujours dans l'état où nous étions avant l'appel GetResult ... mais quand nous entrons dans le bloc try , nous devons imprimer "In Try" et appeler à nouveau GetAwaiter ... et nous ne le ferons que si l'état n'est pas 1. Sans l'affectation state = 0 , il utilisera l'attendeur existant et ignorera l'appel Console.WriteLine .

C'est un bout de code assez compliqué à suivre, mais cela montre simplement le genre de choses auxquelles l'équipe doit penser. Je suis content que je ne sois pas responsable de la mise en œuvre 🙂

s’il était conservé à 1 (premier cas), vous EndAwait un appel à EndAwait sans appeler BeginAwait . Si elle est maintenue à 2 (deuxième cas), vous obtiendrez le même résultat sur l’autre serveur.

Je suppose que l’appel de BeginAwait renvoie false s’il a déjà été démarré (une supposition de mon côté) et conserve la valeur d’origine à retourner à EndAwait. Si tel est le cas, cela fonctionnerait correctement alors que si vous le définissez à -1, vous pourriez avoir un fichier non initialisé this.<1>t__$await1 pour le premier cas.

Cela suppose toutefois que BeginAwaiter ne démarrera pas l’action sur les appels après le premier et qu’il renverra false dans ces cas. Commencer serait évidemment inacceptable car cela pourrait avoir un effet secondaire ou simplement donner un résultat différent. Il est également supposé que EndAwaiter retournera toujours la même valeur, quel que soit le nombre de fois qu’il est appelé et que cela peut être appelé lorsque BeginAwait renvoie false (conformément à l’hypothèse ci-dessus)

Cela semblerait être une protection contre les conditions de course Si nous incluons les instructions où movenext est appelé par un thread différent après l’état = 0 dans les questions, cela ressemblera à quelque chose comme ci-dessous

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

Si les hypothèses ci-dessus sont correctes, il y a des travaux inutiles, comme obtenir sawiater et réatsortingbuer la même valeur à <1> t __ $ waiting1. Si l’état était maintenu à 1, la dernière partie serait:

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

de plus, si elle était définie sur 2, la machine d’état supposerait qu’elle avait déjà obtenu la valeur de la première action qui serait fausse et qu’une variable (potentiellement) non atsortingbuée serait utilisée pour calculer le résultat.

Serait-ce quelque chose à voir avec les appels asynchrones empilés / nesteds?

c’est à dire:

 async Task m1() { await m2; } async Task m2() { await m3(); } async Task m3() { Thread.Sleep(10000); } 

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

Juste une pique vraiment?

Explication des états réels:

états possibles:

  • 0 Initialisé (je le pense) ou en attente de fin d’opération
  • > 0 vient d’appeler MoveNext, en choisissant l’état suivant
  • -1 terminé

Est-il possible que cette implémentation veuille simplement s’assurer que si un autre appel à MoveNext arrive où (quand il attend) il réévalue à nouveau l’ensemble de la chaîne d’état pour réévaluer les résultats qui pourraient être dans le même temps déjà dépassés?