MSDN Magazine > Inicio > Todos los números > 2008 > Agosto >  Operaciones simultáneas: Más características de...
Operaciones simultáneas
Más características de AsyncEnumerator
Jeffrey Richter
Descarga de código disponible en: ConcurrentAffairs2008_08.exe (155 KB)
Examinar el código en línea
En mi columna de Operaciones simultáneas de noviembre de 2007, expuse la idea de usar las características del lenguaje C# para simplificar la programación asincrónica (consulte msdn.microsoft.com/magazine/cc163323). En particular, me centré en los métodos anónimos, las expresiones lambda y los iteradores. Más tarde, en mi columna de junio de 2008, presenté la clase AsyncEnumerator y expliqué cómo podía usarse para controlar un iterador de C# (consulte msdn.microsoft.com/magazine/cc546608). También hablé de la arquitectura de Async­Enumerator y expliqué cómo funciona de manera interna.
En esta columna quiero mostrar algunas características adicionales que ofrece AsyncEnumerator, como la unión de diversas operaciones asincrónicas simultáneas, la compatibilidad con el modelo de programación asincrónica (APM), los valores devueltos, el control del subproceso de devolución de llamadas, el acceso sincronizado a datos compartidos, operaciones de descarte automático de operaciones que no han sido completadas y la compatibilidad con la cancelación o el tiempo de espera. En ese artículo, también veremos algunas pautas de programación comunes que han sido posibles gracias a AsyncEnumerator.

Unión de operaciones asincrónicas simultáneas
Una de las grandes virtudes de la ejecución de operaciones asincrónicas es que pueden ejecutarse diversas operaciones al mismo tiempo, lo cual permite mejorar notablemente el rendimiento de las aplicaciones. Por ejemplo, si se inician tres solicitudes asincrónicas de servicio web al mismo tiempo y cada solicitud tarda 5 segundos en completarse, el tiempo total que el programa debe esperar es de 5 segundos. Por otro parte, si se hacen solicitudes sincrónicas de servicio web, la aplicación debe esperar a que una se complete para poder iniciarse la siguiente. Por lo tanto, si se hacen tres solicitudes sincrónicas de servicio web cuyo tiempo de ejecución es de 5 segundos para cada una, significa que la aplicación debe esperar al menos 15 segundos.
Con AsyncEnumerator, iniciar varias operaciones asincrónicas al mismo tiempo es muy fácil. El código puede procesar las operaciones completadas una vez que todas hayan finalizado, o bien puede hacerlo a medida que cada una finalice. El iterador de la figura 1 muestra las dos técnicas para el procesamiento de operaciones completadas. Cuando lo ejecuté, obtuve el siguiente resultado (observe que debe esperar a que la solicitud web agote el tiempo de espera):
All the operations completed:
   Uri=http://wintellect.com/        ContentLength=41207
   Uri=http://www.devscovery.com/    ContentLength=13258
   Uri=http://1.1.1.1/  WebException=Unable to connect to remote server
An operation completed:
   Uri=http://wintellect.com/        ContentLength=41208
An operation completed:
   Uri=http://www.devscovery.com/    ContentLength=13258
An operation completed:
   Uri=http://1.1.1.1/   WebException=Unable to connect to remote server
public static class AsyncEnumeratorPatterns {
  public static void Main() {
    String[] urls = new String[] { 
      "http://Wintellect.com/", 
      "http://1.1.1.1/",   // Demonstrates error recovery
      "http://www.Devscovery.com/" 
    };

    // Demonstrate process
    AsyncEnumerator ae = new AsyncEnumerator();
    ae.Execute(ProcessAllAndEachOps(ae, urls));
  }

  private static IEnumerator<Int32> ProcessAllAndEachOps(
       AsyncEnumerator ae, String[] urls) {
    Int32 numOps = urls.Length;

    // Issue all the asynchronous operation(s) so they run concurrently
    for (Int32 n = 0; n < numOps; n++) {
      WebRequest wr = WebRequest.Create(urls[n]);
      wr.BeginGetResponse(ae.End(), wr);
    }

    // Have AsyncEnumerator wait until ALL operations complete
    yield return numOps;

    Console.WriteLine("All the operations completed:");
    for (Int32 n = 0; n < numOps; n++) {
      ProcessCompletedWebRequest(ae.DequeueAsyncResult());
    }

    Console.WriteLine(); // *** Blank line between demos ***

    // Issue all the asynchronous operation(s) so they run concurrently
    for (Int32 n = 0; n < numOps; n++) {
      WebRequest wr = WebRequest.Create(urls[n]);
      wr.BeginGetResponse(ae.End(), wr);
    }

    for (Int32 n = 0; n < numOps; n++) {
      // Have AsyncEnumerator wait until EACH operation completes
      yield return 1;

      Console.WriteLine("An operation completed:");
      ProcessCompletedWebRequest(ae.DequeueAsyncResult());
    }
  }

  private static void ProcessCompletedWebRequest(IAsyncResult ar) {
    WebRequest wr = (WebRequest)ar.AsyncState;
    try {
      Console.Write("   Uri=" + wr.RequestUri + "    ");
      using (WebResponse response = wr.EndGetResponse(ar)) {
        Console.WriteLine("ContentLength=" + response.ContentLength);
      }
    }
    catch (WebException e) {
      Console.WriteLine("WebException=" + e.Message);
    }
    }
}
El código que hay al principio del iterador emite diversas operaciones asincrónicas y, a continuación, ejecuta una instrucción numOps yield return. Esta instrucción le indica a AsyncEnumerator que no vuelva a llamar al código hasta que todas las operaciones indicadas mediante el valor de la variable numOps se hayan completado. El código que aparece justo debajo de la instrucción yield return ejecuta un bucle que procesará todas las operaciones completadas.
Observe que las operaciones pueden completarse en un orden diferente del que fueron emitidas. Para establecer la correlación de los resultados con su correspondiente solicitud, pasé wr, que identifica el objeto WebRequest que se usó para iniciar la solicitud, en el último argumento de BeginGetResponse. A continuación, con el método de ProcessCompletedWebRequest, extraje el objeto Web­Request que había usado para iniciar la solicitud a partir de la propiedad AsyncState de IAsyncResult.
El código que aparece debajo del iterador también emite varias operaciones asincrónicas y, a continuación, introduce un bucle para procesar cada una de ellas a medida que se van completando. Sin embargo, el iterador debe esperar antes a que las operaciones se completen una a una. Esto se consigue con la instrucción yield return 1, que le indica a AsyncEnumerator que vuelva a llamar al código del iterador en cuanto se completa una operación.

Compatibilidad con APM
En la última columna expliqué cómo, al llamar al método Execute de Async­Enumerator, se inicia la ejecución de un código del iterador. Sin embargo, también expliqué que el subproceso que llama a Execute se bloquea hasta que el iterador se cierra o ejecuta una instrucción yield break.
El bloqueo de subprocesos perjudica la escalabilidad de las aplicaciones, lo cual es algo que seguramente deseará evitar, en especial con las aplicaciones de servidor. Asimismo, perjudica la capacidad de respuesta si éste fue llamado a través de un subproceso de la GUI, ya que el iterador tarda un período de tiempo indefinido antes de ejecutarse, y durante ese tiempo una aplicación Windows® Forms o de Windows Presentation Foundation (WPF) detendrá las respuestas a entradas. Ciertamente, deseará llamar a Execute sólo cuando escriba código de prueba o cuando experimente con un método del iterador.
Para códigos de producción, se recomienda llamar a los métodos Begin­Execute y EndExecute de AsyncEnumerator. Internamente, cuando se llamar a Begin­Execute, el objeto AsyncEnumerator construye una instancia de la clase AsyncResultNoResult, sobre la que ya hablé en el número de marzo de 2007 de MSDN® Magazine (msdn.microsoft.com/magazine/cc163467). Al llamar a BeginExecute, podrá pasar una referencia a su propio método AsyncCallback y el objeto AsyncEnumerator invocará a este método cuando el iterador acabe de ejecutarse. Este método debería llamar entonces al método EndExecute de AsyncEnumerator para recibir los resultados del iterador. Más adelante mostraré algunos ejemplos en los que aprovecho las ventajas de los métodos BeginExecute y EndExecute. Vea cómo son los métodos:
public class AsyncEnumerator<TResult>: AsyncEnumerator {
  public IAsyncResult BeginExecute(
    IEnumerator<Int32> enumerator,
    AsyncCallback callback, Object state);

  public void EndExecute(IAsyncResult result);
}
Además, puesto que AsyncEnumerator es compatible con APM, puede integrarse con todos los modelos de aplicaciones de Microsoft® .NET Framework existentes, ya que estos ya son compatibles con APM. Esto significa que AsyncEnumerator puede usarse con aplicaciones Web Form de ASP.NET, servicios web XML de ASP.NET, servicios de Windows Communication Foundation (WCF), aplicaciones de Windows Forms, aplicaciones de WPF, aplicaciones de consola, servicios de Windows, etc.
Otro aspecto que cabe mencionar es que, puesto que Async­Enumerator es compatible con APM, puede usarse dentro de otro iterador y ello permite la composición de operaciones asincrónicas. Por ejemplo, el usuario podría implementar un iterador que sabe hacer solicitudes a la base de datos de manera asincrónica y luego, una vez se reciban los resultados, procesarlos. Yo lo llamo el iterador de subrutina. Por tanto, dentro de otro iterador, el usuario podría iniciar varias solicitudes a la base de datos invocando al iterador de subrutina mediante un bucle. Para cada iteración del bucle, construiría un AsyncEnumerator y llamaría al método BeginExecute, especificando el nombre del iterador de subrutina y algún otro argumento adicional que desee.
Observe que, con este modelo, se obtienen beneficios importantes: todos los iteradores de subrutina se ejecutan al mismo tiempo sin bloquear ningún subproceso (a menos que la implementación subyacente del APM bloquee los subprocesos, como, por ejemplo, si se implementase BeginXxx para enviar a la cola a un delegado para el ThreadPool que quedó bloqueado hasta completarse alguna operación). Esto permite al usuario crear un iterador sencillo que encapsula una sola operación asincrónica y la invoca desde dentro de otros iteradores, permitiéndole al mismo tiempo mantener la escalabilidad y capacidad de respuesta.

Valores devueltos
En muchas situaciones, que el iterador nos devuelva un resultado tras haber completado todos los procesos es útil. Sin embargo, un iterador no puede devolver un valor cuando ha finalizado porque los iteradores no pueden contener instrucciones return. Además, las instrucciones yield return devuelven un valor para cada iteración y no valores finales.
Para que un iterador devuelva un valor final una vez que ha finalizado todos los procesos, he creado la clase auxiliar AsyncEnumerator<TResult>. El modelo para esta clase se muestra a continuación:
public class AsyncEnumerator<TResult>: AsyncEnumerator {
  public AsyncEnumerator();
  public TResult Result { get; set; }

  new public TResult Execute(IEnumerator<Int32> enumerator);
  new public TResult EndExecute(IAsyncResult result);
}
El uso de AsyncEnumerator<TResult> es sencillo. Primero, cambie el código para crear una instancia de AsyncEnumerator<TResult> en lugar del AsyncEnumerator normal. Para TResult, especifique el tipo que desea que el iterador devuelva al final. A continuación, modifique la parte de su código que llama a los métodos Execute o EndExecute (que no solían devolver resultados) para obtener el valor devuelto y usarlo comoquiera que desee.
Después, modifique el código del iterador de forma que acepte un AsyncEnumerator<TResult> en lugar de un AsyncEnumerator. Y, por supuesto, deberá especificar los mismos datos para el parámetro genérico TResult. Por último, dentro de su código del iterador, configure la propiedad Result del objeto AsyncEnumerator<TResult> para cualquier valor que desea que el iterador devuelva.
Para ayudarle a entender todo esto, la figura 2 muestra código que implementa un sencillo y asincrónico servicio web de ASP.NET que solicita simultáneamente HTML para diversos sitios web diferentes (especificados como una cadena delimitada por comas). Una vez recibidos todos los datos del sitio web, éste devuelve una matriz de cadenas donde cada elemento muestra la dirección URL del sitio web, el número de bytes descargados o bien un error, si es que se produjo alguno.
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Service : System.Web.Services.WebService {
  private static List<String[]> s_recentRequests = new List<String[]>(10);
  private static SyncGate s_recentRequestsSyncGate = new SyncGate();

  private AsyncEnumerator<String[]> m_webSiteDataLength;

  [WebMethod]
  public IAsyncResult BeginGetWebSiteDataLength(
    String uris, AsyncCallback callback, Object state) {

    // Construct an AsyncEnumerator that will eventually return a String[]
    m_webSiteDataLength = new AsyncEnumerator<String[]>();

    // NOTE: The AsyncEnumerator automatically saves the ASP.NET 
    // SynchronizationContext with it ensuring that the iterator 
    // always executes using the correct IPrincipal, 
    // CurrentCulture, and CurrentUICulture.

    // Initiate the iterator asynchronously. 
    return m_webSiteDataLength.BeginExecute(
      GetWebSiteDataLength(m_webSiteDataLength, uris.Split(',')), 
                          callback, state);
    // NOTE: Since the AsyncEnumerator's BeginExecute method returns an 
    // IAsyncResult, we can just return this back to ASP.NET
  }

  private IEnumerator<Int32> GetWebSiteDataLength(
             AsyncEnumerator<String[]> ae, String[] uris) {

    // Issue several web request simultaneously
    foreach (String uri in uris) {
      WebRequest webRequest = WebRequest.Create(uri);
      webRequest.BeginGetResponse(ae.End(), webRequest);
    }

    yield return uris.Length;  // Wait for ALL the web requests to complete

    // Construct the String[] that will be the ultimate result
    ae.Result = new String[uris.Length];

    for (Int32 n = 0; n < uris.Length; n++) {
      // Grab the result of a completed web request
      IAsyncResult result = ae.DequeueAsyncResult();

      // Get the WebRequest object used to initate the request 
      WebRequest webRequest = (WebRequest)result.AsyncState;

      // Build the String showing the result of this completed web request
      ae.Result[n] = "URI=" + webRequest.RequestUri + ", ";

      using (WebResponse webResponse = webRequest.EndGetResponse(result)) {
        ae.Result[n] += "ContentLength=" + webResponse.ContentLength;
      }
    }

    // Modify the collection of most-recent queries
    s_recentRequestsSyncGate.BeginRegion(SyncGateMode.Exclusive, ae.End());
    yield return 1;   // Continue when collection can be updated (modified)

    // If collection is full, remove the oldest item
    if (s_recentRequests.Count == s_recentRequests.Capacity)
      s_recentRequests.RemoveAt(0);

    s_recentRequests.Add(ae.Result);
    s_recentRequestsSyncGate.EndRegion(ae.DequeueAsyncResult());   
  // Updating is done //
  }

  // ASP.NET calls this method when the iterator completes. 
  [WebMethod]
  public String[] EndGetWebSiteDataLength(IAsyncResult result) {
    return m_webSiteDataLength.EndExecute(result);
  }

  private AsyncEnumerator<String[][]> m_aeRecentRequests;

  [WebMethod]
  public IAsyncResult BeginGetRecentRequests(AsyncCallback callback, 
                                             Object state) {
    m_aeRecentRequests = new AsyncEnumerator<String[][]>();
    return m_aeRecentRequests.BeginExecute(GetRecentRequests(m
       _aeRecentRequests), callback, state);
  }

  private IEnumerator<Int32> GetRecentRequests(
     AsyncEnumerator<String[][]> ae) {
    // In a shared way, read the collection of most-recent requests
    s_recentRequestsSyncGate.BeginRegion(SyncGateMode.Shared, ae.End());
    yield return 1;   // Continue when collection can be examined (read)

    // Return a copy of the collection as an array
    ae.Result = s_recentRequests.ToArray();
    s_recentRequestsSyncGate.EndRegion(ae.DequeueAsyncResult());  
// Reading is done
  }

  [WebMethod]
  public String[][] EndGetRecentRequests(IAsyncResult result) {
    return m_aeRecentRequests.EndExecute(result);
  }
}

Control del subproceso de devolución de llamada en contextos de sincronización
A medida que las operaciones asincrónicas se van completando, se activan diversos subprocesos del grupo de subprocesos para notificar su objeto AsyncEnumerator. Si Async­Enumerator usó estos subprocesos para devolver la llamada al iterador, el código del iterador podrá ejecutarse a través de diferentes subprocesos aunque sólo contenga un subproceso ejecutándose cada vez. En algunas situaciones, tener subprocesos diferentes ejecutando el código del iterador presenta problemas. Por ejemplo, en una aplicación de Windows Forms o de WPF, un control debe ser manipulado por el subproceso que creó el control y éste no puede ser un subproceso del grupo de subprocesos.
Hacer que el código del iterador se ejecute mediante subprocesos arbitrarios del grupo de subprocesos puede suponer otro problema. Con una aplicación de ASP.NET, por ejemplo, cuando una solicitud de cliente entra primero, ASP.NET asocia el IPrincipal del cliente (para la suplantación) y la información de referencia cultural con las propiedades CurrentPrincipal, Current-Culture y CurrentUICulture del subproceso del grupo de subprocesos. Sin embargo, si usa este subproceso para llamar a algún método BeginXxx, cuando el nuevo subproceso del grupo de subprocesos se ejecuta para notificarle la finalización de la operación, no tendrá estas propiedades configuradas correctamente de forma predeterminada.
Para poder resolver estos problemas, el CLR permite que cada subproceso disponga de un objeto derivado de SynchronizationContext asociado. Este objeto se usa para ayudar a mantener el modelo de subprocesos empleado por un modelo de aplicación. Para Windows Forms y WPF, los objetos derivados de SynchronizationContext saben cómo efectuar el cálculo de referencias de una llamada a función del subproceso de la GUI (realizada por un subproceso del grupo de subprocesos). Con respecto a ASP.NET, el objeto derivado de SynchronizationContext sabe cómo inicializar las propiedades de entidad principal de seguridad y referencia cultural en cada subproceso del grupo de subprocesos usado para procesar una sola solicitud.
Para mantener el modelo adecuado de subprocesos para la aplicación, AsyncEnumerator proporciona la propiedad Sync-Context. Esta propiedad se inicializa para el valor devuelto a través de la propiedad Current estática de Synchronization­Context dentro del constructor de Async­Enumerator. Si se trata de un valor nulo —como pasaría normalmente con una aplicación de la consola o de un servicio de Windows—, entonces, siempre que un subproceso del grupo de subprocesos llama al objeto AsyncEnumerator, este objeto usa sólo este subproceso para llamar al iterador. Sin embargo, si la propiedad SyncContext no contiene un valor nulo, el objeto AsyncEnumerator hace que el subproceso del grupo de subprocesos llame a su iterador a través del objeto derivado de SynchronizationContext.
Por tanto, para una aplicación de Windows Forms y de WPF, esto significa que el código del iterador se ejecutará siempre a través del subproceso de la GUI y, por tanto, sólo podrá ejecutar código en su iterador para actualizar los controles en el formulario. No es necesario llamar a los métodos Invoke/BeginInvoke de Control ni a los métodos Invoke/BeginInvoke de Dispatcher. Esto hace que sea sencillo que su iterador vaya proporcionando información a su IU a medida que las operaciones asincrónicas se van completando. Para ASP.NET, esto significa que las propiedades de entidad principal de seguridad y referencia cultural siempre se configurarán correctamente al ejecutarse el código del iterador. El código de la figura 2 y el ejemplo de Windows Forms que mostraré más adelante aprovechan las ventajas de esta característica.

Sincronización de acceso a datos compartidos
En algunas situaciones, especialmente en escenarios de servidor, puede tener múltiples objetos de AsyncEnumerator (uno por solicitud de cliente) procesando sus propios iteradores simultáneamente. Por ejemplo, imagine un sitio web que tenga acceso a alguna base de datos y actualiza un conjunto de objetos en memoria. Seguro que deseará usar el APM para obtener acceso a la base de datos (por ejemplo, llamando a BeginExecuteReader de SqlCommand) y, a continuación, quizás necesite actualizar los objetos en memoria de una manera que sea segura para los subprocesos. Normalmente usaría métodos de la clase Monitor o la instrucción de bloqueo de C#, o quizás Reader­WriterLockSlim, que se proporciona con .NET Framework 3.5. Sin embargo, todos estos bloqueos pueden bloquear el subproceso de llamada y perjudicar la escalabilidad y la capacidad de respuesta. Para evitar que los subprocesos se bloqueen, suelo usar mi ReaderWriterGate, el cual describí en mi columna de mayo de 2006 (consulte msdn.microsoft.com/magazine/cc163532).
Cuando empecé a usar ReaderWriterGate con AsyncEnumerator, me di cuenta de que el modelo de objetos de ReaderWriterGate podría mejorarse para optimizar la integración de AsyncEnumerator. Así que creé una clase nueva llamada SyncGate, que se comporta de forma muy parecida a ReaderWriterGate. Éste es el modelo:
   public sealed class SyncGate {
     public SyncGate();
     public void BeginRegion(SyncGateMode mode, 
       AsyncCallback asyncCallback); 
     public void EndRegion(IAsyncResult ar); 
   }
   public enum SyncGateMode { Exclusive, Shared }
Si tiene diversos iteradores ejecutándose que desean tener acceso a datos compartidos, primero construya un SyncGate y almacene una referencia a éste en un campo estático, o bien obtenga la referencia a éste de algún modo fuera de los diversos iteradores. A continuación, dentro de los iteradores, justo antes de que el código tenga acceso a los datos compartidos, llame al método Begin­Region de SyncGate e indíquele si el código requiere obtener un acceso a los datos exclusivo (de escritura) o compartido (de lectura). A continuación, tome el iterador yield return 1. Llegados a este punto, su iterador dejará su subproceso y, cuando pueda tener acceso a los datos sin riesgos, AsyncEnumerator volverá a llamar a su código de manera automática. Esto significa que los subprocesos no se bloquearán mientras esperan obtener acceso a los datos compartidos.
Desde su iterador, poco después de que el código que obtiene acceso a los datos compartidos, llame a EndRegion. Éste le indicará a SyncGate que su código ha finalizado el proceso de acceso a los datos compartidos y permitirá que otros iteradores puedan obtener acceso si así lo desean. En la figura 2, la parte inferior del iterador GetWebSiteDataLength usa SyncGate para obtener acceso exclusivo a una recopilación estática. También en la figura 2, el iterador GetRecentRequests muestra cómo obtener acceso compartido a la misma recopilación.

Grupos de descarte
Otra característica que ofrece la clase AsyncEnumerator es la de los grupos de descarte. Esta característica permite que su iterador emita varias operaciones asincrónicas simultáneas y luego decida que no tiene interés en algunas (o en ninguna). De esta forma hará que el objeto AsyncEnumerator descarte los resultados automáticamente a medida que se vayan completando el resto de las operaciones.
Por ejemplo, imagine un código que desea obtener la temperatura de una ciudad. Existen muchos servicios web que podría consultar para adquirir esta información. El usuario podría escribir un iterador que consulte a tres servicios web para obtener esa información y, tan pronto como alguno de ellos le devuelva la temperatura, puede descartar los resultados de los otros dos servicios web. Algunas personas usan este patrón para mejorar el rendimiento de sus aplicaciones.
Otro ejemplo muestra cuando un iterador lleva a cabo una secuencia de operaciones y el usuario desea cancelarlas porque se ha cansado de esperar o simplemente ha cambiado de opinión. Una situación parecida se da cuando el usuario inicia una operación asincrónica pero, si no se ha finalizado en un período de tiempo especificado, éste desea descartar todas las operaciones todavía no completadas. Por ejemplo, algunos servicios web procesan la solicitud de un cliente y, si todo el proceso no puede completarse en, por ejemplo, dos segundos, este servicio podría optar por notificar al cliente que su solicitud generó un error antes que hacerle esperar indefinidamente una respuesta.
A continuación se muestra cómo se usan los grupos de descarte: dentro de su iterador, el usuario agrupa numerosas operaciones relacionadas como parte de un grupo descarte. Un grupo de descarte es sólo un valor Int32 que va de 0 a 63, ambos incluidos. Por ejemplo, el usuario puede emitir numerosos métodos BeginXxx indicando que forman parte del grupo de descarte 0. A continuación, su iterador podrá procesarlos a medida que se vayan completando. Cuando, en el código del iterador, el usuario decide que ya no desea procesar las operaciones que forman parte de este grupo de descarte, llama al método DiscardGroup de Async­Enumerator:
public void DiscardGroup(Int32 discardGroup);
Este método le indica al objeto AsyncEnumerator que descarte cualquier operación pendiente de finalización y que fuese emitida como parte del grupo de descarte especificado. De esta forma, el código del iterador nunca consultará ninguna de estas operaciones cuando llame a DequeueAsyncResult. Lamentablemente, este método no acaba de ser efectivo del todo, ya que el APM de .NET obliga a que los métodos EndXxx reciban una llamada de cada método BeginXxx o los recursos podrían perderse.
Para tratar este requisito, AsyncEnumerator debe llamar al método EndXxx adecuado para cualquier operación que éste descarte. Puesto que no hay forma de que AsyncEnumerator pueda escoger el método EndXxx adecuado para llamar por sí mismo, debe indicar cuál es el método que desea usar. Al llamar a un método BeginXxx, en lugar de especificar simplemente ae.End para el argumento AsyncCallback, el usuario debe especificar uno de estos métodos:
AsyncCallback End(Int32 discardGroup, EndObjectXxx callback);
AsyncCallback EndVoid(Int32 discardGroup, EndVoidXxx callback);
EndObjectXxx y EndVoidXxx son delegados que se definen de la siguiente manera:
delegate Object EndObjectXxx(IAsyncResult result);
delegate void EndVoidXxx(IAsyncResult result);
Si un método BeginXxx tiene un método EndXxx correspondiente que devuelve un valor, entonces el usuario llamará al método End que se acaba de mostrar. Si se llama a un método BeginXxx que tiene un método EndXxx correspondiente que devuelve un valor nulo (caso excepcional), entonces se llamaría al método EndVoid. Ahora, siempre que se le indique a AsyncEnumerator que descarte un grupo, sabrá a qué método EndXxx deberá llamar.
Observe que si el método EndXxx genera alguna excepción en algún momento, AsyncEnumerator la detectará y la absorberá. Lo hace porque, si el usuario descarta la operación, estará indicando que no tiene interés en si la operación se realiza correctamente o en si se genera un error.
También debo señalar que cuando el iterador del usuario se cierra o ejecuta una instrucción yield break, AsyncEnumerator descarta automáticamente todos los grupos de descarte, ya que, si el usuario cierra el iterador, estará indicándole que no tiene interés en ninguna de las operaciones que todavía no han sido procesadas. Esto puede ser muy cómodo porque permite que el iterador emita algunas operaciones asincrónicas, procese tantas operaciones completadas como necesite y, a continuación, simplemente se cierre.
AsyncEnumerator limpia automáticamente las operaciones que quedan pendientes de completarse más adelante. Observe, sin embargo, que estas operaciones de descarte no las cancelan. Si una de las operaciones asincrónicas que quedan pendientes estaba escribiendo en un archivo o actualizando alguna base de datos, el hecho de descartar los grupos correspondientes no evitará que estas operaciones se completen. Permitirá simplemente que el código continúe sin tener en cuenta la finalización de las operaciones.

Cancelación
AsyncEnumerator permite que código externo cancele el iterador durante su procesamiento. Esta característica es especialmente útil para aplicaciones de Windows Forms y de WPF, ya que permite al usuario impaciente cancelar una operación en curso y recuperar el control de la aplicación. AsyncEnumerator también tiene la capacidad de cancelarse tras un período de tiempo determinado. Esta característica resulta útil en aplicaciones de servidor que desean configurar límites para el período de tiempo que se tarda en responder a la solicitud de un cliente. A continuación se muestran los métodos relacionados con la característica de la cancelación:
public class AsyncEnumerator {
  // Call this method from inside the iterator
  public Boolean IsCanceled(out Object cancelValue);

  // Call this method from inside the iterator
  public Boolean IsCanceled();

  // Call this method from code outside the iterator
  public Boolean Cancel(Object cancelValue};

  // Call this method from code inside or outside the iterator
  public void SetCancelTimeout(Int32 milliseconds,
    Object cancelValue);
}
Para aprovechar todas las ventajas de esta característica de cancelación, dentro de su iterador, emita cada operación asincrónica como parte de un grupo de descarte. Esto permite a AsyncEnumerator descartar automáticamente cualquier operación que se complete tras la solicitud de cancelación. Por tanto, para detectar una solicitud de cancelación, incluya código parecido al que se muestra en la figura 3 después de cada instrucción yield return.
IEnumerator<Int32> MyIterator(AsyncEnumerator ae, ...) {
  // obj refers to some object that has BeginXxx/EndXxx methods
  obj.BeginXxx(...,         // Pass any arguments as usual
    ae.End(0, obj.EndXxx),  // For AsyncCallback indicate
                            // discard group 0 & proper End method  
                            //to call for cleanup
    null);                  // BeginXxx's AsyncState argument

  // Make more calls to BeginXxx methods if desired here...

  yield return n; // Resume iterator after 'n' operations
                  // complete or if cancelation occurs 

  // Check for cancellation
  Object cancelValue;
  if (ae.IsCanceled(out cancelValue)) {
    // The iterator should cancel due to user request/timeout
    // Note: It is common to call "yield break" here.
  } else {
    // Call DequeueAsyncResult 'n' times to 
    // process the completed operations
  }
}
Ahora, si desea empezar a ejecutar un iterador que pueda cancelarse, deberá construir un objeto AsyncEnumerator y llamar a BeginExecute en éste como haría normalmente. Y, cuando alguna parte de su aplicación desee cancelar el iterador, llamará al método Cancel. Al llamar al método Cancel, podrá pasar una referencia a un objeto, la cual, a continuación, pasará al iterador por medio del parámetro out del método IsCanceled. Este objeto proporciona al código que cancela el iterador una manera de comunicarle por qué está siendo cancelado. Si el iterador no desea saber por qué es cancelado, puede llamar al método sobrecargado IsCanceled que no acepta parámetros.
El método SetCancelTimeout puede ser llamado a través de código tanto desde dentro como desde fuera del iterador. Cuando se llama a este método, se establece un temporizador que, al agotarse el tiempo, llama al método Cancel automáticamente (el cual pasa el valor especificado por el usuario mediante el argumento Cancel-Value).
El código de la figura 4 muestra una aplicación de Windows Forms que usa muchas de las características de las que he hablado en esta columna. La IU de la aplicación se muestra en la figura 5. Usa la característica SyncContext de AsyncEnumerator para asegurarse de que todo el código del iterador se ejecuta a través del subproceso de la GUI y, de esta forma, permite que los controles de la IU se actualicen. Este código también muestra cómo usar la compatibilidad del APM de Async­Enumerator para no bloquear el subproceso de la GUI y permitir así que la IU mantenga su capacidad de respuesta.
namespace WinFormUsingAsyncEnumerator {
  public partial class WindowsFormsViaAsyncEnumerator : Form {
    public static void Main() {
      Application.Run(new WindowsFormsViaAsyncEnumerator());
    }

    public WindowsFormsViaAsyncEnumerator() {
      InitializeComponent();
    }

    private AsyncEnumerator m_ae = null;

    private void m_btnStart_Click(object sender, EventArgs e) {
      String[] uris = new String[] {
        "http://Wintellect.com/", 
        "http://1.1.1.1/",   // Demonstrates error recovery
        "http://www.Devscovery.com/" 
      };

      m_ae = new AsyncEnumerator();

      // NOTE: The AsyncEnumerator automatically saves the 
      // Windows Forms SynchronizationContext with it ensuring
      // that the iterator always runs on the GUI thread; 
      // this allows the iterator to access the UI Controls

      // Start iterator asynchonously so that GUI thread doesn't block
      m_ae.BeginExecute(GetWebData(m_ae, uris), m_ae.EndExecute);
    }

    private IEnumerator<Int32> GetWebData(AsyncEnumerator ae, String[] uris) {
      ToggleStartAndCancelButtonState(false);
      m_lbResults.Items.Clear();

      if (m_chkAutoCancel.Checked)
        ae.SetCancelTimeout(5000, ae);

      // Issue several Web requests (all in discard group 0) simultaneously
      foreach (String uri in uris) {
        WebRequest webRequest = WebRequest.Create(uri);

        // If the AsyncEnumerator is canceled, DiscardWebRequest cleans up
        // any outstanding operations as they complete in the future
        webRequest.BeginGetResponse(ae.EndVoid(0, DiscardWebRequest), 
                                                          webRequest);
      }

      yield return uris.Length;  // Process the completed Web requests 
                                 // after all complete

      String resultStatus; // Ultimate result of processing shown to user

      // Check if iterator was canceled
      Object cancelValue;
      if (ae.IsCanceled(out cancelValue)) {
        ae.DiscardGroup(0);
        // Note: In this example calling DiscardGroup above is not mandatory
        // because the whole iterator is stopping execution; causing all
        // discard groups to be discarded automatically.

        resultStatus = (cancelValue == ae) ? "Timeout" : "User canceled";
        goto Complete;
      }

      // Iterator wasn't canceled, process all the completed operations
      for (Int32 n = 0; n < uris.Length; n++) {
        IAsyncResult result = ae.DequeueAsyncResult();

        WebRequest webRequest = (WebRequest)result.AsyncState;

        String s = "URI=" + webRequest.RequestUri + ", ";
        try {
          using (WebResponse webResponse = webRequest. 
                 EndGetResponse(result)) {
                    s += "ContentLength=" + webResponse.ContentLength;
          }
        }
        catch (WebException e) {
          s += "Error=" + e.Message;
        }
        m_lbResults.Items.Add(s);  // Add result of operation to listbox
      }
      resultStatus = "All operations completed.";

    Complete:
      // All operations have completed or cancellation occurred, tell      // user
      MessageBox.Show(this, resultStatus);

      // Reset everything so that the user can start over if they desire
      m_ae = null;   // Reset since we're finished
      ToggleStartAndCancelButtonState(true);
    }

    private void m_btnCancel_Click(object sender, EventArgs e) {
      m_ae.Cancel(null);
      m_ae = null;
    }

    // Swap the Start/Cancel button states
    private void ToggleStartAndCancelButtonState(Boolean enableStart) {
      m_btnStart.Enabled = enableStart;
      m_btnCancel.Enabled = !enableStart;
    }

    private void DiscardWebRequest(IAsyncResult result) {
      // Get the WebRequest object used to initate the request 
      // (see BeginGetResponse's last argument)
      WebRequest webRequest = (WebRequest)result.AsyncState;

      // Clean up the async operation and Close the WebResponse (if no       // exception)
      webRequest.EndGetResponse(result).Close();
    }
  }
}
Figura 5 La aplicación de muestra
Desde dentro del iterador, muchas solicitudes web se emiten como parte de un grupo de descarte y, puesto que la IU mantiene su capacidad de respuesta, el usuario puede hacer clic en el botón de cancelación si se cansa de esperar los resultados. Si esto sucede, AsyncEnumerator finalizará automáticamente cualquiera de las operaciones de tal forma que el código del iterador no debe preocuparse de llevar a cabo ninguna operación de limpieza. Observe que el formulario también muestra cómo configurar un temporizador para que AsyncEnumerator se cancele automáticamente pasados cinco segundos si todas las operaciones no se han completado.
Esta muestra realiza solicitudes web mediante la clase WebRequest de .NET. Cuando se llama al método Begin­GetResponse de WebRequest, el proceso de limpieza requiere algo más aparte de llamar a EndGetResponse. También debe llamarse a Close (o Dispose) en el objeto WebResponse que EndGetResponse devuelve.
Por este motivo, al llamar a BeginGetResponse, el código pasa el método DiscardWeb­Request al método EndVoid. El método DiscardWebRequest debe asegurarse de que el objeto WebResponse se cierre, si la solicitud web se realizó correctamente y no generó ninguna excepción.

Muchos desarrolladores saben que la programación asincrónica es la clave para aumentar el rendimiento, la escalabilidad, la capacidad de respuesta y la confiabilidad de sus aplicaciones, servidores y componentes. Lamentablemente, muchos desarrolladores se niegan a aceptar completamente la programación asincrónica, ya que el modelo de programación ha resultado ser mucho más tedioso y difícil que el modelo de programación sincrónica de eficacia probada.
Usando la característica iterador de C# y mi clase Async-Enumerator, los desarrolladores pueden aceptar la programación asincrónica a partir de un modelo de programación sincrónica. Async­Enumerator también se integra fácilmente con otras partes de .NET Framework y ofrece muchas características que permiten a los desarrolladores ampliar sus aplicaciones más allá de lo que permite el modelo de programación sincrónica habitual.
He estado usando AsyncEnumerator durante más de un año y he ayudado a muchas compañías a integrarlo en su software con excelentes resultados. Descargue el código en wintellect.com/PowerThreading.aspx. Espero que tenga tan buenos resultados como yo.

Envíe sus preguntas y comentarios a Jeffrey a mmsync@microsoft.com.

Jeffrey Richter es cofundador de Wintellect (www.Wintellect.com), una empresa de revisión de arquitectura, consultoría y cursos. Es autor de varios libros, entre ellos, CLR via C#. Jeffrey colabora como editor de MSDN Magazine y ha sido consultor de Microsoft desde 1990.

Page view tracker