Zanim przejdziemy do pokazania przykładów jak optymalizować async\await najpierw trzeba zrozumieć implementację wewnętrzną w .NET. Bez tego ciężko będzie cokolwiek optymalizować. Na początku async\await wydawał mi się również czymś niezwykłym, a jak zacząłem zagłębiać się w kod IL, okazało się, że to bardzo prosty mechanizm i mógłby być napisany przez każdego z nas.
Kod korzystający z async\await wygląda na synchroniczny. Pod spodem jednak są zwykłe callback’i – dokładnie tak jakby było to napisane w poprzednich wersjach framework’a. Przyjrzyjmy się następującemu przykładowi:
internal class Program { private static void Main(string[] args) { var task= DoAsync(3); } private static async Task<int> DoAsync(int inputParameter) { int localVariable = 3; int before = Before(); int asyncResult1 = await TaskMethod1(inputParameter); int afterTaskMethod1 = AfterTaskMethod1(); int asyncResult2 = await TaskMethod2(inputParameter + asyncResult1+localVariable); AfterTaskMethod2(); return asyncResult1 + asyncResult2 + inputParameter; } private static int Before() { return 1; } private static int AfterTaskMethod1() { return 1; } private static int AfterTaskMethod2() { return 1; } private static Task<int> TaskMethod1(int arg) { return Task.Factory.StartNew(() => arg); } private static Task<int> TaskMethod2(int arg) { return Task.Factory.StartNew(() => { Thread.Sleep(500000); return arg; ; }); } }
Po kolei… Mamy asynchroniczną metodę DoAsync. Najpierw wywołuje ona zwykłą synchroniczną metodę Before. Kolejne wywołanie to TaskMethod1, która jest już asynchroniczna i korzystamy tutaj z await, aby nie blokować wywołania. Potem wywołujemy znów synchroniczną metodę a na końcu TaskMethod2, która jest asynchroniczna. Przekazujemy także wynik z TaskMethod1 do niej. Na końcu zwracamy jakąś tam liczbę, która składa się z z wyników dostarczonych przez poszczególne metody.
Logika nie ma kompletnie sensu tutaj. Stworzyłem tak przykład, aby pokazać jak to działa, gdy przeplatamy ze sobą asynchroniczne i synchroniczne metody.
Proszę zauważyć, że DoAsync zwraca nie integer ale Task<int>. Wszystkie metody oznaczone słowem async muszą zwracać Task (jawnie lub niejawnie). Teraz możemy zajrzeć do Reflector’a, aby przekonać się, co tak naprawdę jest generowane. W DoAsync mamy:
[AsyncStateMachine(typeof(<DoAsync>d__0)), DebuggerStepThrough] private static unsafe Task<int> DoAsync(int inputParameter) { <DoAsync>d__0 d__; Task<int> task; AsyncTaskMethodBuilder<int> builder; &d__.inputParameter = inputParameter; &d__.<>t__builder = AsyncTaskMethodBuilder<int>.Create(); &d__.<>1__state = -1; builder = &d__.<>t__builder; &builder.Start<<DoAsync>d__0>(&d__); task = &&d__.<>t__builder.Task; Label_003C: return task; }
Proszę zauważyć, że nie ma tutaj słowa kluczowego async – tak naprawdę CLR nic o tym nie wiem. Cała funkcjonalność async\await nie wprowadza modyfikacji w CLR. Podczas kompilacji jest po prostu generowany specjalny kod, przedstawiony wyżej.
Przede wszystkim wygenerowana jest maszyna stanów. Atrybut [AsyncStateMachine(typeof(<DoAsync>d__0)), DebuggerStepThrough] po prostu wskazuje typ implementacji tej maszyny.
Maszyna stanów zawiera w formie pól publicznych wszelkie zmienne będące w obrębie metody async (w naszym przypadku DoAsync). Na przykład w DoAsync mamy inputParameter i jak widać w maszynie stanów również mamy publiczne pole inputParameter. Z kolei localVariable zostało przeniesione do samej implementacji maszyny, ponieważ nie ma konieczności ustawiania jej z poziomu DoAsync. Pole state określa aktualny stan maszyny – o tym później. Proszę zauważyć, że StateMachine zwraca Task, czyli wątek reprezentujący operację.
Przejdźmy teraz do samej implementacji StateMachine:
[CompilerGenerated] private struct <DoAsync>d__0 : IAsyncStateMachine { // Fields public int <>1__state; public AsyncTaskMethodBuilder<int> <>t__builder; private object <>t__stack; private TaskAwaiter<int> <>u__$awaiter6; public int <afterTaskMethod1>5__4; public int <asyncResult1>5__3; public int <asyncResult2>5__5; public int <before>5__2; public int <localVariable>5__1; public int inputParameter; // Methods private unsafe void MoveNext() { bool flag; int num; Exception exception; int num2; TaskAwaiter<int> awaiter; TaskAwaiter<int> awaiter2; int num3; Label_0000: try { flag = 1; num2 = this.<>1__state; switch ((num2 - -3)) { case 0: goto Label_0028; case 1: goto Label_0034; case 2: goto Label_0034; case 3: goto Label_002D; case 4: goto Label_002F; } goto Label_0034; Label_0028: goto Label_016F; Label_002D: goto Label_0089; Label_002F: goto Label_011A; Label_0034:; Label_0036: this.<localVariable>5__1 = 3; this.<before>5__2 = Program.Before(); awaiter = Program.TaskMethod1(this.inputParameter).GetAwaiter(); if (&awaiter.IsCompleted != null) { goto Label_00A8; } this.<>1__state = 0; this.<>u__$awaiter6 = awaiter; &this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<DoAsync>d__0>(&awaiter, this); flag = 0; goto Label_019F; Label_0089: awaiter = this.<>u__$awaiter6; this.<>u__$awaiter6 = new TaskAwaiter<int>(); this.<>1__state = -1; Label_00A8: int introduced7 = &awaiter.GetResult(); awaiter = new TaskAwaiter<int>(); num3 = introduced7; this.<asyncResult1>5__3 = num3; this.<afterTaskMethod1>5__4 = Program.AfterTaskMethod1(); awaiter = Program.TaskMethod2((this.inputParameter + this.<asyncResult1>5__3) + this.<localVariable>5__1).GetAwaiter(); if (&awaiter.IsCompleted != null) { goto Label_0139; } this.<>1__state = 1; this.<>u__$awaiter6 = awaiter; &this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<DoAsync>d__0>(&awaiter, this); flag = 0; goto Label_019F; Label_011A: awaiter = this.<>u__$awaiter6; this.<>u__$awaiter6 = new TaskAwaiter<int>(); this.<>1__state = -1; Label_0139: int introduced8 = &awaiter.GetResult(); awaiter = new TaskAwaiter<int>(); num3 = introduced8; this.<asyncResult2>5__5 = num3; Program.AfterTaskMethod2(); num = (this.<asyncResult1>5__3 + this.<asyncResult2>5__5) + this.inputParameter; goto Label_0189; Label_016F: goto Label_0189; } catch (Exception exception1) { Label_0171: exception = exception1; this.<>1__state = -2; &this.<>t__builder.SetException(exception); goto Label_019F; } Label_0189: this.<>1__state = -2; &this.<>t__builder.SetResult(num); Label_019F: return; } [DebuggerHidden] private unsafe void SetStateMachine(IAsyncStateMachine param0) { &this.<>t__builder.SetStateMachine(param0); return; } }
Najważniejsza metoda to MoveNext. Służy ona do wykonania kolejnego stanu. Co jest zatem poszczególnym stanem? Przyjrzyjmy się pierwszemu z nich:
this.<localVariable>5__1 = 3; this.<before>5__2 = Program.Before(); awaiter = Program.TaskMethod1(this.inputParameter).GetAwaiter(); if (&awaiter.IsCompleted != null) { goto Label_00A8; } this.<>1__state = 0; this.<>u__$awaiter6 = awaiter; &this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<DoAsync>d__0>(&awaiter, this); flag = 0;
Najpierw wywoływana jest synchroniczna metoda Before a wynik jest przechowywany znów w polu klasy. Następnie przechodzimy do TaskMethod1. Ze względu, że jest ona asynchroniczna, korzystamy ze specjalnej klasy, TaskWaiter’a. Więcej tak naprawdę nie możemy zrobić w tym stanie więc wywołujemy return. Teraz musimy poczekać na zakończenie metody TaskMethod1. W momencie jej zakończenia, MoveNext znów zostanie wywołany, ale teraz pole state będzie zawierało numer kolejnego stanu, co spowoduje, że następujący kod zostanie wywołany:
int introduced7 = &awaiter.GetResult(); awaiter = new TaskAwaiter<int>(); num3 = introduced7; this.<asyncResult1>5__3 = num3; this.<afterTaskMethod1>5__4 = Program.AfterTaskMethod1(); awaiter = Program.TaskMethod2((this.inputParameter + this.<asyncResult1>5__3) + this.<localVariable>5__1).GetAwaiter(); if (&awaiter.IsCompleted != null) { goto Label_0139; } this.<>1__state = 1; this.<>u__$awaiter6 = awaiter; &this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<DoAsync>d__0>(&awaiter, this); flag = 0; goto Label_019F;
Analogicznie w kolejnym stanie najpierw wywołujemy synchroniczną metodę AfterTaskMethod1, a potem, za pomocą TaskAwaiter’a wywołujemy TaskMethod2. Synchroniczne metody nie potrzebują żadnej szczególnej opieki, dlatego nie są one kryterium rozdzielania metody na kilka stanów.
Na końcu, ostatni stan wykonuje:
int introduced8 = &awaiter.GetResult(); awaiter = new TaskAwaiter<int>(); num3 = introduced8; this.<asyncResult2>5__5 = num3; Program.AfterTaskMethod2(); num = (this.<asyncResult1>5__3 + this.<asyncResult2>5__5) + this.inputParameter; &this.<>t__builder.SetResult(num);
Kod po prostu zwróci wyjątek z rezultatem ustawionym na to, co w oryginalnym kodzie mamy w return.
Wiem, że w moim opisie pominąłem wiele stanów ale nie ma to znaczenia. Ogólnie zasada polega na wygenerowaniu nowej klasy, opakowującej lokalne zmienne i parametry wejściowe. Jak widać, nie ma żadnej czarnej magii tutaj – wszystko to stary, dobrze znany kod c#. Cała maszyna jest wykonywana w osobnym wątku i jak widać w DoAsync, natychmiast jest zwracany wątek:
[AsyncStateMachine(typeof(<DoAsync>d__0)), DebuggerStepThrough] private static unsafe Task<int> DoAsync(int inputParameter) { <DoAsync>d__0 d__; Task<int> task; // inicjalizacja maszyny itp. task = &&d__.<>t__builder.Task; Label_003C: return task; }
DoAsync zatem nie blokuje wywołania. Reszta logiki jest zawarta w MoveNext – wewnętrznie pole state jest aktualizowane co przełącza logikę do konkretnego stanu. Kryterium podziału metody asynchronicznej na stany jest liczba await.
W następnym postach będę kontynuował tematykę async\await i pokażę, jak drobne zmiany mają wpływ na to, co zostanie wygenerowane w StateMachine.