Compartir a través de


Async y await

Procedimientos recomendados para la programación asincrónica

Stephen Cleary

 

Hoy en día existe abundante información sobre las nuevas palabras clave async y await en Microsoft .NET Framework 4.5. Este artículo pretende ser un “segundo paso” en el aprendizaje de la programación asincrónica: doy por entendido que ya leyó al menos un artículo introductorio sobre el tema. De hecho, este artículo no presenta nada nuevo, ya que estos mismos consejos se encuentran también en recursos en línea tales como Stack Overflow, foros de MSDN y preguntas más frecuentes sobre async y await. Este artículo destaca simplemente algunos procedimientos recomendados que podrían desaparecer dentro del caudal de documentación disponible.

Los procedimientos recomendados en este artículo son más bien “pautas” que reglas. Todos ellos tienen sus excepciones. Explicaré las razones que hay detrás de cada directriz, para que quede claro en qué situaciones se cumple y en cuáles no. Las directrices se resumen en la Figura 1; las analizaré una por una en las secciones siguientes.

Figura 1 Resumen de las directrices para la programación asincrónica

Name Descripción Excepciones
Evite async void Prefiera los métodos async Task en vez de los métodos async void Controladores de eventos
Async hasta el infinito No mezcle el código de bloqueo con código asincrónico Método main para consola
Contexto Configure Use ConfigureAwait(false) cuando pueda Métodos que requieren de contexto

Evite async void

Los métodos async tienen tres tipos posibles de retorno: Task, Task<T> y void, pero los tipos de retorno naturales para los métodos asincrónicos son solo Task y Task<T>. Al convertir código sincrónico en asincrónico, cualquier método que devuelva el tipo T se convierte en un método asincrónico que devuelve Task<T> y cualquier método que devuelva void se convierte en un método asincrónico que devuelve Task. El siguiente fragmento ilustra un método sincrónico que devuelve void, junto con el equivalente asincrónico:

void MyMethod()
{
  // Do synchronous work.
  Thread.Sleep(1000);
}
async Task MyMethodAsync()
{
  // Do asynchronous work.
  await Task.Delay(1000);
}

Los métodos asincrónicos que devuelven void tienen un propósito específico: permitir la creación de controladores de eventos asincrónicos. Es posible tener un controlador de eventos que devuelva un tipo concreto, pero esto no funciona bien con el lenguaje: invocar un controlador de eventos que devuelve un tipo resulta muy incómodo, y la sola idea de un controlador de eventos que devuelve algo no tiene mucho sentido. Los controladores de eventos naturalmente devuelven void: por esto mismo, los métodos asincrónicos devuelven void, para poder crear controladores de eventos asincrónicos. Sin embargo, la semántica de los métodos async void tiene diferencias sutiles de los métodos async Task y async Task<T>.

Los métodos async void tienen una semántica de control de errores diferente. Cuando se genera una excepción en un método async Task o async Task<T>, esa excepción se captura y se deja en el objeto Task. En los métodos async void, no hay ningún objeto Task. Por lo tanto, cualquier excepción que se genere en un método async void se generará directamente en el objeto SynchronizationContext que estaba activo cuando se inició el método async void. La Figura 2 ilustra las excepciones que se generan en los métodos async void que no se pueden atajar naturalmente.

Figura 2 Las excepciones de un método async void no se pueden atajar con catch

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

Estas excepciones se pueden observar con AppDomain.UnhandledException u otro evento genérico similar para las aplicaciones GUI/ASP.NET, pero el uso de estos eventos para el control de excepciones ciertamente conducirá a dificultades enormes en el mantenimiento.

Los métodos async void tienen una semántica de composición diferente. Los métodos async que devuelven Task o Task<T> se pueden componer fácilmente mediante await, Task.WhenAny, Task.WhenAll, etcétera. Los métodos async que devuelven void no ofrecen ninguna forma fácil para notificar al autor de la llamada sobre su finalización. No cuesta nada iniciar varios métodos async void, pero resulta difícil determinar cuándo estos terminan. Los métodos async void notificarán a su objeto SynchronizationContext cuando inician y terminan, pero una clase SynchronizationContext personalizada es una solución muy compleja para el código de aplicaciones corrientes.

Los métodos async void dificultan la implementación de pruebas. Debido a las diferencias en el control de errores y la composición, resulta difícil escribir pruebas unitarias que llamen los métodos async void. Las funciones de pruebas asincrónicas de MSTest solo funcionan con los métodos asincrónicos que devuelven Task o Task<T>. Se podría instalar una clase SynchronizationContext que detecte el momento cuando todos los métodos async void finalizaron y recopile todas las excepciones, pero resulta mucho más fácil permitir que los métodos async void devuelvan Task en vez de esto.

Queda claro que los métodos async void tienen varias desventajas en comparación con los métodos async Task, pero son bastante útiles en un caso específico: para los controladores de eventos asincrónicos. Las diferencias semánticas en este caso resultan útiles. Los controladores de eventos asincrónicos generan las excepciones directamente en el objeto SynchronizationContext, lo que se parece al comportamiento de los controladores de eventos sincrónicos. Además, generalmente son privados y por lo tanto no se pueden componer ni se prestan para las pruebas. Un método que me gusta personalmente es minimizar el código dentro del controlador de eventos asincrónico: por ejemplo, dejar que espere un método async Task que contiene la lógica propiamente tal. El siguiente código ilustra este método, donde se emplean métodos async void para los controladores de eventos, sin sacrificar la posibilidad de escribir pruebas:

private async void button1_Click(object sender, EventArgs e)
{
  await Button1ClickAsync();
}
public async Task Button1ClickAsync()
{
  // Do asynchronous work.
  await Task.Delay(1000);
}

Los métodos async void pueden causar estragos si el autor de la llamada no cuenta con que sean asincrónicos. Cuando el tipo devuelto es Task, el autor sabe que se trata de una operación futura; cuando el tipo devuelto es void, en cambio, el autor podría suponer que el método finalizó en el momento de la devolución. Este problema puede surgir de muchas formas imprevistas. Generalmente no es correcto entregar una implementación asincrónica (o reemplazo) de un método que devuelve void en una interfaz (o clase base). Algunos eventos también suponen que los controladores finalizaron en el momento de la devolución. Una trampa sutil se esconde cuando pasamos una función lambda asincrónica a un método que recibe un parámetro Action. En este caso, la función lambda asincrónica devuelve void y hereda todos los problemas de los métodos async void. Como regla general, las funciones lambda asincrónicas solo se deben usar si se convierten en un tipo delegado que devuelve Task (por ejemplo Func<Task>).

Para resumir esta primera directriz: prefiera async Task en vez de async void. Los métodos async Task ofrecen un control de errores más fácil, se componen mejor y se prestan mejor para las pruebas. La excepción a esta directriz son los controladores de eventos asincrónicos, que tienen que devolver void. Esta excepción incluye todos los métodos que cumplen la función lógica de controladores de eventos, incluso cuando no son controladores de eventos en el sentido literal (por ejemplo, las implementaciones de ICommand.Execute).

Async hasta el infinito

El código asincrónico me recuerda la historia de un científico que mencionó que el mundo estaba suspendido en el espacio y que inmediatamente fue impugnado por una señora de edad que afirmaba que el mundo descansaba sobre el lomo de una tortuga gigante. Cuando el hombre preguntó en qué estaba parada la tortuga, la señora respondió: “Usted es muy inteligente, joven, ¡pero hay infinitas tortugas una debajo de otra!” Cuando convierta código sincrónico en asincrónico, descubrirá que todo funciona mejor si el código asincrónico llama y es llamado por más código asincrónico: async hasta el infinito. Otros programadores también se han percatado del comportamiento expansivo de la programación asincrónica y la han llamado “contagiosa” o la han comparado el virus de los zombis. Sean tortugas o zombis, no podemos negar que el código asincrónico tiende a provocar que el código circundante también sea asincrónico. Este comportamiento es inherente a todos los tipos de programación asincrónica, no solo para las palabras clave nuevas async y await.

“Async hasta el infinito” significa que no se debe mezclar el código sincrónico con el asincrónico sin evaluar primero cuidadosamente las consecuencias. En concreto, por lo general no conviene bloquear el código asincrónico mediante una llamada a Task.Wait o Task.Result. Esto es un problema especialmente frecuente para los programadores que se están iniciando en la programación asincrónica, cuando convierten solo una pequeña parte de una aplicación y la encapsulan en una API asincrónica para aislar el resto de la aplicación de los cambios. Desgraciadamente, se topan con problemas de interbloqueos. Después de responder muchísimas preguntas relacionadas con async en los foros de MSDN, Stack Overflow y por correo electrónico, estoy en condiciones de afirmar que esta es una de las preguntas más frecuentes por parte de los novatos en async, cuando ya aprendieron lo básico: “¿Por qué mi código parcialmente asincrónico genera un interbloqueo?”

En la Figura 3 se muestra un ejemplo sencillo, donde un método se bloquea en el resultado de un método asincrónico. Este código funcionará perfectamente bien en una aplicación de consola, pero generará un interbloqueo cuando se llama desde el contexto de una interfaz gráfica o ASP.NET. Este comportamiento puede ser poco claro, sobre todo si tenemos en cuenta que al recorrer el código con el depurador se concluye que es el await el que no finaliza. La verdadera causa del interbloqueo se encuentra más arriba en la pila de llamadas, cuando se llama Task.Wait.

Figura 3 Un problema de interbloqueo común de bloqueo en el código asincrónico

public static class DeadlockDemo
{
  private static async Task DelayAsync()
  {
    await Task.Delay(1000);
  }
  // This method causes a deadlock when called in a GUI or ASP.NET context.
  public static void Test()
  {
    // Start the delay.
    var delayTask = DelayAsync();
    // Wait for the delay to complete.
    delayTask.Wait();
  }
}

La causa de este interbloqueo se encuentra en la forma en que await controla los contextos. De manera predeterminada, cuando se espera un Task incompleto, se captura el “contexto” actual y se usa para reanudar el método una vez que finaliza el Task. Este “contexto” es el objeto SynchronizationContext, a menos que sea nulo, en cuyo caso es el objeto TaskScheduler actual. El objeto SynchronizationContext de las aplicaciones ASP.NET y con interfaz de usuario gráfica solo permite que un fragmento de código se ejecute a la vez. Cuando await finaliza, intenta ejecutar el resto del método asincrónico dentro del contexto capturado. Pero ese contexto ya contiene un subproceso y este está esperando (en forma sincrónica) que el método asincrónico finalice. Cada uno espera al otro, lo que conduce al interbloqueo.

Observe que las aplicaciones de consola no generan este interbloqueo. Tienen un objeto SynchronizationContext para un grupo de subprocesos en vez de un SynchronizationContext que funciona con un solo fragmento a la vez. Por lo tanto, al finalizar el await, el resto del método asincrónico se programa en un subproceso del grupo de subprocesos. Así, el método puede finalizar, lo que finaliza la tarea devuelta sin que se genere ningún interbloqueo. Estos comportamientos diferentes pueden confundir a los programadores que escriben un programa de prueba para la consola, comprueban que el código parcialmente asincrónico funciona según lo previsto y luego trasladan el mismo código a una aplicación ASP.NET o con interfaz gráfica de usuario, donde se genera un interbloqueo.

La mejor solución para este problema es permitir que el código asincrónico crezca en forma natural por toda la base de código. Si se guía por esta solución, verá que el código asincrónico se expande hasta el punto de entrada, generalmente un controlador de eventos o acción del controlador. Las aplicaciones de consola no pueden respetar esta solución completamente, ya que el método Main no puede ser asincrónico. Si el método Main fuera asincrónico, podría devolverse antes de finalizar, lo que terminaría el programa. La Figura 4 ilustra esta excepción a la directriz: el método Main de las aplicaciones de consola es una de las pocas situaciones donde el código puede bloquearse con un método asincrónico.

Figura 4 El método Main puede llamar Task.Wait o Task.Result

class Program
{
  static void Main()
  {
    MainAsync().Wait();
  }
  static async Task MainAsync()
  {
    try
    {
      // Asynchronous implementation.
      await Task.Delay(1000);
    }
    catch (Exception ex)
    {
      // Handle exceptions.
    }
  }
}

Permitir que async crezca dentro de toda la base de código es la mejor solución, pero esto significa que hay que realizar una buena cantidad de trabajo inicial en la aplicación hasta obtener un beneficio real del código asincrónico. Existen algunas técnicas para convertir el código de un proyecto grande gradualmente en código asincrónico, pero están fuera del alcance de este artículo. En algunos casos, el uso de Task.Wait o Task.Result puede ayudar en una conversión parcial, pero debemos tener conciencia del problema del interbloqueo y del problema del control de errores. Explicaré el problema del control de errores ahora y mostraré cómo evitar el problema del interbloqueo más adelante.

Cada Task almacena una lista de excepciones. Cuando esperamos un Task, la primera excepción se vuelve a generar para que podamos atajar el tipo específico de la excepción (como, por ejemplo, InvalidOperationException). Sin embargo, cuando bloqueamos un Task en forma sincrónica con Task.Wait o Task.Result, todas las excepciones primero se encapsulan dentro de un objeto AggregateException y luego se generan. Vuelva a consultar la Figura 4. El bloque try/catch en MainAsync atajará un tipo de excepción específico, pero si colocamos el bloque try/catch en Main, entonces siempre atajará una instancia de AggregateException. El control de errores siempre es mucho más fácil cuando no tenemos que lidiar con AggregateException, así que dejé el bloque try/catch “global” en el método MainAsync.

Hasta ahora, he ilustrado dos problemas de bloqueo con el código asincrónico: interbloqueos posibles y control de errores más complicados. También existe un problema cuando se usa código de bloqueo dentro de un método asincrónico. Veamos este ejemplo sencillo:

public static class NotFullyAsynchronousDemo
{
  // This method synchronously blocks a thread.
  public static async Task TestNotFullyAsync()
  {
    await Task.Yield();
    Thread.Sleep(5000);
  }
}

Este método no es completamente asincrónico. Entregará la ejecución inmediatamente y devolverá una tarea incompleta, pero cuando se reanude bloqueará en forma sincrónica cualquier subproceso que se esté ejecutando. Si este método se llama desde un contexto de una interfaz gráfica de usuario, bloqueará el subproceso de la interfaz gráfica, y si se llama desde el contexto de una solicitud de ASP.NET, bloqueará el subproceso de solicitud de ASP.NET actual. El código asincrónico funciona mejor cuando no realiza bloqueos sincrónicos. En la Figura 5 se entrega una ayuda de memoria para los reemplazos asincrónicos para las operaciones sincrónicas.

Figura 5 La manera asincrónica de hacer las cosas

Para hacer esto… En vez de esto… Use esto
Recuperar el resultado de una tarea en segundo plano Task.Wait o Task.Result await
Esperar que finalice cualquier tarea Task.WaitAny await Task.WhenAny
Recuperar los resultados de varias tareas Task.WaitAll await Task.WhenAll
Esperar durante un tiempo determinado Thread.Sleep await Task.Delay

Para resumir esta segunda directriz, debe evitar mezclar el código asincrónico con el código de bloqueo. El código asincrónico y de bloqueo mixto puede generar interbloqueos, un control de errores más complejo y bloqueos inesperados de subprocesos de contexto. La excepción a esta directriz es el método Main de las aplicaciones de consola o, para los usuarios avanzados, las bases de código parcialmente asincrónicas.

Contexto Configure

Ya expliqué previamente cómo se captura el “contexto” de manera predeterminada cuando se espera un Task incompleto y que este contexto capturado se emplea para reanudar el método asincrónico. El ejemplo de la Figura 3 ilustra cómo la reanudación en el contexto entra en conflicto con el bloqueo sincrónico y genera un interbloqueo. Este comportamiento del contexto también puede provocar otro problema: deficiencias en el rendimiento. A medida que las aplicaciones de interfaz gráfica de usuario crecen en tamaño, nos encontramos con que varias partes pequeñas de los métodos asincrónicos usan el subproceso de la interfaz gráfica como su contexto. Esto puede provocar una respuesta lenta, causada por “miles de heridas con hojas de papel”.

Para atenuar esto, espere el resultado de ConfigureAwait siempre que pueda. El siguiente fragmento ilustra el comportamiento predeterminado del contexto y el uso de ConfigureAwait:

async Task MyMethodAsync()
{
  // Code here runs in the original context.
  await Task.Delay(1000);
  // Code here runs in the original context.
  await Task.Delay(1000).ConfigureAwait(
    continueOnCapturedContext: false);
  // Code here runs without the original
  // context (in this case, on the thread pool).
}

Al usar ConfigureAwait, habilitamos un pequeño grado de paralelismo: Algunas partes del código se pueden ejecutar en forma paralela al subproceso de la interfaz gráfica de usuario, en vez de importunarlo constantemente con pequeñas tareas pendientes.

Aparte del rendimiento, ConfigureAwait tiene otro aspecto importante: Puede evitar los interbloqueos. Regresemos a la Figura 3; si agregáramos “ConfigureAwait(false)” a la línea en DelayAsync, entonces evitaríamos el interbloqueo. Esta vez, cuando finaliza la espera, se intenta ejecutar el resto del método asincrónico dentro del contexto del grupo de subprocesos. Así, el método puede finalizar, lo que finaliza la tarea devuelta sin que se genere ningún interbloqueo. Esta técnica resulta especialmente útil cuando debemos convertir gradualmente una aplicación sincrónica en asincrónica.

Si puede usar ConfigureAwait en algún lugar dentro de un método, entonces le recomiendo que lo use en todos los await del método de ahí en adelante. Recuerde que el contexto se captura solo si se espera un Task incompleto; si Task ya finalizó, entonces el contexto no se captura. Algunas tareas podrían finalizar antes de lo esperado en diferentes situaciones de hardware y de red: y usted debe controlar en forma predecible las tareas que finalicen antes de lo esperado. En la Figura 6 vemos un ejemplo modificado.

Figura 6 Control de una tarea devuelta que finaliza antes de lo esperado

async Task MyMethodAsync()
{
  // Code here runs in the original context.
  await Task.FromResult(1);
  // Code here runs in the original context.
  await Task.FromResult(1).ConfigureAwait(continueOnCapturedContext: false);
  // Code here runs in the original context.
  var random = new Random();
  int delay = random.Next(2); // Delay is either 0 or 1
  await Task.Delay(delay).ConfigureAwait(continueOnCapturedContext: false);
  // Code here might or might not run in the original context.
  // The same is true when you await any Task
  // that might complete very quickly.
}

No debe usar ConfigureAwait cuando tiene código después del await en el método que necesita el contexto. Para las aplicaciones con interfaz gráfica de usuario, esto incluye cualquier código que manipule los elementos de la interfaz gráfica, escriba propiedades enlazadas a datos o que dependa de un tipo propio de la interfaz gráfica como, por ejemplo, Dispatcher o CoreDispatcher. En el caso de las aplicaciones ASP.NET, esto incluye cualquier código que use HttpContext.Current o cree una respuesta de ASP.NET, incluidas las instrucciones return en las acciones de los controladores. La Figura 7 ilustra un patrón común en las aplicaciones de interfaz gráfica de usuario: un controlador de evento asincrónico que desactiva el control al principio del método para realizar algunos await y luego vuelve a habilitar el control al final del controlador; el controlador de eventos no puede ceder el contexto, ya que tiene que volver a habilitar el control.

Figura 7 Un controlador de eventos asincrónico que desactiva y vuelve a activar el control

private async void button1_Click(object sender, EventArgs e)
{
  button1.Enabled = false;
  try
  {
    // Can't use ConfigureAwait here ...
    await Task.Delay(1000);
  }
  finally
  {
    // Because we need the context here.
    button1.Enabled = true;
  }
}

Cada método asincrónico tiene su propio contexto, así que si un método asincrónico llama otro método asincrónico, los dos tienen contextos independientes. En la Figura 8 vemos una modificación menor de la Figura 7.

Figura 8 Cada método asincrónico tiene su propio contexto

private async Task HandleClickAsync()
{
  // Can use ConfigureAwait here.
  await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
}
private async void button1_Click(object sender, EventArgs e)
{
  button1.Enabled = false;
  try
  {
    // Can't use ConfigureAwait here.
    await HandleClickAsync();
  }
  finally
  {
    // We are back on the original context for this method.
    button1.Enabled = true;
  }
}

El código sin contexto se puede reutilizar mejor. Intente agregar una barrera en el código, entre el código que depende del contexto y el código sin contexto, y de minimizar el código que depende del contexto. En la Figura 8, recomiendo que junte toda la lógica central del controlador de eventos dentro de un método Task asincrónico sin contexto que se pueda probar y que solo deje el código mínimo dentro del controlador de eventos que depende del contexto. Incluso cuando escriba una aplicación ASP.NET, si tiene una biblioteca central que se podría compartir con aplicaciones de escritorio, considere la posibilidad de usar ConfigureAwait en el código de la biblioteca.

Para resumir esta tercera directriz: debe usar Configure­Await siempre que sea posible. El código libre de contexto tiene un rendimiento mejor en las aplicaciones de interfaz gráfica de usuario y es una técnica útil para evitar los interbloqueos cuando se trabaja con una base de código parcialmente asincrónica. Las excepciones a esta directriz son los métodos que requieren del contexto.

Conozca sus herramientas

Hay mucho que aprender sobre async y await, y es normal desorientarse un poco. En la Figura 9 se entrega una referencia rápida con soluciones para problemas frecuentes.

Figura 9 Soluciones para problemas frecuentes con async

Problema Solución
Crear una tarea para ejecutar código Task.Run o TaskFactory.StartNew (no el constructor de Task ni Task.Start)
Crear un contenedor de tareas para una operación o evento TaskFactory.FromAsync o TaskCompletionSource<T>
Permitir la cancelación CancellationTokenSource y CancellationToken
Informar el progreso IProgress<T> y Progress<T>
Trabajar con flujos de datos TPL Dataflow o Extensiones reactivas
Sincronizar el acceso a un recurso compartido SemaphoreSlim
Inicializar un recurso en forma asincrónica AsyncLazy<T>
Estructuras productoras y consumidoras listas para async TPL Dataflow o AsyncCollection<T>

El primer problema es la creación de tareas. Evidentemente, un método asincrónico puede crear una tarea y eso es la opción más fácil. Si tiene que ejecutar código en la pila de subprocesos, entones use Task.Run. Si quiere crear un contenedor de tareas para una operación o evento asincrónico existente, use TaskCompletionSource<T>. El siguiente problema más común es cómo realizar la cancelación e informar el progreso. La biblioteca de clases base (BCL) incluye tipos especiales para solucionar estos problemas: CancellationTokenSource/CancellationToken y IProgress<T>/Progress<T>. El código debe usar el patrón asincrónico basado en tareas (TAP, msdn.microsoft.com/library/hh873175), que explica en detalle la creación de tareas, cancelación y cómo informar el progreso.

Otro problema que aparece es qué hacer con las transmisiones de datos asincrónicos. Las tareas son muy útiles, pero solo pueden devolver un objeto y solo pueden terminar una vez. Para las transmisiones asincrónicas, puede usar Dataflow de la biblioteca TPL o las Extensiones reactivas (Rx). TPL Dataflow crea una “malla” que tiene un comportamiento similar a un actor. Rx es más poderoso y eficiente, pero es más difícil de aprender. Tanto TPL Dataflow como Rx tienen métodos listos para async y funcionan bien con el código asincrónico.

Solo porque su código es asincrónico no significa que sea seguro. Igual debe preocuparse por proteger los recursos compartidos y esto se vuelve más difícil debido a que no podemos esperar desde el interior de un bloqueo. Aquí vemos un ejemplo de código asincrónico que puede dañar un estado compartido si se ejecuta dos veces, aunque siempre se ejecute en el mismo subproceso:

int value;
Task<int> GetNextValueAsync(int current);
async Task UpdateValueAsync()
{
  value = await GetNextValueAsync(value);
}

El problema es que el método lee el valor y se suspende en el momento del await y, cuando se reanuda, el valor no cambió. Para solucionar este problema, la clase SemaphoreSlim se extendió con las sobrecargas WaitAsync que funcionan en las situaciones asincrónicas. La Figura 10 ilustra el método SemaphoreSlim.WaitAsync.

Figura 10 SemaphoreSlim permite la sincronización asincrónica

SemaphoreSlim mutex = new SemaphoreSlim(1);
int value;
Task<int> GetNextValueAsync(int current);
async Task UpdateValueAsync()
{
  await mutex.WaitAsync().ConfigureAwait(false);
  try
  {
    value = await GetNextValueAsync(value);
  }
  finally
  {
    mutex.Release();
  }
}

El código asincrónico se emplea frecuentemente para inicializar un recurso que luego se almacena en caché y se comparte. No existe ningún tipo integrado para esto, pero Stephen Toub desarrolló el tipo AsyncLazy<T> que actúa como una mezcla de Task<T> y Lazy<T>. El tipo original está descrito en su blog (bit.ly/dEN178) y una versión actualizada está disponible en mi biblioteca AsyncEx (nitoasyncex.codeplex.com).

Por último, a veces necesitamos algunas estructuras de datos que funcionen con async. TPL Dataflow ofrece el tipo BufferBlock<T>, que actúa como una cola consumidor/productor que funciona con async. Otra posibilidad es AsyncEx, que entrega el tipo AsyncCollection<T>, una versión asincrónica de BlockingCollection<T>.

Espero que estas directrices y pistas le hayan servido. Async es una característica realmente maravillosa y ahora es un excelente momento para comenzar a usarlo.

Stephen Cleary es esposo, padre y programador, y vive en el norte de Michigan. Trabaja en la programación multithreading y asincrónica desde hace 16 años y ha usado las funciones asincrónicas de Microsoft .NET Framework desde la primera CTP. Su página principal y su blog se encuentran en stephencleary.com.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Stephen Toub
Stephen Toub trabaja en el equipo de Visual Studio en Microsoft. Se especializa en las áreas relacionadas con el paralelismo y la asincronía.