Decompilando async/await

¡Hola!
Como bien dice el título de la entrada, vamos a echar un vistazo al código que se genera cuando utilizamos async/await. Siempre hemos escuchado que async/await es azúcar sintáctico y que genera un montón de código; pues voy a intentar explicar aprender qué es lo que hace el código que se genera cuando utilizamos async/await, a ver si me sale.
Para este ejemplo, vamos a decompilar el siguiente código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Runtime.CompilerServices;
    using System.Text;
    using System.Threading.Tasks;
 
    namespace Samples
    {
        class Program
        {
            static void Main(string[] args)
            {
                GetWebData().ConfigureAwait(false);
                Console.ReadLine();
            }
 
            private static async Task<string> GetWebData()
            {
                BeforeTaskCreation();
                var task = new HttpClient().GetStringAsync(new Uri("https://www.google.es"));
                AfterTaskCreation();
                var result = await task;
                AfterAwaitTask(result);
                return result;
            }
 
            private static void BeforeTaskCreation()
            {
                Console.WriteLine("BeforeTaskCreation - Antes de crear la tarea");
            }
            private static void AfterTaskCreation()
            {
                Console.WriteLine("AfterTaskCreation - Después de crear la tarea");
            }
            private static void AfterAwaitTask(string data)
            {
                Console.WriteLine("AfterAwaitTask");
            }
 
        }
    }

Hacemos poca cosa. Dentro del método GetWebData ejecutamos BeforeTaskCreation al principio de todo, creamos la tarea (sólo la creamos), ejecutamos AfterTaskCreation y, posteriormente, hacemos el await de la tarea; por último llamamos a AfterAwaitTask con los datos obtenidos de la web. Los métodos BeforeTaskCreation/AfterTaskCreation/AfterAwaitTask los he añadido para que se vea dónde se mueven cuando veamos el código decompilado. Si ejecutamos lo anterior, en la consola se mostraría esto:
El siguiente paso es, a partir de nuestro ejecutable, decompilarlo (o descompilarlo). Para ello utilizo la herramienta dotPeek y muestra el siguiente código decompilado:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
    // Decompiled with JetBrains decompiler
    // Type: Samples.Program
    // Assembly: Samples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    // MVID: 40BA6ECC-170B-4DA1-9E85-45EB912888CB
    // Assembly location: C:UsersLuisDesktopSamplesSamplesinDebugSamples.exe
    // Compiler-generated code is shown
 
    using System;
    using System.Diagnostics;
    using System.Net.Http;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    using System.Threading.Tasks;
 
    namespace Samples
    {
      internal class Program
      {
        private static void Main(string[] args)
        {
          Program.GetWebData().ConfigureAwait(false);
          Console.ReadLine();
        }
 
        private static Task<string> GetWebData()
        {
          Program.<GetWebData>d__0 stateMachine;
          stateMachine.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
          stateMachine.<>1__state = -1;
          stateMachine.<>t__builder.Start<Program.<GetWebData>d__0>(ref stateMachine);
          return stateMachine.<>t__builder.Task;
        }
 
        private static void BeforeTaskCreation()
        {
          Console.WriteLine("BeforeTaskCreation - Antes de crear la tarea");
        }
 
        private static void AfterTaskCreation()
        {
          Console.WriteLine("AfterTaskCreation - Después de crear la tarea");
        }
 
        private static void AfterAwaitTask(string data)
        {
          Console.WriteLine(nameof (AfterAwaitTask) + data);
        }
 
        public Program()
        {
          base..ctor();
        }
 
        [CompilerGenerated]
        [StructLayout(LayoutKind.Auto)]
        private struct <GetWebData>d__0 : IAsyncStateMachine
        {
          public int <>1__state;
          public AsyncTaskMethodBuilder<string> <>t__builder;
          public Task<string> <task>5__1;
          public string <result>5__2;
          private TaskAwaiter<string> <>u__$awaiter3;
          private object <>t__stack;
 
          void IAsyncStateMachine.MoveNext()
          {
            string result52;
            try
            {
              bool flag = true;
              TaskAwaiter<string> awaiter;
              switch (this.<>1__state)
              {
                case -3:
                  goto label_6;
                case 0:
                  awaiter = this.<>u__$awaiter3;
                  this.<>u__$awaiter3 = new TaskAwaiter<string>();
                  this.<>1__state = -1;
                  break;
                default:
                  Program.BeforeTaskCreation();
                  this.<task>5__1 = new HttpClient().GetStringAsync(new Uri("https://www.google.es"));
                  Program.AfterTaskCreation();
                  awaiter = this.<task>5__1.GetAwaiter();
                  if (!awaiter.IsCompleted)
                  {
                    this.<>1__state = 0;
                    this.<>u__$awaiter3 = awaiter;
                    this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, Program.<GetWebData>d__0>(ref awaiter, ref this);
                    flag = false;
                    return;
                  }
                  break;
              }
              string result = awaiter.GetResult();
              awaiter = new TaskAwaiter<string>();
              this.<result>5__2 = result;
              Program.AfterAwaitTask(this.<result>5__2);
              result52 = this.<result>5__2;
            }
            catch (Exception ex)
            {
              this.<>1__state = -2;
              this.<>t__builder.SetException(ex);
              return;
            }
    label_6:
            this.<>1__state = -2;
            this.<>t__builder.SetResult(result52);
          }
 
          [DebuggerHidden]
          void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
          {
            this.<>t__builder.SetStateMachine(param0);
          }
        }
      }
    }

Antes que nada, me parece curioso que en la línea 46 ha cambiado el literal que habíamos puesto con el nombre del método «AfterAwaitTask» por nameof(AfterAwaitTask), seguro que tiene alguna explicación. En mi caso lo voy a cambiar de nuevo por el string porque estoy utilizando VS2013 y no estoy usando C# 6. Además, como el código es algo denso, vamos a simplificarlo un poco y cambiar algunos nombres de variables:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
    using System;
    using System.Diagnostics;
    using System.Net.Http;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    using System.Threading.Tasks;
 
    namespace Samples
    {
        internal class Program
        {
            private static void Main(string[] args)
            {
                GetWebData().ConfigureAwait(false);
                Console.ReadLine();
            }
 
            private static Task<string> GetWebData()
            {
                AsyncStateMachine stateMachine = new AsyncStateMachine();
                stateMachine.Builder = AsyncTaskMethodBuilder<string>.Create();
                stateMachine.CurrentState = -1;
                stateMachine.Builder.Start<AsyncStateMachine>(ref stateMachine);
                return stateMachine.Builder.Task;
            }
 
            private static void BeforeTaskCreation()
            {
                Console.WriteLine("BeforeTaskCreation - Antes de crear la tarea");
            }
 
            private static void AfterTaskCreation()
            {
                Console.WriteLine("AfterTaskCreation - Después de crear la tarea");
            }
 
            private static void AfterAwaitTask(string data)
            {
                Console.WriteLine("AfterAwaitTask" + data);
            }
 
            private struct AsyncStateMachine : IAsyncStateMachine
            {
                public int CurrentState;
                public AsyncTaskMethodBuilder<string> Builder;
                public Task<string> Task;
                public string Result;
                private TaskAwaiter<string> awaiter;
 
                void IAsyncStateMachine.MoveNext()
                {
                    string currentResult = null;
                    try
                    {
                        bool flag = true;
                        TaskAwaiter<string> awaiter;
                        switch (this.CurrentState)
                        {
                            case -3:
                                goto label_6;
                            case 0:
                                awaiter = this.awaiter;
                                this.awaiter = new TaskAwaiter<string>();
                                this.CurrentState = -1;
                                break;
                            default:
                                BeforeTaskCreation();
                                this.Task = new HttpClient().GetStringAsync(new Uri("https://www.google.es"));
                                AfterTaskCreation();
                                awaiter = this.Task.GetAwaiter();
                                if (!awaiter.IsCompleted)
                                {
                                    this.CurrentState = 0;
                                    this.awaiter = awaiter;
                                    this.Builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, AsyncStateMachine>(ref awaiter, ref this);
                                    flag = false;
                                    return;
                                }
                                break;
                        }
                        string result = awaiter.GetResult();
                        awaiter = new TaskAwaiter<string>();
                        this.Result = result;
                        AfterAwaitTask(this.Result);
                        currentResult = this.Result;
                    }
                    catch (Exception ex)
                    {
                        this.CurrentState = -2;
                        this.Builder.SetException(ex);
                        return;
                    }
 
                label_6:
                    this.CurrentState = -2;
                    this.Builder.SetResult(currentResult);
                }
 
                void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
                {
                    this.Builder.SetStateMachine(param0);
                }
            }
        }
    }

Así ya queda un poco más claro. Vamos a empezar a analizar un poco el código.
En lo primero que nos fijamos es que el método GetWebData ya no es async Task, sino que simplemente devuelve un Task. Además ya no tenemos rastro de await por ningún sitio. Todo esto se ha eliminado y se ha creado una máquina de estados llamada AsyncStateMachine. Si miramos el método GetWebData, inicializamos nuestra máquina de estados en el estado -1 y creamos un AsyncTaskMethodBuilder que será el encargado de lanzar el código que recupera los datos de la web (en concreto www.google.es) y, cuando finalice, llamará al método MoveNext de nuestra máquina de estados, pero vamos por partes.
Ya tenemos inicializada nuestra máquina de estados y hemos añadido el AsyncTaskMethodBuilder en la propiedad Builder. Lo siguiente que hacemos es iniciar nuestro Builder asociado a la máquina de estados pasándole una referencia a nuestra máquina de estados:

1
    stateMachine.Builder.Start<AsyncStateMachine>(ref stateMachine);

Este método, entre otras cosas, llamará al método MoveNext de nuestra máquina de estados. Si recordamos, hemos puesto el valor -1 a la máquina de estados. Vale, ya tenemos todo listo, pero, y qué es una máquina de estados. Pues dicho a grosso modo, es un trozo de código en el que, dependiendo el estado en el que se encuentre, se comportará de una forma u otra. En nuestro caso, para representarla se utiliza el interfaz IAsyncStateMachine, que tiene la siguiente firma:

1
2
3
4
5
    public interface IAsyncStateMachine
    {
        void MoveNext();
        void SetStateMachine(IAsyncStateMachine stateMachine);
    }

El método MoveNext se va a llamar cuando se haya cambiado el estado para ejecutar el código asociado a dicho estado. El método SetStateMachine se utiliza para establecer el estado actual (en concreto de nuestro Builder). Revisando todo nuestro código, no vemos que en ningún caso se esté llamando al método MoveNext, realmente nosotros en nuestra máquina de estados sólo estamos definiendo los comportamientos que se deben ejecutar cuando cambia el estado, es decir, le decimos el cómo, no el cuándo. Realmente quien se va a encargar de llamar en el momento adecuado al método MoveNext va a ser nuestro Builder.
Recapitulando, tenemos preparada nuestra máquina de estados con el estado -1 y nuestro Builder asociado. El primer paso es llamar al método Start del Builder. Este método (Start) va a llamar al método MoveNext de nuestra máquina de estados… pues vamos a ver qué hace nuestra máquina de estados cuando el estado es -1. En concreto, dentro del switch va a ejecutar exclusivamente nuestro caso por defecto y digo exclusivamente, porque si la tarea no está completa (que sería lo normal) hay un return que haría no se ejecutase más código del método MoveNext:

1
2
3
4
5
6
7
8
9
10
11
12
13
    BeforeTaskCreation();
    this.Task = new HttpClient().GetStringAsync(new Uri("https://www.google.es"));
    AfterTaskCreation();
    awaiter = this.Task.GetAwaiter();
    if (!awaiter.IsCompleted)
    {
        this.CurrentState = 0;
        this.awaiter = awaiter;
        this.Builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, AsyncStateMachine>(ref awaiter, ref this);
        flag = false;
        return;
    }
    break;

Lo primero que nos encontramos es que ha movido todo el código que teníamos antes de hacer el await a esta parte del código (si recordamos, antes estaba dentro de GetWebData). Es decir, aquí ejecutamos BeforeTaskCreation, obtenemos la tarea para recuperar los datos de www.google.es y ejecutamos AfterTaskCreation. El siguiente paso es obtener el awaiter para nuestra task que recupera los datos de la web. Hecho esto hay dos caminos posibles. Si el awaiter ya ha sido completado (no sería lo habitual), aquí finalizaría nuestro código del switch y continuaría por el método MoveNext acabando aquí el proceso de nuestra máquina de estados, es decir, establecería el resultado obtenido, ejecutaría el método AfterAwaitTask, etc. Este funcionamiento lo podríamos simular si en vez de this.Task = new HttpClient().GetStringAsync(new Uri(«https://www.google.es»)); pusiésemos this.Task = System.Threading.Tasks.Task.FromResult(«datos de google.es»). Realmente este no suele ser el caso, lo típico es que nuestra tarea no esté completada antes de iniciarla, y, en este caso, lo que se hace básicamente es poner el estado a 0 y lanzar la tarea a través de AwaitUnsafeOnCompleted. Vamos a analizar un poco esto último:

1
    this.Builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, AsyncStateMachine>(ref awaiter, ref this);

Esta sentencia recibe el awaiter que hemos creado y la propia máquina de estados. Entre otras muchas cosas, va a ser la que se encargue de lanzar la tarea y, cuando ésta haya finalizado, llamará de nuevo al método MoveNext de nuestra máquina de estados (le pasamos la máquina de estados por referencia en el segundo parámetro). Simplificando mucho (mucho), haría algo así:

1
2
3
4
5
    IAsyncStateMachine stateMachine = this;
    this.awaiter.UnsafeOnCompleted(() =>
    {
        stateMachine.MoveNext();
    });

Por claridad del ejemplo, no he sacado stateMachine.MoveNext() a un método (porque por lo visto no deberíamos hacer algo como IAsyncStateMachine stateMachine = this). En cualquier caso, lo que hacemos con el método UnsafeOnCompleted es, una vez se haya finalizado la Task (en nuestro caso, se hayan recuperado los datos de la web), se continúa con la acción stateMachine.MoveNext(). Como decía, this.Builder.AwaitUnsafeOnCompleted, AsyncStateMachine>(ref awaiter, ref this) hace muchas más cosas, pero lo que está claro es que cuando finalice la tarea va a llamar de nuevo a MoveNext. Si recordamos, antes de hacer esto habíamos puesto el estado a 0, por lo que vamos a ver qué haría a continuación (con estado 0).
Cuando en nuestro método MoveNext entramos y tenemos estado 0, se supone que nuestra tarea ya ha finalizado y se ejecutaría el siguiente código dentro del switch:

1
2
3
4
    awaiter = this.awaiter;
    this.awaiter = new TaskAwaiter<string>();
    this.CurrentState = -1;
    break;

Lo más reseñable es que pone el estado de nuestra máquina de estados al valor -1. Si recordamos, este es el valor inicial, y tiene sentido, porque se supone que éste (nuestro ciclo actual) va a ser nuestro estado final porque ya hemos finalizado la tarea y no nos queda más que obtener su resultado. Tras esto, y si todo ha ido bien, se va a ejecutar la parte final del método MoveNext :

1
2
3
4
5
    string result = awaiter.GetResult();
    awaiter = new TaskAwaiter<string>();
    this.Result = result;
    AfterAwaitTask(this.Result);
    currentResult = this.Result;

Puesto que nuestra tarea ya ha finalizado, obtenemos su resultado de forma instantánea con awaiter.GetResult(). Si avanzamos un par de líneas, vemos otra parte del código que antes teníamos en el método GetWebData, y es AfterAwaitTask(this.Result), y con esto se finalizaría todo lo que hacíamos en nuestro método GetWebData.

Conclusiones

Con todo esto que hemos visto, más o menos se ve con claridad qué es lo que realmente se hace cuando utilizamos un async/await. He obviado el caso del switch -3 porque no tengo ni idea (tampoco lo he mirado) en qué momento se puede lanzar, pero al final lo importante es tener una visión general. Realmente el código más complejo es el que no hemos visto, y se encuentra en el Builder (AsyncTaskMethodBuilder), pero creo que si más o menos ha quedado claro cómo funciona la máquina de estados que se crea, es más que suficiente, el resto… detalles de implementación 🙂
Simplificando mucho y muy muy mal, podríamos dejar la máquina de estados en lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    private struct AsyncStateMachine : IAsyncStateMachine
    {
        public int CurrentState;
        public AsyncTaskMethodBuilder<string> Builder;
        public Task<string> Task;
        private TaskAwaiter<string> awaiter;
 
        void IAsyncStateMachine.MoveNext()
        {
            if(this.CurrentState==0)
            {
                this.CurrentState = -1;
            }
            else
            {
                BeforeTaskCreation();
                this.Task = new HttpClient().GetStringAsync(new Uri("https://www.google.es"));
                AfterTaskCreation();
                this.awaiter = this.Task.GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    this.CurrentState = 0;
                    this.Builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, AsyncStateMachine>(ref this.awaiter, ref this);
                    return;
                }
            }
            string result = this.awaiter.GetResult();
            AfterAwaitTask(result);
        }
 
        void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
        {
            this.Builder.SetStateMachine(param0);
        }
    }

Espero que más o menos haya quedado clara la explicación.

¡Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *