Hier, je faisais un exposé sur la nouvelle fonctionnalité C # "async", en particulier sur l'aspect du code généré et the GetAwaiter()
/ BeginAwait()
/ les EndAwait()
appels.
Nous avons examiné en détail la machine d'état générée par le compilateur C #, et il y avait deux aspects que nous ne pouvions pas comprendre:
- Pourquoi la classe générée contient une
Dispose()
méthode et une$__disposing
variable, qui ne semblent jamais être utilisées (et la classe ne l'implémente pasIDisposable
). - Pourquoi la
state
variable interne est définie sur 0 avant tout appel àEndAwait()
, alors que 0 semble normalement signifier "c'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 des informations supplémentaires, je serais heureux de l'entendre. Cette question concerne cependant davantage le deuxième point.
Voici un exemple de code très simple:
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 la MoveNext()
méthode qui implémente la machine d'état. Ceci est copié directement à partir de Reflector - je n'ai pas corrigé les noms de variables indescriptibles:
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);
}
}
C'est long, mais les lignes importantes pour cette question sont les suivantes:
// 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 à nouveau modifié par la suite avant d'être ensuite observé de manière évidente ... alors pourquoi le mettre à 0? Si j'étais MoveNext()
appelé à nouveau à ce stade (soit directement, soit via Dispose
), il redémarrerait effectivement la méthode asynchrone, ce qui serait tout à fait inapproprié pour autant que je sache ... si et MoveNext()
n'est pas appelé, 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 bloc d'itérateur pour async, où il peut avoir une explication plus évidente?
Avertissement important
Évidemment, ce n'est qu'un compilateur CTP. Je m'attends à ce que les choses changent avant la version finale - et peut-être même avant la prochaine version CTP. Cette question n'essaie en aucun cas de prétendre qu'il s'agit d'une faille dans le compilateur C # ou quelque chose comme ça. J'essaie juste de savoir s'il y a une raison subtile à cela que j'ai ratée :)
la source
Réponses:
D'accord, j'ai enfin une vraie réponse. J'ai en quelque sorte réglé cela par moi-même, mais seulement après que Lucian Wischik de la partie VB de l'équipe ait confirmé qu'il y avait vraiment une bonne raison. Un grand merci à lui - et s'il vous plaît visitez son blog , qui est génial.
La valeur 0 ici est uniquement spéciale car ce n'est pas un état valide dans lequel vous pourriez être juste avant le
await
dans un cas normal. En particulier, ce n'est pas un état que la machine d'état peut finir par tester ailleurs. Je crois que l'utilisation d'une valeur non positive fonctionnerait aussi bien: -1 n'est pas utilisé pour cela car il est logiquement incorrect, car -1 signifie normalement "terminé". Je pourrais dire que nous donnons un sens supplémentaire à l'état 0 pour le moment, mais en fin de compte, cela n'a pas vraiment d'importance. Le point de cette question était de savoir pourquoi l'État est mis en place.La valeur est pertinente si l'attente se termine par une exception qui est interceptée. Nous pouvons finir par revenir à la même déclaration d'attente, mais nous ne devons pas être dans l'état, ce qui signifie «Je suis sur le point de revenir de cette attente» car 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:
Sur le plan conceptuel, le
SimpleAwaitable
peut être tout à fait attendue - peut-être une tâche, peut-être autre chose. Aux fins de mes tests, il renvoie toujours false forIsCompleted
et lève une exception dansGetResult
.Voici le code généré pour
MoveNext
:J'ai dû bouger
Label_ContinuationPoint
pour en faire un code valide - sinon ce n'est pas dans la portée de lagoto
déclaration - mais cela n'affecte pas la réponse.Pensez à ce qui se passe quand
GetResult
lève son exception. Nous allons parcourir le bloc catch, incrémenteri
, puis boucler à nouveau (en supposant qu'ili
est toujours inférieur à 3). Nous sommes toujours dans l'état où nous étions avant l'GetResult
appel ... mais quand nous entrons dans letry
bloc, nous devons imprimer "In Try" et rappelerGetAwaiter
... et nous ne le ferons que si l'état n'est pas 1. Sans l'state = 0
affectation, il utilisera l'attente existante et sautera l'Console.WriteLine
appel.C'est un morceau de code assez tortueux à travailler, mais cela ne fait que montrer le genre de choses auxquelles l'équipe doit penser. Je suis content de ne pas être responsable de la mise en œuvre de cela :)
la source
si elle était maintenue à 1 (premier cas), vous obtiendriez un appel à
EndAwait
sans appel à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 à renvoyer à EndAwait. Si tel est le cas, cela fonctionnerait correctement alors que si vous le définissez sur -1, vous pourriez avoir un fichier non initialisé
this.<1>t__$await1
pour le premier cas.Cela suppose cependant que BeginAwaiter ne démarrera pas réellement l'action sur les appels après le premier et qu'il retournera false dans ces cas. Le démarrage serait bien sûr inacceptable car il pourrait avoir un effet secondaire ou simplement donner un résultat différent. Il suppose également que EndAwaiter retournera toujours la même valeur, peu importe combien de fois il est appelé et qui peut être appelé lorsque BeginAwait retourne faux (selon l'hypothèse ci-dessus)
Cela semblerait être une protection contre les conditions de concurrence. Si nous insérons les déclarations où movenext est appelé par un thread différent après l'état = 0 dans les questions, il ressemblera à ce qui suit
Si les hypothèses ci-dessus sont correctes, des travaux inutiles sont effectués, tels que get sawiater et la réaffectation de la même valeur à <1> t __ $ wait1. Si l'état était maintenu à 1, la dernière partie serait plutôt:
en outre, si elle était définie sur 2, la machine d'état supposerait qu'elle a déjà obtenu la valeur de la première action qui serait fausse et une variable (potentiellement) non affectée serait utilisée pour calculer le résultat.
la source
Serait-ce quelque chose à voir avec les appels asynchrones empilés / imbriqués? ..
c'est à dire:
Le délégué movenext est-il appelé plusieurs fois dans cette situation?
Juste un coup de pied vraiment?
la source
MoveNext()
serait appelé une fois sur chacun d'eux.Explication des états réels:
états possibles:
Est-il possible que cette implémentation veuille simplement garantir que si un autre appel à MoveNext d'où qu'il se produise (en attendant), il réévalue à nouveau toute la chaîne d'état depuis le début, pour réévaluer des résultats qui pourraient être entre-temps déjà dépassés?
la source