Está en la página 1de 336

Manual de .

NET

Framework 4.5
Manual de .NET Framework 4.5

Índice del Manual

1. Subprocesamiento administrado
1.1. Principios básicos del subprocesamiento administrado
1.1.1.Subprocesos y subprocesamiento
1.1.2.Excepciones en subprocesos administrados
1.1.3.Sincronizar datos para subprocesamiento múltiple
1.1.4.Estados de subprocesos administrados
1.1.5.Subprocesos de primer y segundo plano
1.1.6.Subprocesamiento administrado y no administrado en Windows
1.1.7.Thread.Suspend, recolección de elementos no utilizados y puntos de seguridad
1.1.8.Almacenamiento local de subprocesos: Campos estáticos relacionados con
subprocesos y ranuras de datos
1.1.9.Cancelación en subprocesos administrados
1.1.9.1. Cómo: Realizar escuchas de solicitudes mediante sondeo
1.1.9.2. Cómo: Registrar devoluciones de llamadas de solicitudes de
cancelación
1.1.9.3. Cómo: Realizar escuchas de solicitudes de cancelación cuando tienen
controladores de espera
1.1.9.4. Cómo: Realizar escuchas de varias solicitudes de cancelación
1.2. Utilizar subprocesos y subprocesamiento
1.2.1.Crear subprocesos y analizar los datos en el inicio
1.2.2.Pausar y reanudar subprocesos
1.2.3.Destruir subprocesos
1.2.4.Planear subprocesos
1.2.5.Cancelar subprocesos de manera cooperativa
1.3. Procedimientos recomendados para el subprocesamiento administrado
1.4. Objetos y características de subprocesos
1.4.1.Grupo de subprocesos administrados
1.4.2.Temporizadores
1.4.3.Monitores
1.4.4.Controladores de espera
1.4.5.Controladores de espera
1.4.5.1. EventWaitHandle
1.4.5.2. AutoResetEvent
1.4.5.3. ManualResetEvent y ManualResetEventSlim
1.4.5.4. CountdownEvent
1.4.6.Exclusiones mutuas (mutex)
1.4.7.Operaciones de bloqueo
1.4.8.Bloqueos de lector y escritor
1.4.9.Semaphore y SemaphoreSlim
1.4.10. Información general sobre los primitivos de sincronización
1.4.11. Barrier
1.4.11.1. Cómo: Sincronizar operaciones simultáneas con una clase Barrier
1.4.12. SpinLock

MCT: Luis Dueñas Pag 2 de 336


Manual de .NET Framework 4.5

1.4.12.1. Cómo: Utilizar SpinLock para la sincronización de bajo nivel


1.4.12.2. Cómo: Habilitar el modo de seguimiento de subproceso en el bloqueo
SpinLock
1.4.13. Tokens de cancelación
1.4.14. SpinWait
1.4.14.1. Cómo: Usar SpinWait para implementar una operación de espera de
dos fases
2. Modelos para la programación asincrónica
2.1. Modelo asincrónico basado en tareas (TAP)
2.1.1.Implementar el modelo asincrónico basado en tareas
2.1.2.Utilizar el modelo asincrónico basado en tareas
2.1.3.Interoperabilidad con los modelos asincrónicos y otros tipos
2.2. Publicar datos de símbolos
2.2.1.Programación multiproceso con el modelo asincrónico basado en eventos
2.2.1.1. Información general sobre el modelo asincrónico basado en eventos
2.2.1.2. Implementar el modelo asincrónico basado en eventos
2.2.1.3. Procedimientos recomendados para implementar el modelo
asincrónico basado en eventos
2.2.1.4. Decidir cuándo implementar el modelo asincrónico basado en eventos
2.2.1.5. Tutorial: Implementar un componente que admita el modelo
asincrónico basado en eventos
2.2.1.5.1. Cómo: Implementar un componente que admita el modelo
asincrónico basado en eventos
2.2.1.5.2. Cómo: Implementar un cliente en un modelo asincrónico
basado en eventos
2.2.1.6. Cómo: Utilizar componentes que admitan el modelo asincrónico
basado en eventos
2.3. Modelo de programación asincrónica (APM)
2.3.1.Llamar a métodos asincrónicos mediante IAsyncResult
2.3.1.1. Bloquear la ejecución de una aplicación mediante AsyncWaitHandle
2.3.1.2. Bloquear la ejecución de una aplicación al finalizar una operación
asincrónica
2.3.1.3. Sondear el estado de una operación asincrónica
2.3.1.4. Utilizar un delegado AsyncCallback para finalizar una operación
asincrónica
2.3.1.4.1. Utilizar un delegado AsyncCallback y un objeto State
2.3.2.Programación asincrónica mediante delegados
2.3.2.1. Llamar a métodos sincrónicos de forma asincrónica
2.3.2.2. Ejemplo de programación de delegados asincrónicos
3. Programación paralela en .NET Framework
3.1. Biblioteca de procesamiento paralelo basado en tareas (TPL)
3.1.1.Paralelismo de datos (Task Parallel Library)
3.1.1.1. Cómo: Escribir un bucle Parallel.For simple
3.1.1.2. Cómo: Escribir un bucle Parallel.ForEach simple
3.1.1.3. Cómo: Detener o interrumpir un bucle Parallel.For

MCT: Luis Dueñas Pag 3 de 336


Manual de .NET Framework 4.5

3.1.1.4. Cómo: Escribir un bucle Parallel.For que tenga variables locales de


subproceso
3.1.1.5. Cómo: Escribir un bucle Parallel.ForEach que tenga variables locales de
subproceso
3.1.1.6. Cómo: Cancelar un bucle Parallel.For o ForEach
3.1.1.7. Cómo: Controlar excepciones en bucles paralelos
3.1.1.8. Cómo: Acelerar cuerpos de bucle pequeños
3.1.1.9. Cómo: Recorrer en iteración directorios con la clase paralela
3.1.2.Paralelismo de tareas (Task Parallel Library)
3.1.2.1. Tareas de continuación
3.1.2.2. Tareas anidadas y tareas secundarias
3.1.2.3. Cancelación de tareas
3.1.2.4. Control de excepciones
3.1.2.5. Cómo: Usar Parallel.Invoke para ejecutar operaciones paralelas
3.1.2.6. Cómo: Devolver un valor de una tarea
3.1.2.7. Cómo: Esperar a que una o varias tareas se completen
3.1.2.8. Cómo: Cancelar una tarea y sus elementos secundarios
3.1.2.9. Cómo: Controlar excepciones iniciadas por tareas
3.1.2.10. Cómo: Encadenar varias tareas con continuaciones
3.1.2.11. Cómo: Crear tareas precalculadas
3.1.2.12. Cómo: Recorrer un árbol binario con tareas paralelas
3.1.2.13. Cómo: Desencapsular una tarea anidada
3.1.2.14. Cómo: Evitar que una tarea secundaria se adjunte a su elemento
primario
3.1.3.Biblioteca de procesamiento paralelo basado en tareas (TPL)
3.1.3.1. Cómo: Escribir y leer mensajes en un bloque de flujo de datos
3.1.3.2. Cómo: Implementar un modelo de flujo de datos productor-
consumidor
3.1.3.3. Cómo: Realizar una acción cuando un bloque de flujos de datos recibe
datos
3.1.3.4. Tutorial: Crear una canalización de flujos de datos
3.1.3.5. Cómo: Desvincular bloques de flujos de datos
3.1.3.6. Tutorial: Usar flujos de datos en aplicaciones de Windows Forms
3.1.3.7. Cómo: Cancelar un bloque de flujos de datos
3.1.3.8. Tutorial: Crear tipos de bloques de flujos de datos personalizados
3.1.3.9. Cómo: Usar JoinBlock para leer datos de diferentes orígenes
3.1.3.10. Cómo: Especificar el grado de paralelismo en un bloque de flujos de
datos
3.1.3.11. Cómo: Especificar un programador de tareas en un bloque de flujos de
datos
3.1.3.12. Tutorial: Usar BatchBlock y BatchedJoinBlock para mejorar la eficacia
3.1.4.TPL con otros modelos asincrónicos
3.1.4.1. TPL y la programación asincrónica tradicional de .NET
3.1.4.2. Cómo: Encapsular modelos de EAP en una tarea
3.1.4.3. Problemas potenciales en el paralelismo de datos y tareas

MCT: Luis Dueñas Pag 4 de 336


Manual de .NET Framework 4.5

3.2. Parallel LINQ (PLINQ)


3.2.1.Introducción a PLINQ
3.2.2.Introducción a la velocidad en PLINQ
3.2.3.Conservar el orden en PLINQ
3.2.4.Opciones de combinación en PLINQ
3.2.5.Posibles problemas con PLINQ
3.2.6.Cómo: Crear y ejecutar una consulta PLINQ simple
3.2.7.Cómo: Controlar la ordenación en una consulta PLINQ
3.2.8.Cómo: Combinar consultas LINQ paralelas y secuenciales
3.2.9.Cómo: Controlar excepciones en una consulta PLINQ
3.2.10. Cómo: Cancelar una consulta PLINQ
3.2.11. Cómo: Escribir una función de agregado personalizada de PLINQ
3.2.12. Cómo: Especificar el modo de ejecución en PLINQ
3.2.13. Cómo: Especificar opciones de combinación en PLINQ
3.2.14. Cómo: Recorrer en iteración directorios con PLINQ
3.2.15. Cómo: Medir el rendimiento de consultas PLINQ
3.2.16. Ejemplo de datos de PLINQ
3.3. Estructuras de datos para la programación paralela
3.4. Herramientas de diagnóstico paralelo
3.5. Particionadores personalizados para PLINQ y TPL
3.5.1.Cómo: Implementar las particiones dinámicas
3.5.2.Cómo: Implementar un particionador con un número estático de particiones
3.6. Generadores de tareas
3.7. Programadores de tareas
3.7.1.Cómo: Crear un programador de tareas que limita el grado de simultaneidad
3.7.2.Cómo: Programar trabajo en el subproceso de la interfaz de usuario
3.8. Expresiones lambda en PLINQ y TPL

MCT: Luis Dueñas Pag 5 de 336


Manual de .NET Framework 4.5

Procesamiento Paralelo y Simultaneidad en .NET Framework


.NET Framework proporciona distintas formas de usar varios subprocesos de ejecución de modo
que su aplicación responda al usuario de la forma adecuada maximizando al mismo tiempo el
rendimiento del equipo del usuario.
1. Subprocesamiento administrado
Independientemente de si está programando para equipos con un procesador o con varios,
deseará que la aplicación proporcione la interacción más rápida posible con el usuario, incluso si
ésta está realizando actualmente otro trabajo. El uso de varios subprocesos de ejecución es una
de las formas más eficaces para mantener la respuesta de la aplicación al usuario y, al mismo
tiempo, permite utilizar el procesador entre o incluso durante los eventos del usuario. Aunque
esta sección presenta los conceptos básicos del subprocesamiento, se centra en conceptos
relacionados con el subprocesamiento administrado y su utilización.
Nota
A partir de .NET Framework 4, la programación multiproceso se ha simplificado
considerablemente con las clases System.Threading.Tasks.Task y System.Threading.Tasks.
Parallel, Parallel LINQ (PLINQ), nuevas clases de colección simultáneas en el espacio de
nombres System.Collections.Concurrent y un nuevo modelo de programación que está basado
en el concepto de tareas en lugar de subprocesos.

1.1. Principios básicos del subprocesamiento administrado


En los cinco primeros temas de esta sección encontrará información que le ayudará a determinar
cuándo se debe utilizar un subprocesamiento administrado, así como explicaciones de algunas
de las características básicas.
En el resto de la sección se tratan temas más avanzados, incluida la interacción del
subprocesamiento administrado con el sistema operativo Windows.
Nota
En .NET Framework 4, Task Parallel Library y PLINQ proporcionan API para el paralelismo de
tareas y datos en programas multiproceso.

1.1.1. Subprocesos y subprocesamiento


Los sistemas operativos utilizan procesos para independizar las diferentes aplicaciones que
ejecutan. Los subprocesos son la unidad básica a la que el sistema operativo asigna tiempo de
procesador. Puede que haya más de un subproceso ejecutando código dentro del proceso. Cada
subproceso mantiene controladores de excepciones, una prioridad de programación y un
conjunto de estructuras que el sistema utiliza para guardar el contexto del subproceso hasta que
se programe. El contexto del subproceso incluye, en el espacio de direcciones del proceso host
del subproceso, toda la información que necesita éste para reanudar sin problemas la ejecución,
como el conjunto de registros de la CPU y la pila.
.NET Framework subdivide un proceso de sistema operativo en pequeños subprocesos
administrados, denominados dominios de aplicación y representados por System.AppDomain.
Dentro del mismo proceso administrado se pueden ejecutar uno o varios subprocesos
administrados (representados por System.Threading.Thread) en uno o varios dominios de
aplicación. Aunque cada dominio de aplicación se inicia con un único subproceso, su código
puede crear otros dominios de aplicación y subprocesos adicionales. El resultado es que un
subproceso administrado puede moverse libremente entre dominios de aplicación dentro del
mismo proceso administrado; podría tener sólo un subproceso que se moviera entre varios
dominios de aplicación.
Un sistema operativo que admita multitareas prioritarias crea el efecto de ejecución simultánea
de varios subprocesos desde varios procesos. Para ello, se divide el tiempo de procesador

MCT: Luis Dueñas Pag 6 de 336


Manual de .NET Framework 4.5

disponible entre los subprocesos que lo necesitan y se asigna un espacio de tiempo de


procesador a cada subproceso, uno tras otro. Cuando trascurre su espacio de tiempo, el
subproceso que se esté ejecutando actualmente se suspende y otro subproceso reanuda su
ejecución. Cuando el sistema cambia de un subproceso a otro, guarda el contexto del subproceso
prioritario y vuelve a cargar el contexto de subproceso guardado del siguiente subproceso de la
cola.
El espacio de tiempo asignado depende del sistema operativo y del procesador. Puesto que cada
espacio de tiempo es pequeño, parece que se ejecutan varios subprocesos a la vez, incluso si hay
un único procesador. De hecho, éste es el caso en sistemas multiprocesador, donde los
subprocesos ejecutables se distribuyen entre los procesadores disponibles.
Cuándo utilizar varios subprocesos
El software que requiere la interacción del usuario debe reaccionar a sus actividades lo más
rápido posible para que la sensación del usuario ante el sistema sea satisfactoria. No obstante, al
mismo tiempo, debe realizar los cálculos necesarios para presentarle los datos tan rápido como
sea posible. Si una aplicación utiliza sólo un subproceso de ejecución, puede combinar la
programación asincrónica con comunicación remota de .NET Framework o con servicios Web
XML creados mediante ASP.NET para utilizar el tiempo de procesamiento de otros equipos,
además del de su propio equipo, y aumentar la capacidad de respuesta al usuario a la vez que se
reduce el tiempo de procesamiento de los datos de la aplicación. Si está realizando un trabajo
intenso de entrada y salida, también puede utilizar los puertos de finalización de E/S para
aumentar la capacidad de respuesta de la aplicación.
Ventajas del uso de varios subprocesos
No obstante, el uso de más de un subproceso es la técnica más eficaz disponible para elevar la
capacidad de respuesta al usuario y procesar los datos requeridos al realizar el trabajo
prácticamente al mismo tiempo. En un equipo con un procesador, varios subprocesos pueden
crear este efecto si aprovechan los breves períodos entre los eventos de usuario para procesar los
datos en segundo plano. Por ejemplo, un usuario puede modificar una hoja de cálculo mientras
otro subproceso vuelve a calcular otras partes de la hoja de cálculo dentro de la misma
aplicación.
La misma aplicación, sin modificación, aumentaría considerablemente el grado de satisfacción
del usuario si se ejecutara en un equipo con más de un procesador. Su único dominio de
aplicación podría utilizar varios subprocesos para realizar las siguientes tareas:

 Comunicarse a través de una red, con un servidor Web y una base de datos.
 Realizar operaciones que requieren una gran cantidad de tiempo.
 Distinguir tareas de diversas prioridades. Por ejemplo, un subproceso de prioridad alta
administra tareas en las que el tiempo es decisivo y un proceso de prioridad baja realiza
otras tareas.
 Permitir a la interfaz de usuario que siga respondiendo, mientras se asigna tiempo a las
tareas en segundo plano.

Desventajas del uso de varios subprocesos


Se recomienda utilizar el menor número de subprocesos posible, puesto que de este modo se
reduce el uso de recursos del sistema operativo y se mejora el rendimiento. Al diseñar la
aplicación, el subprocesamiento debe tener en cuenta los requisitos de recursos y conflictos
potenciales. Los requisitos de recursos son los siguientes:

 El sistema utiliza memoria para la información de contexto requerida por los procesos,
objetos AppDomain y subprocesos. Por tanto, el número de procesos, objetos
AppDomain y subprocesos que se pueden crear está limitado por la memoria
disponible.

MCT: Luis Dueñas Pag 7 de 336


Manual de .NET Framework 4.5

 Si se realiza un seguimiento de un gran número de subprocesos, se utiliza una gran


cantidad de tiempo de procesador. Si hay demasiados subprocesos, la mayoría no
avanzarán de forma significativa. Si la mayor parte de los subprocesos actuales
pertenecen a un proceso, los subprocesos de otros procesos se programan con menor
frecuencia.
 El control de la ejecución del código con muchos subprocesos es complejo y puede
constituir una fuente de muchos errores.
 Para destruir los subprocesos es necesario conocer lo que podría suceder y controlar
dichos problemas.

Si se proporciona acceso compartido a los recursos, se pueden producir conflictos. Para evitar
los conflictos, es necesario sincronizar o controlar el acceso a los recursos compartidos. Si no se
sincroniza el acceso correctamente (en los mismos dominios de aplicación o en otros) se pueden
producir problemas como interbloqueos (en los que dos subprocesos dejan de responder
mientras cada uno espera a que el otro se complete) y condiciones de carrera (cuando se produce
un resultado anómalo debido a una dependencia decisiva e inesperada de la duración de dos
eventos). El sistema proporciona objetos de sincronización que pueden utilizarse para coordinar
recursos compartidos entre varios subprocesos. Si se reduce el número de subprocesos, se
facilita la sincronización de los recursos.
Entre los recursos que requieren sincronización se incluyen los siguientes:
 Recursos del sistema (como puertos de comunicaciones).
 Recursos compartidos por varios procesadores (como identificadores de archivo).
 Los recursos de un único dominio de aplicación (como campos globales, estáticos y de
instancia) a los que tienen acceso varios subprocesos.
Subprocesamiento y diseño de aplicaciones
En general, el uso de la clase ThreadPool es la forma más fácil de controlar varios subprocesos
para tareas relativamente cortas que no bloquearán otros subprocesos y cuando no se espera
ninguna programación particular de las tareas. No obstante, hay diversos motivos para crear sus
propios subprocesos:
 Si es necesario que una tarea tenga una prioridad en particular.
 Si tiene una tarea que podría ejecutarse durante bastante tiempo (y, por tanto, bloquear
otras tareas).
 Si debe ubicar subprocesos en un contenedor uniproceso (todos los subprocesos
ThreadPool se encuentran en un contenedor multiproceso).
 Si necesita asociar una identidad estable al subproceso. Por ejemplo, debería utilizar un
subproceso dedicado para anular dicho subproceso, suspenderlo o detectarlo por
nombre.
 Si es necesario ejecutar subprocesos de fondo que interactúan con la interfaz de usuario,
la versión 2.0 de .NET Framework proporciona un componente BackgroundWorker que
se comunica utilizando eventos, con cálculo de referencias entre subprocesos al
subproceso de la interfaz de usuario.
Subprocesamiento y excepciones
Controle las excepciones en subprocesos. Las excepciones no controladas en subprocesos,
incluso los subprocesos de fondo, generalmente finalizan el proceso. Hay tres excepciones a esta
regla:

 Se produce una excepción ThreadAbortException en un subproceso porque se llamó a


Abort.
 Se produce una excepción AppDomainUnloadedException en un subproceso, porque se
descarga el dominio de aplicación.
 Common Language Runtime o un proceso de host finaliza el subproceso.

MCT: Luis Dueñas Pag 8 de 336


Manual de .NET Framework 4.5

Nota
En las versiones 1.0 y 1.1 de .NET Framework, Common Language Runtime intercepta
silenciosamente algunas excepciones, como por ejemplo en subprocesos ThreadPool. Esto
puede dañar el estado de la aplicación y hacer que en el futuro las aplicaciones no respondan, lo
que podría ser muy difícil de depurar.

1.1.2. Excepciones en subprocesos administrados


A partir de .NET Framework versión 2.0, Common Language Runtime permite que la mayoría
de las excepciones no controladas en subprocesos avancen naturalmente. En la mayor parte de
los casos, esto significa que la excepción no controlada hace que finalice la aplicación.
Nota
Este hecho supone un cambio significativo respecto a las versiones 1.0 y 1.1 de .NET
Framework, que proporcionaban una parada para muchas excepciones no controladas, por
ejemplo, las excepciones no controladas en subprocesos ThreadPool.
Common Language Runtime proporciona una parada para ciertas excepciones no controladas
que se utilizan para controlar el flujo del programa:
 Se produce una excepción ThreadAbortException en un subproceso porque se llamó a
Abort.
 Se inicia una excepción AppDomainUnloadedException en un subproceso porque se
descarga el dominio de aplicación en el que se está ejecutando el subproceso.
 Common Language Runtime o un proceso del host finaliza el subproceso iniciando una
excepción interna.
Si alguna de estas excepciones no se controla en los subprocesos creados por Common
Language Runtime, la excepción finaliza el subproceso, pero Common Language Runtime no
permite que la excepción continúe posteriormente.
Si no se controlan estas excepciones en el subproceso principal o en subprocesos que entraron
en el tiempo de ejecución desde código no administrado, continúan normalmente, lo que hace
que finalice la aplicación.
Nota
Ahora el runtime puede iniciar una excepción no controlada antes de que cualquier código
administrado haya tenido oportunidad de instalar un controlador de excepciones. Aunque el
código administrado no tuviera ninguna oportunidad para controlar este tipo de excepción, la
excepción puede continuar naturalmente.
Exponer problemas de subprocesamiento durante el desarrollo
Cuando se permite que los subprocesos tengan errores no indicados, sin finalizar la aplicación,
los problemas de programación serios pueden quedar sin detectar. Éste es un problema concreto
para los servicios y otras aplicaciones que se ejecutan durante períodos prolongados. Cuando se
producen errores en los subprocesos, el estado del programa se va dañando gradualmente. El
rendimiento de la aplicación se puede degradar o la aplicación podría no responder.
Permitir que las excepciones no controladas en subprocesos continúen naturalmente, hasta que
el sistema operativo finalice el programa, expone tales problemas durante las fases de desarrollo
y pruebas. Los informes de errores creados al cerrarse los programas ayudan en las tareas de
depuración.
Cambiar desde versiones anteriores
El cambio más significativo se refiere a los subprocesos administrados. En las versiones 1.0 y
1.1 de .NET Framework, Common Language Runtime proporciona una parada para las
excepciones no controladas en las situaciones siguientes:

MCT: Luis Dueñas Pag 9 de 336


Manual de .NET Framework 4.5

 No hay una excepción no controlada en un subproceso ThreadPool. Cuando una tarea


inicia una excepción que no controla, el runtime imprime el seguimiento de pila de
excepción en la consola y luego devuelve el subproceso al grupo de subprocesos.
 No hay ninguna excepción no controlada en un subproceso creado con el método Start
de la clase Thread. Cuando el código que se ejecuta en un subproceso así inicia una
excepción que no controla, el runtime imprime el seguimiento de pila de la excepción
en la consola y luego finaliza el subproceso sin problemas.
 No hay ninguna excepción no controlada en el subproceso finalizador. Cuando un
subproceso finalizador inicia una excepción que no controla, el runtime imprime el
seguimiento de pila de excepción en la consola y luego devuelve el subproceso
finalizador para reanudar los subprocesos finalizadores en ejecución.
El estado que indica si un subproceso administrado es en primer plano o en segundo plano no
afecta a este comportamiento.
Para las excepciones no controladas en subprocesos que se originan en código no administrado,
la diferencia es más sutil. El cuadro de diálogo de asociación JIT en tiempo de ejecución se
apropia del cuadro de diálogo de sistema operativo para excepciones administradas o
excepciones nativas en subprocesos que han pasado por el código nativo. El proceso finaliza en
todos los casos.
Migrar código
En general, el cambio expondrá los problemas de programación previamente no reconocidos
para que se puedan corregir. En algunos casos, sin embargo, los programadores podrían haber
aprovechado la parada en tiempo de ejecución para, por ejemplo, finalizar subprocesos.
Dependiendo de la situación, deberían plantearse una de las estrategias de migración siguientes:
 Reestructurar el código para que el subproceso finalice sin problemas cuando se recibe
una señal.
 Utilizar el método Thread.Abort para anular el subproceso.
 Si se debe detener un subproceso para que pueda continuar la finalización de procesos,
convierta el subproceso en un subproceso en segundo plano para que finalice
automáticamente al salir del proceso.
Marcador de compatibilidad de aplicaciones
Como una medida de compatibilidad temporal, los administradores pueden colocar un marcador
de compatibilidad en la sección <runtime> del archivo de configuración de la aplicación. Esto
hace que Common Language Runtime regrese al comportamiento de las versiones 1.0 y 1.1.
<legacyUnhandledExceptionPolicy enabled="1"/>

Reemplazo de host
En .NET Framework versión 2.0, un host no administrado puede utilizar la interfaz
ICLRPolicyManager de la API de hospedaje para invalidar la directiva predeterminada de
excepciones no controladas de Common Language Runtime. La función
ICLRPolicyManager::SetUnhandledExceptionPolicy se utiliza para establecer la directiva para
las excepciones no controladas.
1.1.3. Sincronizar datos para subprocesamiento múltiple
Cuando varios subprocesos pueden llamar a las propiedades y métodos de un solo objeto, es
fundamental sincronizar las llamadas. En caso contrario, un subproceso puede interrumpir la
ejecución de otro subproceso, y el objeto puede terminar teniendo un estado no válido. Se dice
que una clase es segura para subprocesos cuando sus miembros están protegidos contra este tipo
de interrupciones.
La infraestructura de Common Language proporciona diversas estrategias para sincronizar el
acceso a instancias y miembros estáticos:

MCT: Luis Dueñas Pag 10 de 336


Manual de .NET Framework 4.5

 Regiones de código sincronizado. Puede utilizar la clase Monitor o la compatibilidad


del compilador de esta clase para sincronizar sólo el bloque de código necesario y así
mejorar el rendimiento.
 Sincronización manual. Puede utilizar los objetos de sincronización proporcionados por
la biblioteca de clases de .NET Framework.
 Contextos sincronizados. Puede utilizar SynchronizationAttribute para habilitar la
sincronización automática y simple de objetos ContextBoundObject.
 Clases de colección del espacio de nombres System.Collections.Concurrent. Estas
clases proporcionan operaciones integradas sincronizadas de agregar y quitar.
Common Language Runtime proporciona un modelo de subproceso en el que las clases
pertenecen a un número de categorías que pueden sincronizarse de varias formas diferentes en
función de los requisitos. En la tabla siguiente se muestra la compatibilidad de sincronización
proporcionada para campos y métodos con una categoría de sincronización dada.
Bloques de
Campos Campos Métodos Campos de Métodos de
Categoría código
globales estáticos estáticos instancia instancia
específico
Sin sincronización No No No No No No
Contexto
No No No Sí Sí No
sincronizado
Regiones de código Sólo si se Sólo si se
No No No Sólo si se marca
sincronizado marca marca
Sincronización
Manual Manual Manual Manual Manual Manual
manual

Sin sincronización
Ésta es la opción predeterminada para objetos. Cualquier subproceso puede tener acceso a un
método o campo en cualquier momento. Sólo debería tener acceso a estos objetos un subproceso
cada vez.
Sincronización manual
La biblioteca de clases de .NET Framework proporciona una serie de clases para sincronizar los
subprocesos.
Regiones de código sincronizado
Puede utilizar la clase Monitor o una palabra clave del compilador para sincronizar bloques de
código, métodos de instancia y métodos estáticos. No se admiten los campos estáticos
sincronizados.
Visual Basic y C# admiten el marcado de bloques de código con una determinada palabra clave
de lenguaje, la instrucción lock en C# o la instrucción SyncLock en Visual Basic. Cuando un
subproceso ejecuta el código, se realiza un intento de bloqueo. Si otro subproceso ha realizado
ya el bloqueo, el subproceso se bloquea hasta que esté disponible el bloqueo. Cuando el
subproceso sale del bloque sincronizado de código, el bloqueo se libera, independientemente de
cómo salga del bloque el subproceso.
Nota
Las instrucciones lock y SyncLock se implementan mediante Monitor.Enter y Monitor.Exit, por
lo que se pueden utilizar otros métodos de Monitor con ellas en el área sincronizada.
También puede decorar un método con MethodImplAttribute y
MethodImplOptions.Synchronized, lo que tiene el mismo efecto que utilizar Monitor o una
de las palabras clave del compilador para bloquear todo el cuerpo del método.

MCT: Luis Dueñas Pag 11 de 336


Manual de .NET Framework 4.5

Se puede utilizar Thread.Interrupt para que un subproceso salga de operaciones de bloqueo, por
ejemplo, de la espera para tener acceso a una región de código sincronizado. También se utiliza
Thread.Interrupt para que los subprocesos salgan de operaciones como Thread.Sleep.
Importante
No bloquee el tipo, es decir, typeof(MyType) en C#, GetType(MyType) en Visual Basic o
MyType::typeid en C++, para proteger los métodos static (métodos Shared en Visual Basic).
Utilice en su lugar un objeto estático privado. De igual forma, no utilice this en C# (Me en
Visual Basic) para bloquear los métodos de instancia. Utilice en su lugar un objeto privado. Una
clase o instancia se puede bloquear por código distinto del suyo propio, produciendo potenciales
interbloqueos o problemas de rendimiento.
Compatibilidad del compilador
Visual Basic y C# admiten una palabra clave que utiliza Monitor.Enter y Monitor.Exit para
bloquear el objeto. Visual Basic admite la instrucción SyncLock y C# admite la instrucción
lock.
En ambos casos, si se inicia una excepción en el bloque de código, el bloqueo adquirido por
lock o SyncLock se libera automáticamente. Los compiladores de C# y de Visual Basic emiten
un bloque try/finally con Monitor.Enter al principio de try y Monitor.Exit en el bloque
finally. Si se inicia una excepción dentro del bloque lock o SyncLock, se ejecuta el controlador
finally para permitir realizar cualquier trabajo de limpieza.
Contexto sincronizado
Puede utilizar el atributo SynchronizationAttribute en cualquier objeto ContextBoundObject
para sincronizar todos los métodos y campos de instancia. Todos los objetos del mismo dominio
de contexto comparten el mismo bloqueo. Varios subprocesos pueden tener acceso a los
métodos y campos, pero sólo uno al mismo tiempo.
1.1.4. Estados de subprocesos administrados
La propiedad Thread.ThreadState proporciona una máscara de bits que indica el estado actual
del subproceso. Un subproceso está siempre en al menos uno de los estados posibles en la
enumeración ThreadState y puede estar en varios estados al mismo tiempo.
Importante
El estado de los subprocesos sólo es de interés en algunos escenarios de depuración. El código
nunca debe utilizar el estado de los subprocesos para sincronizar las actividades de los
subprocesos.
Al crear un subproceso administrado, este se encuentra en el estado Unstarted. El subproceso
permanece en el estado Unstarted hasta que el sistema operativo lo cambia al estado iniciado.
Llamar a Start, permite al sistema operativo saber que se puede iniciar el subproceso, pero no
cambia su estado.

Los subprocesos no administrados que entran en el entorno administrado ya están en el estado


iniciado. Una vez que un subproceso está en un estado de inicio, varias acciones pueden hacer
que cambie su estado. En la tabla siguiente se muestran las acciones que pueden provocar un
cambio de estado, junto con el nuevo estado correspondiente.

Acción Nuevo estado resultante


Se llama al constructor para la clase Thread. Unstarted
Otro subproceso llama a Thread.Start. Unstarted
El subproceso responde a Thread.Start y empieza a
Running
ejecutarse.

MCT: Luis Dueñas Pag 12 de 336


Manual de .NET Framework 4.5

El subproceso llama a Thread.Sleep. WaitSleepJoin


El subproceso llama a Monitor.Wait en otro objeto. WaitSleepJoin
El subproceso llama a Thread.Join en otro subproceso. WaitSleepJoin
Otro subproceso llama a Thread.Suspend. SuspendRequested
El subproceso responde a una solicitud Thread.Suspend. Suspended
Otro subproceso llama a Thread.Resume. Running
Otro subproceso llama a Thread.Abort. AbortRequested
Aborted , a continuación
El subproceso responde a Thread.Abort.
Stopped.
Puesto que el estado Running tiene el valor 0, no es posible realizar una prueba de bits para
detectarlo. En cambio, se puede utilizar la siguiente prueba (en seudocódigo):
if ((state & (Unstarted | Stopped)) == 0) // implies Running

Muchas veces, los subprocesos están en más de un estado en un momento dado. Por ejemplo, si
se bloquea un subproceso en una llamada a Monitor.Wait y otro subproceso llama a Abort en
ese mismo subproceso, este estará en los estados WaitSleepJoin y AbortRequested al mismo
tiempo. En este caso, tan pronto como el subproceso vuelva de la llamada a Wait o se
interrumpa, recibirá la excepción ThreadAbortException.
Una vez que un subproceso deja el estado Unstarted como resultado de una llamada a Start, no
puede volver nunca al estado Unstarted. Un subproceso no puede dejar nunca el estado Stopped.
1.1.5. Subprocesos de primer y segundo plano
Un subproceso administrado es un subproceso en segundo plano o un subproceso de primer
plano. Los subprocesos en segundo plano son idénticos a los subprocesos en primer plano con
una excepción: un subproceso en segundo plano no mantiene activo el entorno de ejecución
administrado. Una vez que todos los subprocesos de primer plano se han detenido en un proceso
administrado (donde el archivo .exe es un ensamblado administrado), el sistema detiene todos
los subprocesos en segundo plano y se cierra.
Nota
Cuando el tiempo de ejecución detiene un subproceso en segundo plano porque el proceso está
cerrándose, no se produce ninguna excepción en el subproceso. Sin embargo, cuando los
subprocesos se detienen porque el método AppDomain.Unload descarga el dominio de
aplicación, se produce una excepción ThreadAbortException tanto en los subprocesos en primer
plano como en los subprocesos segundo plano.
Utilice la propiedad Thread.IsBackground para determinar si un subproceso es un subproceso en
segundo plano o en primer plano, o para cambiar su estado. Un subproceso puede cambiarse a
segundo plano en cualquier momento estableciendo su propiedad IsBackground en true.
Importante
El estado de primer plano o segundo plano de un subproceso no afecta al resultado de una
excepción no controlada en el subproceso. En la versión 2.0 de .NET Framework, una
excepción no controlada tanto en subprocesos en primer plano como en subprocesos en segundo
plano da como resultado la finalización de la aplicación.
Los subprocesos que pertenecen al grupo de subprocesos administrados (es decir, los
subprocesos cuya propiedad IsThreadPoolThread es true) son subprocesos en segundo plano.
Todos los subprocesos que entran en el entorno de ejecución administrado desde código no
administrado se marcan como subprocesos en segundo plano. Todos los subprocesos generados
al crear e iniciar un nuevo objeto Thread son subprocesos en primer plano de forma
predeterminada.

MCT: Luis Dueñas Pag 13 de 336


Manual de .NET Framework 4.5

Si utiliza un subproceso para controlar una actividad, como una conexión de socket, establezca
su propiedad IsBackground en true para que el subproceso no impida la finalización del proceso.
1.1.6. Subprocesamiento administrado y no administrado en Windows
La administración de todos los subprocesos se realiza mediante la clase Thread, incluso la de los
creados por Common Language Runtime o fuera del motor en tiempo de ejecución que entran
en el entorno administrado para ejecutar el código. El motor en tiempo de ejecución supervisa
todos los subprocesos de su proceso que han ejecutado código alguna vez dentro del entorno de
ejecución administrado. No realiza el seguimiento de otros subprocesos. Los subprocesos
pueden entrar en el entorno de ejecución administrado a través de la interoperabilidad COM
(puesto que el motor en tiempo de ejecución expone los objetos administrados como objetos
COM al universo no administrado), la función COM DllGetClassObject() y por invocación de
plataforma.
Cuando un subproceso no administrado entra en el motor en tiempo de ejecución, por ejemplo, a
través de un contenedor CCW, el sistema comprueba el almacenamiento local de los
subprocesos de dicho subproceso para buscar un objeto administrado interno Thread. Si
encuentra alguno, el motor en tiempo de ejecución ya está al corriente de este subproceso. No
obstante, si no puede encontrar ninguno, compila un nuevo objeto Thread y lo instala en el
almacenamiento local de subprocesos de dicho subproceso.
En el subprocesamiento administrado, Thread.GetHashCode es la identificación estable del
subproceso administrado. Mientras dure el subproceso, no colisionará con el valor de ningún
otro subproceso, independientemente del dominio de aplicación del que se obtenga este valor.
Nota
Un ThreadId del sistema operativo no tiene relación fija con un subproceso administrado,
puesto que un host no administrado puede controlar la relación entre los subprocesos
administrados y los no administrados. En concreto, un host sofisticado puede utilizar la API de
fibra para programar muchos subprocesos administrados con el mismo subproceso del sistema
operativo o para pasar un subproceso administrado entre diferentes subprocesos del sistema
operativo.
Correspondencia entre el subprocesamiento de Win32 y el subprocesamiento
administrado
En la tabla siguiente se asignan elementos del subprocesamiento de Win32 a su equivalente
aproximado del motor en tiempo de ejecución. Tenga en cuenta que esta correspondencia no
representa una funcionalidad idéntica. Por ejemplo, TerminateThread no ejecuta cláusulas
finally ni libera recursos, y no se puede evitar. No obstante, Thread.Abort ejecuta todo el código
para revertir, recupera todos los recursos y puede denegarse mediante ResetAbort. Lea
atentamente la documentación antes de realizar suposiciones acerca de la funcionalidad.
En Win32 En Common Language Runtime
CreateThread Combinación de Thread y ThreadStart
TerminateThread Thread.Abort
SuspendThread Thread.Suspend
ResumeThread Thread.Resume
Sleep Thread.Sleep
WaitForSingleObject en el controlador del subproceso Thread.Join
ExitThread Ningún equivalente
GetCurrentThread Thread.CurrentThread
SetThreadPriority Thread.Priority
Ningún equivalente Thread.Name

MCT: Luis Dueñas Pag 14 de 336


Manual de .NET Framework 4.5

Ningún equivalente Thread.IsBackground


Próximo a CoInitializeEx (OLE32.DLL) Thread.ApartmentState
Subprocesos administrados y apartamentos COM
Un subproceso administrado puede marcarse para indicar que no hospedará ningún apartamento
de un único subproceso o un apartamento multiproceso. Los métodos GetApartmentState,
SetApartmentState y TrySetApartmentState de la clase Thread devuelven y asignan el estado de
apartamento de un subproceso. Si no se ha establecido el estado, GetApartmentState devuelve
ApartmentState.Unknown.
Nota
En .NET Framework 1.0 y 1.1, para obtener y establecer el estado de tipo apartamento se utiliza
la propiedad ApartmentState.
La propiedad se puede establecer cuando el subproceso está en el estado ThreadState.Unstarted;
no obstante, sólo se puede establecer una vez por subproceso.
Si no se establece el estado de apartamento antes de iniciar el subproceso, este último se
inicializa como apartamento multiproceso (MTA). El subproceso finalizador y todos los
subprocesos controlados por ThreadPool son MTA.
Importante
Para el código de inicio de la aplicación, la única manera de controlar el estado de tipo
apartamento es aplicar MTAThreadAttribute o STAThreadAttribute al procedimiento de punto
de entrada. En .NET Framework 1.0 y 1.1, la propiedad ApartmentState se puede establecer
como la primera línea de código. Esto no se permite en la versión 2.0 de .NET Framework.
Los objetos administrados que se exponen a COM se comportan como si hubieran agregado el
contador de referencias de subprocesamiento libre. En otras palabras, se les puede llamar desde
cualquier apartamento COM con subprocesamiento libre. Los únicos objetos administrados que
no presentan este comportamiento de subproceso libre son los que se derivan de
ServicedComponent o StandardOleMarshal Object.
En el universo administrado, no hay compatibilidad con el atributo SynchronizationAttribute, a
menos que se utilicen contextos e instancias administradas enlazadas a un contexto. Si utiliza
EnterpriseServices, el objeto debe derivar de ServicedComponent (que se deriva a su vez de
ContextBoundObject).
Cuando el código administrado llama a objetos COM, siempre sigue reglas COM. En otras
palabras, llama a través de un proxy de apartamento COM y contenedores de contexto COM+
1.0, según dicta OLE32.
Problemas de bloqueo
Si un subproceso realiza una llamada no administrada al sistema operativo que ha bloqueado el
subproceso en código no administrado, el motor en tiempo de ejecución no tomará el control del
mismo para Thread.Interrupt o Thread.Abort. En el caso de Thread.Abort, el tiempo de
ejecución marca el subproceso para Abort y toma control del mismo cuando vuelve a entrar en
el código administrado. Es preferible utilizar el bloqueo administrado en lugar del bloqueo no
administrado. WaitHandle.WaitOne ,WaitHandle.WaitAny, WaitHandle.WaitAll,
Monitor.Enter, Monitor.TryEnter, Thread.Join, GC.WaitForPendingFinalizers, etc. responden
todos a Thread.Interrupt y a Thread.Abort. Además, si su subproceso se realiza en un
contenedor uniproceso, todas estas operaciones de bloqueo administradas suministrarán
correctamente los mensajes al contenedor mientras que el subproceso está bloqueado.
1.1.7. Thread.Suspend, recolección de elementos no utilizados y puntos
de seguridad

MCT: Luis Dueñas Pag 15 de 336


Manual de .NET Framework 4.5

Cuando se llama a Thread.Suspend en un subproceso, el sistema detecta que se ha solicitado la


suspensión de un subproceso y permite que el subproceso se ejecute hasta que alcance un punto
seguro antes de suspenderlo. En un subproceso, un punto seguro es un punto en su ejecución en
el que es posible realizar la recolección de elementos no utilizados.
Una vez que se alcanza un punto seguro, el motor en tiempo de ejecución garantiza que el
subproceso suspendido no seguirá avanzando en código administrado. Un subproceso que se
ejecute fuera de código administrado es siempre seguro para la recolección de elementos no
utilizados, y su ejecución continúa hasta que intenta reanudar la ejecución de código
administrado.
Nota
Para realizar una recolección de elementos no utilizados, el motor en tiempo de ejecución debe
suspender todos los subprocesos, salvo el subproceso que realiza la recolección. Cada
subproceso debe llevarse a un punto de seguridad para que se pueda suspender.

1.1.8. Almacenamiento local de subprocesos: Campos estáticos


relacionados con subprocesos y ranuras de datos
Puede usar TSL (Thread Local Storage, almacenamiento local de subprocesos) para almacenar
datos que sean exclusivos de un subproceso y dominio de aplicación. .NET Framework permite
utilizar TLS administrado de dos maneras: campos estáticos relacionados con subprocesos y
ranuras de datos.
 Si puede anticipar sus necesidades exactas en el momento de la compilación, utilice campos
estáticos relacionados con subprocesos (campos Shared relacionados con subprocesos en
Visual Basic). Los campos estáticos relacionados con subprocesos proporcionan el máximo
rendimiento. También ofrecen las ventajas de la comprobación de tipos en tiempo de
compilación.
 Utilice ranuras de datos cuando las necesidades reales sólo se puedan detectar en tiempo de
ejecución. Las ranuras de datos son más lentas y tediosas de usar que los campos estáticos
relacionados con subprocesos, y los datos se almacenan con el tipo Object, por lo que debe
convertirlos al tipo correcto antes de utilizarlos.
En C++ no administrado, se utiliza TlsAlloc para asignar ranuras de forma dinámica y
__declspec(thread) para declarar que una variable debe asignarse a un almacenamiento
relacionado con subprocesos. Los campos estáticos relacionados con subprocesos y las ranuras
de datos ofrecen la versión administrada de este comportamiento.
En .NET Framework 4, puede usar la clase System.Threading.ThreadLocal<T> para crear
objetos de subprocesos locales que se inicializan de forma diferida la primera vez que se
consume el objeto.
Exclusividad de los datos en TLS administrado
Tanto si utiliza campos estáticos relacionados con subprocesos o ranuras de datos, los datos de
TLS administrado son exclusivos de la combinación de subproceso y dominio de aplicación.
 En un dominio de aplicación, un subproceso no puede modificar los datos de otro
subproceso, aunque ambos subprocesos utilicen el mismo campo o ranura.
 Cuando un subproceso tiene acceso al mismo campo o ranura de varios dominios de
aplicación, se mantiene un valor independiente en cada dominio de aplicación.
Por ejemplo, si un subproceso establece el valor de un campo estático relacionado con
subprocesos, entra en otro dominio de aplicación y, a continuación, recupera el valor del campo,
el valor recuperado en el segundo dominio de aplicación difiere del valor en el primer dominio
de aplicación. Establecer un nuevo valor para el campo en el segundo dominio de aplicación no
afecta al valor del campo en el primer dominio de aplicación.

MCT: Luis Dueñas Pag 16 de 336


Manual de .NET Framework 4.5

De igual forma, cuando un subproceso obtiene la misma ranura de datos con nombre en dos
dominios de aplicación diferentes, los datos del primer dominio de aplicación son
independientes de los datos del segundo dominio de aplicación.
Campos estáticos relacionados con subprocesos
Si sabe que un dato siempre es exclusivo de una combinación de subproceso y dominio de
aplicación, aplique el atributo ThreadStaticAttribute al campo estático. Utilice el campo igual
que cualquier otro campo estático. Los datos del campo son exclusivos de cada subproceso que
los utiliza.
Los campos estáticos relacionados con subprocesos proporcionan un rendimiento mejor que las
ranuras de datos y tienen la ventaja de que comprueban los tipos en tiempo de compilación.
Tenga en cuenta que cualquier código constructor de clases se ejecutará en el primer subproceso
del primer contexto que tenga acceso al campo. En todos los demás subprocesos o contextos del
mismo dominio de aplicación, los campos se inicializarán en null (Nothing en Visual Basic) si
son tipos de referencia, o en sus valores predeterminados si son tipos de valor. Por consiguiente,
no debe confiar en los constructores de clases para inicializar campos estáticos relacionados con
subprocesos. En su lugar, evite inicializar los campos estáticos relacionados con subprocesos y
asuma que se inicializan en null (Nothing) o en sus valores predeterminados.
Ranuras de datos
.NET Framework proporciona ranuras de datos dinámicos que son exclusivas de una
combinación de dominio de aplicación y subproceso. Hay dos tipos de ranuras de datos: con
nombre y sin nombre. Ambas se implementan mediante la estructura LocalDataStoreSlot.
 Para crear una ranura de datos con nombre, utilice el método
Thread.AllocateNamedDataSlot o Thread.GetNamedDataSlot. Para obtener una referencia a
una ranura con nombre existente, pase su nombre al método GetNamedDataSlot.
 Para crear una ranura de datos sin nombre, utilice el método Thread.AllocateDataSlot.
Para las ranuras con nombre y sin nombre, utilice los métodos Thread.SetData y
Thread.GetData para establecer y recuperar la información de la ranura. Éstos son métodos
estáticos que siempre representan los datos del subproceso que los está ejecutando.
Las ranuras con nombre pueden resultar cómodas, porque puede recuperar la ranura cuando lo
necesite pasando su nombre al método GetNamedDataSlot, en lugar de mantener una referencia
a una ranura sin nombre. Sin embargo, si otro componente usa el mismo nombre para su propio
almacenamiento relacionado con subprocesos y un subproceso ejecuta código tanto de su
componente como del otro, ambos componentes podrían dañar los datos del otro. (En este
escenario se supone que ambos componentes se están ejecutando en el mismo dominio de
aplicación y que no están diseñados para compartir los mismos datos.)
1.1.9. Cancelación en subprocesos administrados
.NET Framework 4 presenta un nuevo modelo unificado para la cancelación cooperativa de
operaciones asincrónicas o sincrónicas de ejecución prolongada. Este modelo se basa en un
objeto ligero denominado token de cancelación. El objeto que invoca una operación cancelable,
por ejemplo creando un nuevo subproceso o tarea, pasa el token a la operación. Esa operación
puede pasar a su vez copias del token a otras operaciones. En algún momento posterior, el
objeto que creó el token puede usarlo para solicitar que la operación deje de hacer lo que está
haciendo. Solo el objeto solicitante puede emitir la solicitud de cancelación y cada agente de
escucha es responsable de observar la solicitud y responder a ella de manera puntual. En la
siguiente ilustración se muestra la relación entre un origen de token y todas las copias de su
token.

MCT: Luis Dueñas Pag 17 de 336


Manual de .NET Framework 4.5

El nuevo modelo de cancelación simplifica la creación de aplicaciones y bibliotecas preparadas


para la cancelación, y admite las siguientes características:
 La cancelación es cooperativa y no se fuerza en el agente de escucha. El agente de
escucha determina cómo finalizar correctamente como respuesta a una solicitud de
cancelación.
 Solicitar es distinto que escuchar. Un objeto que invoca una operación cancelable puede
controlar cuándo se solicita la cancelación (si ocurre alguna vez).
 El objeto solicitante emite la solicitud de cancelación a todas las copias del token
usando simplemente una llamada a un método.
 Un agente de escucha puede escuchar varios tokens simultáneamente combinándolos en
un token vinculado.
 El código de usuario puede observar y responder a las solicitudes de cancelación desde
código de biblioteca, y éste puede observar y responder a las solicitudes de cancelación
desde código de usuario.
 Se puede notificar a los agentes de escucha de las solicitudes de cancelación sondeando,
registrando la devolución de llamada o esperando en identificadores de espera.
Nuevos tipos de cancelación
El nuevo marco de cancelación se implementa como un conjunto de tipos relacionados, que se
enumeran en la tabla siguiente.
Nombre de tipo Descripción
Objeto que crea un token de cancelación y también emite la
CancellationTokenSource
solicitud de cancelación para todas las copias de ese token.
Tipo de valor ligero pasado a uno o más agentes de escucha,
normalmente como un parámetro del método. Los agentes de
CancellationToken escucha supervisan el valor de la propiedad
IsCancellationRequested del token sondeando, devolviendo la
llamada o en un identificador de espera.
Las nuevas sobrecargas de esta excepción aceptan
CancellationToken como parámetro de entrada. Los agentes de
OperationCanceledException escucha pueden producir opcionalmente esta excepción para
comprobar el origen de la cancelación y notificar a otros que ha
respondido a una solicitud de cancelación.
El nuevo modelo de cancelación se integra en .NET Framework en varios tipos. Los más
importantes son System.Threading.Tasks.Parallel, System.Threading.Tasks.Task,
System.Threading.Tasks. Task<TResult> y System.Linq.ParallelEnumerable. Se recomienda
usar este nuevo modelo de cancelación para todo el código de biblioteca y aplicación nuevo.
Ejemplo de código
En el ejemplo siguiente, el objeto solicitante crea un objeto CancellationTokenSource y, a
continuación, pasa su propiedad Token a la operación cancelable. La operación que recibe la
solicitud supervisa el valor de la propiedad IsCancellationRequested del token mediante sondeo.
Cuando el valor se convierte en true, el agente de escucha puede finalizar de la manera
adecuada. En este ejemplo, el método simplemente sale, que es todo lo necesario en muchos
casos.

MCT: Luis Dueñas Pag 18 de 336


Manual de .NET Framework 4.5

Nota
En el ejemplo se usa el método QueueUserWorkItem para mostrar que el nuevo marco de
cancelación es compatible con las API heredadas.
static void CancelWithThreadPoolMiniSnippet()
{
//Thread 1: The Requestor
// Create the token source.
CancellationTokenSource cts = new CancellationTokenSource();
// Pass the token to the cancelable operation.
ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
// Request cancellation by setting a flag on the token.
cts.Cancel();
}

//Thread 2: The Listener


static void DoSomeWork(object obj)
{
CancellationToken token = (CancellationToken)obj;
for (int i = 0; i < 100000; i++)
{
// Simulating work.
Thread.SpinWait(5000000);
if (token.IsCancellationRequested)
{
// Perform cleanup if necessary. Terminate the operation.
break;
}
}
}

Cancelación de operaciones frente a cancelación de objetos


En el nuevo marco de cancelación, la cancelación se refiere a operaciones, no a objetos. La
solicitud de cancelación significa que la operación se debe detener lo antes posible una vez
realizada cualquier limpieza necesaria. Un token de cancelación debe hacer referencia a una
"operación cancelable"; sin embargo, esa operación se puede implementar en su programa. Una
vez establecida en true la propiedad IsCancellationRequested del token, no se puede restablecer
a false. Por tanto, los tokens de cancelación no se pueden reutilizar una vez cancelados.
Si necesita un mecanismo de cancelación de objetos, puede basarlo en el mecanismo de
cancelación de operaciones, como se muestra en el ejemplo siguiente.
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// User defined Class with its own method for cancellation
var obj1 = new MyCancelableObject();
var obj2 = new MyCancelableObject();
var obj3 = new MyCancelableObject();
// Register the object's cancel method with the token's cancellation request.
token.Register(() => obj1.Cancel());
token.Register(() => obj2.Cancel());
token.Register(() => obj3.Cancel());
// Request cancellation on the token.
cts.Cancel();

Si un objeto admite más de una operación cancelable simultánea, pase un token diferente como
entrada a cada operación cancelable distinta. De esa forma, se puede cancelar una operación sin
afectar a las demás.
Realizar escuchas y responder a solicitudes de cancelación
El implementador de una operación cancelable determina, en el delegado de usuario, cómo
finalizar la operación en respuesta a una solicitud de cancelación. En muchos casos, el delegado
de usuario puede realizar simplemente cualquier limpieza necesaria y volver inmediatamente.

MCT: Luis Dueñas Pag 19 de 336


Manual de .NET Framework 4.5

Sin embargo, en casos más complejos podría ser necesario que un delegado de usuario notificara
al código de biblioteca que se ha producido la cancelación. En estos casos, la manera correcta de
finalizar la operación es que el delegado llame a ThrowIfCancellationRequested, que producirá
OperationCanceled Exception. Las nuevas sobrecargas de esta excepción en .NET Framework 4
toman CancellationToken como argumento. El código de biblioteca puede detectar esta
excepción en el subproceso de delegado de usuario y examinar el token de la excepción para
determinar si la excepción indica una cancelación cooperativa o alguna otra situación
excepcional.
La clase Task controla OperationCanceledException de esta forma.
Realizar escuchas mediante sondeo
En el caso de cálculos de ejecución prolongada que usan bucles o recorridos, puede escuchar
una solicitud de cancelación sondeando periódicamente el valor de la propiedad
CancellationToken. IsCancellationRequested. Si el valor es true, el método debe limpiar y
finalizar lo más rápidamente posible. La frecuencia óptima de sondeo depende del tipo de
aplicación. Es el desarrollador quien determina la mejor frecuencia de sondeo para cualquier
programa dado. El sondeo propiamente dicho no afecta al rendimiento considerablemente. En el
ejemplo siguiente se muestra una forma posible de sondeo.
static void NestedLoops(Rectangle rect, CancellationToken token)
{
for (int x = 0; x < rect.columns && !token.IsCancellationRequested; x++)
{
for (int y = 0; y < rect.rows; y++)
{
// Simulating work.
Thread.SpinWait(5000);
Console.Write("{0},{1} ", x, y);
}
// Assume that we know that the inner loop is very fast.
// Therefore, checking once per row is sufficient.
if (token.IsCancellationRequested)
{
// Cleanup or undo here if necessary...
Console.WriteLine("\r\nCancelling after row {0}.", x);
Console.WriteLine("Press any key to exit.");
// then...
break;
// ...or, if using Task:
// token.ThrowIfCancellationRequested();
}
}
}

Realizar escuchas registrando una devolución de llamada


Algunas operaciones se pueden bloquear de forma que no puedan comprobar el valor del token
de cancelación a tiempo. En estos casos, puede registrar un método de devolución de llamada
que desbloquee el método cuando se reciba una solicitud de cancelación.
El método Register devuelve un objeto CancellationTokenRegistration que se usa
específicamente para este fin. En el ejemplo siguiente se muestra cómo usar el método Register
para cancelar una solicitud web asincrónica.
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
WebClient wc = new WebClient();
// To request cancellation on the token will call CancelAsync on the
WebClient.
token.Register(() => wc.CancelAsync());
Console.WriteLine("Starting request");
wc.DownloadStringAsync(new Uri("http://www.contoso.com"));

MCT: Luis Dueñas Pag 20 de 336


Manual de .NET Framework 4.5

El objeto CancellationTokenRegistration administra la sincronización de los subprocesos y


asegura que la devolución de llamada dejará de ejecutarse en un momento concreto.
Para asegurar que el sistema responde y para evitar interbloqueos, es preciso tener en cuenta las
siguientes pautas a la hora de registrar las devoluciones de llamada:
 El método de devolución de llamada debe ser rápido porque se invoca de forma
sincrónica, por lo que la llamada a Cancel no devuelve ningún valor hasta que la
devolución de llamada devuelva un valor.
 Si llama a Dispose mientras se ejecuta la devolución de llamada, y mantiene un bloqueo
que la devolución de llamada está esperando, se puede producir un interbloqueo en el
programa. Después de que Dispose devuelva un valor, se podrán liberar todos los
recursos que la devolución de llamada necesite.
 No se deben realizar subprocesos manuales ni usar SynchronizationContext en las
devoluciones de llamada. Si una devolución de llamada debe ejecutarse en un
subproceso concreto, utilice el constructor
System.Threading.CancellationTokenRegistration, que permite especificar que la clase
syncContext de destino es la propiedad SynchronizationContext.Current activa. Si se
realizan subprocesos manuales en una devolución de llamada, se puede producir un
interbloqueo.
Realizar escuchas mediante un identificador de espera
Cuando una operación cancelable se puede bloquear mientras espera en un primitiva de
sincronización como System.Threading.ManualResetEvent o System.Threading.Semaphore,
puede usar la propiedad CancellationToken.WaitHandle para permitir que la operación espere
tanto en el evento como en la solicitud de cancelación. El identificador de espera del token de
cancelación se señalizará como respuesta a una solicitud de cancelación y el método puede
emplear el valor devuelto del método WaitAny para determinar si se señalizó el token de
cancelación. A continuación, la operación puede salir simplemente o producir
OperationCanceledException, según corresponda.
// Wait on the event if it is not signaled.
int eventThatSignaledIndex = WaitHandle.WaitAny(new WaitHandle[] { mre,
token.WaitHandle },new TimeSpan(0, 0, 20));

En el nuevo código destinado a .NET Framework 4, tanto


System.Threading.ManualResetEventSlim como System.Threading.SemaphoreSlim admiten el
nuevo marco de cancelación en sus métodos Wait. Puede pasar CancellationToken al método y,
cuando se solicite la cancelación, el evento se reactivará y producirá una excepción
OperationCanceledException.
try
{
// mres is a ManualResetEventSlim
mres.Wait(token);
}
catch (OperationCanceledException)
{
// Throw immediately to be responsive. The
// alternative is to do one more item of work,
// and throw on next iteration, because IsCancellationRequested will be
true.
Console.WriteLine("The wait operation was canceled.");
throw;
}
Console.Write("Working...");
// Simulating work.
Thread.SpinWait(500000);

MCT: Luis Dueñas Pag 21 de 336


Manual de .NET Framework 4.5

Realizar escuchas en varios tokens simultáneamente


En algunos casos, un agente de escucha puede tener que escuchar varios tokens de cancelación
simultáneamente. Por ejemplo, una operación cancelable puede tener que supervisar un token de
cancelación interno además de un token pasado externamente como un argumento a un
parámetro de un método. Para ello, cree un origen de token vinculado que pueda combinar dos o
más tokens en uno, como se muestra en el ejemplo siguiente.
public void DoWork(CancellationToken externalToken)
{
// Create a new token that combines the internal and external tokens.
this.internalToken = internalTokenSource.Token;
this.externalToken = externalToken;

using (CancellationTokenSource linkedCts =


CancellationTokenSource.CreateLinkedTokenSource(internalToken,
externalToken))
{
try
{
DoWorkInternal(linkedCts.Token);
}
catch (OperationCanceledException)
{
if (internalToken.IsCancellationRequested)
{
Console.WriteLine("Operation timed out.");
}
else if (externalToken.IsCancellationRequested)
{
Console.WriteLine("Cancelling per user request.");
externalToken.ThrowIfCancellationRequested();
}
}
}
}

Tenga en cuenta que debe llamar a Dispose en el origen de token vinculado cuando haya
terminado con él.
Cooperación entre código de biblioteca y código de usuario
El marco de cancelación unificada permite que el código de biblioteca cancele código de
usuario y que el código de usuario cancele código de biblioteca de manera cooperativa. Una
buena cooperación depende de que cada lado siga estas instrucciones:

 Si el código de biblioteca proporciona operaciones cancelables, también debe


proporcionar métodos públicos que acepten un token de cancelación externo para que el
código de usuario pueda solicitar la cancelación.
 Si el código de biblioteca llama a código de usuario, el código de biblioteca debe
interpretar OperationCanceledException(externalToken) como una cancelación
cooperativa y no necesariamente como una excepción de error.
 Los delegados de usuario deben intentar responder a las solicitudes de cancelación del
código de biblioteca de manera puntual.

System.Threading.Tasks.Task y System.Linq.ParallelEnumerable son ejemplos de clases que


siguen estas instrucciones.
1.1.9.1. Cómo: Realizar escuchas de solicitudes mediante sondeo
En el ejemplo siguiente se muestra una manera en que el código de usuario puede sondear un
token de cancelación a intervalos periódicos para ver si el subproceso que realiza la llamada ha
solicitado la cancelación. En este ejemplo se usa el tipo System.Threading.Tasks.Task, pero el

MCT: Luis Dueñas Pag 22 de 336


Manual de .NET Framework 4.5

mismo modelo es aplicable a las operaciones asincrónicas creadas directamente por el tipo
System.Threading.ThreadPool o System.Threading.Thread.
Ejemplo
El sondeo necesita algún tipo de bucle o código recursivo que pueda leer periódicamente el
valor de la propiedad booleana IsCancellationRequested. Si está usando el tipo
System.Threading.Tasks.Task y espera que la tarea se complete en el subproceso que realiza la
llamada, puede emplear el método ThrowIfCancellationRequested para comprobar la propiedad
y producir la excepción. Mediante este método, se asegura de que se produce la excepción
correcta como respuesta a una solicitud. Si está usando Task, es mejor llamar a este método que
producir manualmente una excepción OperationCanceledException. Si no tiene que producir la
excepción, simplemente puede comprobar la propiedad y volver del método si la propiedad es
true.
class CancelByPolling
{
static void Main()
{
var tokenSource = new CancellationTokenSource();
// Toy object for demo purposes
Rectangle rect = new Rectangle() { columns = 1000, rows = 500 };
// Simple cancellation scenario #1. Calling thread does not wait
// on the task to complete, and the user delegate simply returns
// on cancellation request without throwing.
Task.Run(() => NestedLoops(rect, tokenSource.Token),
tokenSource.Token);
// Simple cancellation scenario #2. Calling thread does not wait
// on the task to complete, and the user delegate throws
// OperationCanceledException to shut down task and transition its
state.
// Task.Run(() => PollByTimeSpan(tokenSource.Token),
tokenSource.Token);
Console.WriteLine("Press 'c' to cancel");
if (Console.ReadKey().KeyChar == 'c')
{
tokenSource.Cancel();
Console.WriteLine("Press any key to exit.");
}
Console.ReadKey();
}

static void NestedLoops(Rectangle rect, CancellationToken token)


{
for (int x = 0; x < rect.columns && !token.IsCancellationRequested;
x++)
{
for (int y = 0; y < rect.rows; y++)
{
// Simulating work.
Thread.SpinWait(5000);
Console.Write("{0},{1} ", x, y);
}
// Assume that we know that the inner loop is very fast.
// Therefore, checking once per row is sufficient.
if (token.IsCancellationRequested)
{
// Cleanup or undo here if necessary...
Console.WriteLine("\r\nCancelling after row {0}.", x);
Console.WriteLine("Press any key to exit.");
// then...
break;
// ...or, if using Task:
// token.ThrowIfCancellationRequested();
}
}
}

MCT: Luis Dueñas Pag 23 de 336


Manual de .NET Framework 4.5

La llamada a ThrowIfCancellationRequested es sumamente rápida y no presenta una gran


sobrecarga en los bucles.
Si está llamando a ThrowIfCancellationRequested, solo tiene que comprobar explícitamente la
propiedad IsCancellationRequested si tiene que hacer otro trabajo como respuesta a la
cancelación además de producir la excepción. En este ejemplo, puede ver que el código
realmente tiene acceso dos veces a la propiedad: una vez en el acceso explícito y de nuevo en el
método ThrowIfCancellationRequested. Pero puesto que el acto de leer la propiedad
IsCancellationRequested implica solo una instrucción de lectura volátil por acceso, el acceso
doble no es significativo desde una perspectiva de rendimiento. Sigue siendo preferible llamar al
método en lugar de producir manualmente OperationCanceledException.
1.1.9.2. Cómo: Registrar devoluciones de llamadas de solicitudes de
cancelación
En el ejemplo siguiente, se muestra cómo registrar un delegado que será invocado cuando una
propiedad IsCancellationRequested sea verdadera debido a una llamada a Cancel en el objeto
que creó el token. Emplee esta técnica para cancelar operaciones asincrónicas que no admiten el
marco de cancelación unificada de forma nativa y para desbloquear métodos que podrían estar
esperando la finalización de una operación asincrónica.
Nota
Cuando está habilitada la opción "Solo mi código", en algunos casos, Visual Studio se
interrumpe en la línea que produce la excepción y muestra el mensaje de error "Excepción no
controlada por el código de usuario". Este error es benigno. Puede presionar F5 para continuar y
ver el comportamiento de control de excepciones que se muestra en los ejemplos siguientes.
Para evitar que Visual Studio se interrumpa con el primer error, desactive la casilla "Solo mi
código" bajo Herramientas, Opciones, Depuración, General.
Ejemplo
En el ejemplo siguiente, el método CancelAsync se registra como el método que se va a invocar
cuando se solicite la cancelación a través del token.
namespace Cancel3
{
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

class CancelWithCallback
{
static void Main(string[] args)
{
var cts = new CancellationTokenSource();
// Start cancelable task.
Task t = Task.Run(() =>
{
DoWork(cts.Token);
});
Console.WriteLine("Press 'c' to cancel.");
char ch = Console.ReadKey().KeyChar;
if (ch == 'c')
{
cts.Cancel();
}
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}

MCT: Luis Dueñas Pag 24 de 336


Manual de .NET Framework 4.5

static void DoWork(CancellationToken token)


{
WebClient wc = new WebClient();
// Create an event handler to receive the result.
wc.DownloadStringCompleted += (obj, e) =>
{
// Checks status of WebClient, not external token
if (!e.Cancelled)
{
Console.WriteLine(e.Result + "\r\nPress any key.");
}
else
Console.WriteLine("Download was canceled.");
};
// Do not initiate download if the external token
// has already been canceled.
if (!token.IsCancellationRequested)
{
// Register the callback to a method that can unblock.
// Dispose of the CancellationTokenRegistration object
// after the callback has completed.
using (CancellationTokenRegistration ctr = token.Register(()
=>
wc.CancelAsync()))
{
Console.WriteLine("Starting request");
wc.DownloadStringAsync(new Uri("http://www.contoso.com"));
}
}
}
}
}

Si ya se ha solicitado la cancelación cuando se registra la devolución de llamada, se garantiza


que se sigue llamando a la devolución de llamada. En este caso concreto, el método
CancelAsync no hará nada si no hay ninguna operación asincrónica en curso, por lo que siempre
es seguro llamar al método.
1.1.9.3. Cómo: Realizar escuchas de solicitudes de cancelación cuando
tienen controladores de espera
Si se bloquea un método mientras está esperando la señalización de un evento, no puede
comprobar el valor del token de cancelación y responder a tiempo. En el primer ejemplo se
muestra cómo resolver este problema cuando está trabajando con eventos como
System.Threading.ManualResetEvent que no admiten el marco de cancelación unificada de
forma nativa. En el segundo ejemplo se muestra un enfoque más sencillo que usa
System.Threading.ManualResetEventSlim, que admite cancelación unificada.
Nota
Cuando está habilitada la opción "Solo mi código", en algunos casos, Visual Studio se
interrumpe en la línea que produce la excepción y muestra el mensaje de error "Excepción no
controlada por el código de usuario". Este error es benigno. Puede presionar F5 para continuar y
ver el comportamiento de control de excepciones que se muestra en los ejemplos siguientes.
Para evitar que Visual Studio se interrumpa con el primer error, desactive la casilla "Solo mi
código" bajo Herramientas, Opciones, Depuración, General.
Ejemplo
En el ejemplo siguiente se usa ManualResetEvent para mostrar cómo desbloquear
identificadores de espera que no admiten cancelación unificada.
using System;
using System.Threading;
using System.Threading.Tasks;

MCT: Luis Dueñas Pag 25 de 336


Manual de .NET Framework 4.5

class CancelOldStyleEvents
{
// Old-style MRE that doesn't support unified cancellation.
static ManualResetEvent mre = new ManualResetEvent(false);

static void Main()


{
var cts = new CancellationTokenSource();
// Pass the same token source to the delegate and to the task
instance.
Task.Run(() => DoWork(cts.Token), cts.Token);
Console.WriteLine("Press s to start/restart, p to pause, or c to
cancel.");
Console.WriteLine("Or any other key to exit.");
// Old-style UI thread.
bool goAgain = true;
while (goAgain)
{
char ch = Console.ReadKey().KeyChar;
switch (ch)
{
case 'c':
cts.Cancel();
break;
case 'p':
mre.Reset();
break;
case 's':
mre.Set();
break;
default:
goAgain = false;
break;
}
Thread.Sleep(100);
}
}

static void DoWork(CancellationToken token)


{
while (true)
{
// Wait on the event if it is not signaled.
int eventThatSignaledIndex = WaitHandle.WaitAny(new WaitHandle[]
{ mre, token.WaitHandle }, new TimeSpan(0, 0, 20));
// Were we canceled while waiting?
if (eventThatSignaledIndex == 1)
{
Console.WriteLine("The wait operation was canceled.");
throw new OperationCanceledException(token);
}
// Were we canceled while running?
else if (token.IsCancellationRequested)
{
Console.WriteLine("I was canceled while running.");
token.ThrowIfCancellationRequested();
}
// Did we time out?
else if (eventThatSignaledIndex == WaitHandle.WaitTimeout)
{
Console.WriteLine("I timed out.");
break;
}
else
{
Console.Write("Working... ");
// Simulating work.

MCT: Luis Dueñas Pag 26 de 336


Manual de .NET Framework 4.5

Thread.SpinWait(5000000);
}
}
}
}

En el ejemplo siguiente se usa ManualResetEventSlim para mostrar cómo desbloquear


primitivas de coordinación que admiten cancelación unificada. Se puede usar el mismo enfoque
con otras primitivas de coordinación ligeras, como SemaphoreSlim y CountdownEvent.
class CancelNewStyleEvents
{
// New-style MRESlim that supports unified cancellation in its Wait
methods.
static ManualResetEventSlim mres = new ManualResetEventSlim(false);

static void Main()


{
var cts = new CancellationTokenSource();
// Pass the same token source to the delegate and to the task
instance.
Task.Run(() => DoWork(cts.Token), cts.Token);
Console.WriteLine("Press c to cancel, p to pause, or s to
start/restart,");
Console.WriteLine("or any other key to exit.");
// New-style UI thread.
bool goAgain = true;
while (goAgain)
{
char ch = Console.ReadKey().KeyChar;
switch (ch)
{
case 'c':
// Token can only be canceled once.
cts.Cancel();
break;
case 'p':
mres.Reset();
break;
case 's':
mres.Set();
break;
default:
goAgain = false;
break;
}
Thread.Sleep(100);
}
}

static void DoWork(CancellationToken token)


{
while (true)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("Canceled while running.");
token.ThrowIfCancellationRequested();
}
// Wait on the event to be signaled or the token to be canceled,
// whichever comes first. The token will throw an exception if
// it is canceled while the thread is waiting on the event.
try
{
// mres is a ManualResetEventSlim
mres.Wait(token);
}
catch (OperationCanceledException)

MCT: Luis Dueñas Pag 27 de 336


Manual de .NET Framework 4.5

{
// Throw immediately to be responsive. The alternative is to
// do one more item of work, and throw on next iteration,
because
// IsCancellationRequested will be true.
Console.WriteLine("The wait operation was canceled.");
throw;
}
Console.Write("Working...");
// Simulating work.
Thread.SpinWait(500000);
}
}
}

1.1.9.4. Cómo: Realizar escuchas de varias solicitudes de cancelación


En este ejemplo se muestra cómo escuchar dos tokens de cancelación simultáneamente de
manera que pueda cancelar una operación si uno de los tokens lo solicita.
Nota
Cuando está habilitada la opción "Solo mi código", en algunos casos, Visual Studio se
interrumpe en la línea que produce la excepción y muestra el mensaje de error "Excepción no
controlada por el código de usuario". Este error es benigno. Puede presionar F5 para continuar y
ver el comportamiento de control de excepciones que se muestra en los ejemplos siguientes.
Para evitar que Visual Studio se interrumpa con el primer error, desactive la casilla "Solo mi
código" bajo Herramientas, Opciones, Depuración, General.
Ejemplo
En el ejemplo siguiente se usa el método CreateLinkedTokenSource para combinar dos tokens
en uno. Esto permite pasar el token a métodos que solo toman un token de cancelación como
argumento. En el ejemplo se muestra un escenario común en el que un método debe observar un
token pasado desde fuera de la clase y un token generado dentro de la clase.
namespace WaitForMultiple
{
using System;
using System.Threading;
using System.Threading.Tasks;

class LinkedTokenSourceDemo
{
static void Main()
{
WorkerWithTimer worker = new WorkerWithTimer();
CancellationTokenSource cts = new CancellationTokenSource();

// Task for UI thread, so we can call Task.Wait wait on the main


thread.
Task.Run(() =>
{
Console.WriteLine("Press 'c' to cancel within 3 seconds after
work begins.");
Console.WriteLine("Or let the task time out by doing
nothing.");
if (Console.ReadKey().KeyChar == 'c') cts.Cancel();
});
// Let the user read the UI message.
Thread.Sleep(1000);
// Start the worker task.
Task task = Task.Run(() => worker.DoWork(cts.Token), cts.Token);
try
{
task.Wait(cts.Token);
}

MCT: Luis Dueñas Pag 28 de 336


Manual de .NET Framework 4.5

catch (OperationCanceledException e)
{
if (e.CancellationToken == cts.Token)
Console.WriteLine("Canceled from UI thread throwing
OCE.");
}
catch (AggregateException ae)
{
Console.WriteLine("AggregateException caught: " +
ae.InnerException);
foreach (var inner in ae.InnerExceptions)
{
Console.WriteLine(inner.Message + inner.Source);
}
}
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}

class WorkerWithTimer
{
CancellationTokenSource internalTokenSource = new
CancellationTokenSource();
CancellationToken internalToken;
CancellationToken externalToken;
Timer timer;

public WorkerWithTimer()
{
internalTokenSource = new CancellationTokenSource();
internalToken = internalTokenSource.Token;
// A toy cancellation trigger that times out after 3 seconds
// if the user does not press 'c'.
timer = new Timer(new TimerCallback(CancelAfterTimeout), null,
3000, 3000);
}

public void DoWork(CancellationToken externalToken)


{
// Create a new token that combines the internal and external
tokens.
this.internalToken = internalTokenSource.Token;
this.externalToken = externalToken;
using (CancellationTokenSource linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(internalToken,
externalToken))
{
try
{
DoWorkInternal(linkedCts.Token);
}
catch (OperationCanceledException)
{
if (internalToken.IsCancellationRequested)
{
Console.WriteLine("Operation timed out.");
}
else if (externalToken.IsCancellationRequested)
{
Console.WriteLine("Cancelling per user request.");
externalToken.ThrowIfCancellationRequested();
}
}
}
}

private void DoWorkInternal(CancellationToken token)

MCT: Luis Dueñas Pag 29 de 336


Manual de .NET Framework 4.5

{
for (int i = 0; i < 1000; i++)
{
if (token.IsCancellationRequested)
{
// We need to dispose the timer if cancellation
// was requested by the external token.
timer.Dispose();
// Throw the exception.
token.ThrowIfCancellationRequested();
}
// Simulating work.
Thread.SpinWait(7500000);
Console.Write("working... ");
}
}

public void CancelAfterTimeout(object state)


{
Console.WriteLine("\r\nTimer fired.");
internalTokenSource.Cancel();
timer.Dispose();
}
}
}

Cuando el token vinculado produce OperationCanceledException, el token que se pasa a la


excepción es el token vinculado, no uno de los token del predecesor. Para determinar cuál de los
tokens se canceló, compruebe el estado de los tokens del predecesor directamente.
En este ejemplo, AggregateException no se debe producir nunca, pero se detecta aquí porque en
los escenarios reales todas las demás excepciones aparte de OperationCanceledException que se
producen desde el delegado de la tarea se encapsulan en una excepción
OperationCanceledException.
1.2. Utilizar subprocesos y subprocesamiento
Los temas de esta sección explican la creación y la administración de los subprocesos
administrados, cómo pasar datos a subprocesos administrados y recibir resultados devueltos, y
cómo destruir subprocesos y administrar una excepción ThreadAbortException.
1.2.1. Crear subprocesos y analizar los datos en el inicio
Al crear un proceso del sistema operativo, éste introduce un subproceso para ejecutar el código
de dicho proceso, incluido cualquier dominio de aplicación original. A partir de ese punto,
pueden crearse y destruirse dominios de aplicación sin que necesariamente se cree o destruya
ningún subproceso del sistema operativo. Si el código que se ejecuta es administrado, puede
obtenerse un objeto Thread para el subproceso que se ejecuta en el dominio de aplicación actual
si se recupera la propiedad estática CurrentThread de tipo Thread. En este tema se describe la
creación de subprocesos y se analizan las alternativas para pasar datos al procedimiento de
subproceso.
Crear un subproceso
Al crear un nuevo objeto Thread se crea un nuevo subproceso administrado. La clase Thread dispone de
constructores que toman un delegado ThreadStart o ParameterizedThreadStart; el delegado contiene el
método al que invoca el nuevo subproceso cuando se llama al método Start. Si se llama a Start más de una
vez, se produce una excepción ThreadStateException.
El método Start devuelve un resultado inmediatamente, con frecuencia antes de que el nuevo
subproceso se haya iniciado realmente. Puede utilizar las propiedades ThreadState y IsAlive
para determinar el estado del subproceso en cualquier momento, pero estas propiedades no
deben utilizarse nunca para sincronizar las actividades de los subprocesos.

MCT: Luis Dueñas Pag 30 de 336


Manual de .NET Framework 4.5

Nota
Una vez iniciado un subproceso, no es necesario conservar una referencia al objeto Thread. El
subproceso se continúa ejecutando hasta que el procedimiento del mismo finaliza.
En el ejemplo de código siguiente se crean dos subprocesos nuevos para llamar a métodos
estáticos y una instancia de otro objeto.
using System;
using System.Threading;

public class ServerClass


{
// The method that will be called when the thread is started.
public void InstanceMethod()
{
Console.WriteLine(
"ServerClass.InstanceMethod is running on another thread.");
// Pause for a moment to provide a delay to make threads more
apparent.
Thread.Sleep(3000);
Console.WriteLine(
"The instance method called by the worker thread has ended.");
}

public static void StaticMethod()


{
Console.WriteLine(
"ServerClass.StaticMethod is running on another thread.");
// Pause for a moment to provide a delay to make threads more
apparent.
Thread.Sleep(5000);
Console.WriteLine(
"The static method called by the worker thread has ended.");
}
}

public class Simple


{
public static void Main()
{
Console.WriteLine("Thread Simple Sample");
ServerClass serverObject = new ServerClass();
// Create the thread object, passing in the serverObject.
// InstanceMethod method using a ThreadStart delegate.
Thread InstanceCaller = new Thread(
new ThreadStart(serverObject.InstanceMethod));
// Start the thread.
InstanceCaller.Start();
Console.WriteLine("The Main() thread calls this after "
+ "starting the new InstanceCaller thread.");
// Create the thread object, passing in the serverObject.StaticMethod
// method using a ThreadStart delegate.
Thread StaticCaller = new Thread(new
ThreadStart(ServerClass.StaticMethod));
// Start the thread.
StaticCaller.Start();
Console.WriteLine("The Main() thread calls this after "
+ "starting the new StaticCaller thread.");
}
}

Pasar datos a subprocesos y recuperar datos de subprocesos


En la versión 2.0 de .NET Framework, el delegado ParameterizedThreadStart proporciona una
forma sencilla de pasar un objeto que contiene datos a un subproceso cuando se llama a la
sobrecarga del método Thread.Start.

MCT: Luis Dueñas Pag 31 de 336


Manual de .NET Framework 4.5

El uso del delegado ParameterizedThreadStart no constituye un modo con seguridad de tipos


para pasar los datos, dado que la sobrecarga del método Thread.Start acepta cualquier objeto.
Una alternativa consiste en encapsular el procedimiento de subproceso y los datos en una clase
auxiliar y utilizar el delegado ThreadStart para ejecutar el procedimiento de subproceso. Esta
técnica se muestra en los dos ejemplos de código siguientes.
Ninguno de estos delegados presenta un valor devuelto, porque no hay ningún lugar donde
devolver los datos de una llamada asincrónica. Para recuperar los resultados de un método de
subproceso, puede utilizar un método de respuesta, como se muestra en el segundo ejemplo de
código.
using System;
using System.Threading;

// The ThreadWithState class contains the information needed for


// a task, and the method that executes the task.
public class ThreadWithState
{
// State information used in the task.
private string boilerplate;
private int value;

// The constructor obtains the state information.


public ThreadWithState(string text, int number)
{
boilerplate = text;
value = number;
}

// The thread procedure performs the task, such as formatting


// and printing a document.
public void ThreadProc()
{
Console.WriteLine(boilerplate, value);
}
}

// Entry point for the example.


public class Example
{
public static void Main()
{
// Supply the state information required by the task.
ThreadWithState tws = new ThreadWithState(
"This report displays the number {0}.", 42);
// Create a thread to execute the task, and then
// start the thread.
Thread t = new Thread(new ThreadStart(tws.ThreadProc));
t.Start();
Console.WriteLine("Main thread does some work, then waits.");
t.Join();
Console.WriteLine("Independent task has completed; main thread
ends.");
}
}

Recuperar datos con métodos de devolución de llamada


En el siguiente ejemplo se muestra un método de respuesta que recupera datos de un
subproceso. El constructor de la clase que contiene los datos y el método del subproceso
también acepta un delegado que representa el método de devolución de llamada; antes de
finalizar, el método del subproceso invoca al delegado de devolución de llamada.
using System;
using System.Threading;

// The ThreadWithState class contains the information needed for

MCT: Luis Dueñas Pag 32 de 336


Manual de .NET Framework 4.5

// a task, the method that executes the task, and a delegate


// to call when the task is complete.
public class ThreadWithState
{
// State information used in the task.
private string boilerplate;
private int value;

// Delegate used to execute the callback method when the task is complete.
private ExampleCallback callback;

// The constructor obtains the state information and the callback


delegate.
public ThreadWithState(string text, int number,
ExampleCallback callbackDelegate)
{
boilerplate = text;
value = number;
callback = callbackDelegate;
}

// The thread procedure performs the task, such as


// formatting and printing a document, and then invokes
// the callback delegate with the number of lines printed.
public void ThreadProc()
{
Console.WriteLine(boilerplate, value);
if (callback != null) callback(1);
}
}

// Delegate that defines the signature for the callback method.


public delegate void ExampleCallback(int lineCount);

// Entry point for the example.


public class Example
{
public static void Main()
{
// Supply the state information required by the task.
ThreadWithState tws = new ThreadWithState(
"This report displays the number {0}.",42,
new ExampleCallback(ResultCallback)
);
Thread t = new Thread(new ThreadStart(tws.ThreadProc));
t.Start();
Console.WriteLine("Main thread does some work, then waits.");
t.Join();
Console.WriteLine("Independent task has completed; main thread
ends.");
}

// The callback method must match the signature of the callback delegate.
public static void ResultCallback(int lineCount)
{
Console.WriteLine("Independent task printed {0} lines.", lineCount);
}
}

1.2.2. Pausar y reanudar subprocesos


Las formas más habituales de sincronizar las actividades de los subprocesos consisten en
bloquear y liberar los subprocesos o en bloquear objetos o regiones de código.
También puede hacer que los subprocesos pasen a estar en suspensión por sí mismos. Cuando
los subprocesos se encuentran bloqueados o en suspensión, se puede utilizar
ThreadInterruptedException para interrumpir sus estados de espera.

MCT: Luis Dueñas Pag 33 de 336


Manual de .NET Framework 4.5

El método Thread.Sleep
Si se llama al método Thread.Sleep, el subproceso actual se bloquea inmediatamente durante el
número de milisegundos que se pasen a Thread.Sleep y el resto de su espacio de tiempo se
asigna a otro subproceso. Un subproceso no puede llamar a Thread.Sleep en otro subproceso.
Cuando se llama a Thread.Sleep con Timeout.Infinite, un subproceso pasa a un estado de
suspensión hasta que lo interrumpe otro subproceso que llama a Thread.Interrupt o hasta que
Thread.Abort finaliza dicho subproceso.
Interrumpir subprocesos
Puede interrumpir un subproceso en espera llamando a Thread.Interrupt en el subproceso
bloqueado para producir una excepción ThreadInterruptedException, que saca al subproceso de
la llamada que lo bloquea. El subproceso debe detectar la excepción
ThreadInterruptedException y hacer lo que sea necesario para seguir funcionando. Si el
subproceso pasa por alto la excepción, el motor en tiempo de ejecución detecta la excepción y
detiene el subproceso.
Nota
Si el subproceso de destino no está bloqueado cuando se llama a Thread.Interrupt, el subproceso
no se interrumpe hasta que se bloquea. Si el subproceso no se bloquea nunca, puede finalizar sin
ser interrumpido.
Si una espera es de tipo administrado, Thread.Interrupt y Thread.Abort activan el subproceso
inmediatamente. Si una espera es de tipo no administrado (por ejemplo, una llamada de
invocación de plataforma a la función WaitForSingleObject de Win32), ni Thread.Interrupt ni
Thread.Abort pueden tomar el control del subproceso hasta que éste vuelva o llame al código
administrado. En código administrado, el comportamiento es el siguiente:
 Thread.Interrupt activa un subproceso de cualquier tipo de espera y hace que se produzca
una excepción ThreadInterruptedException en el subproceso de destino.
 Thread.Abort es similar a Thread.Interrupt, con la diferencia de que hace que se produzca
una excepción ThreadAbortException en el subproceso.
Suspender y reanudar (obsoleto)
Importante
En la versión 2.0 de .NET Framework, los métodos Thread.Suspend y Thread.Resume están
marcados como obsoletos y, en futuras versiones, se quitarán estos métodos.
También puede pausar un subproceso si llama a Thread.Suspend. Cuando un subproceso llama a
Thread.Suspend en sí mismo, la llamada se bloquea hasta que el subproceso es reanudado por
otro subproceso. Cuando un subproceso llama a Thread.Suspend en otro subproceso, la llamada
no es de bloqueo y hace que el otro subproceso se pause. Al llamar a Thread.Resume, otro
subproceso sale del estado de suspensión y hace que el subproceso reanude la ejecución,
independientemente de cuántas veces se haya llamado a Thread.Suspend. Por ejemplo, si llama
a Thread.Suspend cinco veces consecutivas y, a continuación, llama a Thread.Resume, el
subproceso reanuda la ejecución inmediatamente después de llamar a Thread.Resume.
A diferencia de Thread.Sleep, Thread.Suspend no hace que un subproceso detenga
inmediatamente su ejecución. Common Language Runtime debe esperar hasta que el
subproceso alcance un punto de seguridad para poder suspender el subproceso. Un subproceso
no se puede suspender si no se ha iniciado o si se ha detenido.
Importante
Los métodos Thread.Suspend y Thread.Resume no suelen resultar útiles para las aplicaciones y
no deben confundirse con mecanismos de sincronización. Puesto que Thread.Suspend y
Thread.Resume no dependen de la cooperación del subproceso que se está controlando, son muy

MCT: Luis Dueñas Pag 34 de 336


Manual de .NET Framework 4.5

intrusivos y pueden dar como resultado problemas graves de aplicación como interbloqueos (por
ejemplo, si se suspende un subproceso que contiene un recurso que necesitará otro subproceso).
Algunas aplicaciones necesitan controlar la prioridad de los subprocesos para obtener un mayor
rendimiento. Para ello, debe utilizar la propiedad Priority en lugar de Thread.Suspend.
1.2.3. Destruir subprocesos
El método Abort se utiliza para detener un subproceso administrado de forma permanente.
Cuando llama a Abort, Common Language Runtime inicia una ThreadAbortException en el
subproceso de destino, que puede detectar el subproceso de destino.
Nota
Si un subproceso está ejecutando código no administrado cuando se llama a su método Abort, el
motor en tiempo de ejecución lo marca como ThreadState.AbortRequested. Se inicia la
excepción cuando el subproceso vuelve al código administrado.
Una vez anulado un subproceso, no se puede reiniciar.
El método Abort no causa que el subproceso se anule inmediatamente, porque el subproceso de
destino puede detectar la ThreadAbortException y ejecutar cantidades arbitrarias de código en
un bloque finally. Puede llamar a Thread.Join si necesita esperar hasta que haya finalizado el
subproceso. Thread.Join es una llamada de bloqueo que no vuelve hasta que la ejecución del
subproceso se ha detenido realmente o ha transcurrido un tiempo de espera opcional. El
subproceso anulado podría llamar al método ResetAbort o realizar un procesamiento
independiente en un bloque finally, por lo que si no especifica un tiempo de espera, no existe
garantía de que finalice dicha espera.
Otros subprocesos que llaman a Thread.Interrupt pueden interrumpir subprocesos que están
esperando una llamada al método Thread.Join.
Controlar ThreadAbortException
Si espera que se vaya a anular su subproceso, bien como resultado de llamar a Abort desde su
propio código o como resultado de descargar un dominio de aplicación en el que se está
ejecutando el subproceso (AppDomain.Unload utiliza Thread.Abort para finalizar subprocesos),
su subproceso debe controlar la ThreadAbortException y realizar cualquier procesamiento final
en una cláusula finally, como se puede ver en el código siguiente.
try
{
// Code that is executing when the thread is aborted.
}
catch (ThreadAbortException ex)
{
// Clean-up code can go here.
// If there is no Finally clause, ThreadAbortException is
// re-thrown by the system at the end of the Catch clause.
}
// Do not put clean-up code here, because the exception
// is rethrown at the end of the Finally clause.

Su código de limpieza debe estar en la cláusula catch o en la cláusula finally, porque el sistema
vuelve a iniciar una ThreadAbortException al final de la cláusula finally, o al final de la cláusula
catch si no hay ninguna cláusula finally.
Puede evitar el sistema de vuelva a iniciar la excepción llamando al método Thread.ResetAbort.
Sin embargo, sólo debería hacerlo si la ThreadAbortException fue provocada por su propio
código.
1.2.4. Planear subprocesos

MCT: Luis Dueñas Pag 35 de 336


Manual de .NET Framework 4.5

Cada subproceso tiene asignada una prioridad. Inicialmente, a los subprocesos creados en
Common Language Runtime se les asignan la prioridad ThreadPriority.Normal. Los
subprocesos creados fuera del motor en tiempo de ejecución mantienen la prioridad que tenían
antes de entrar en el entorno administrado. Con la propiedad Thread.Priority, puede obtener o
establecer la prioridad de cualquier subproceso.
La ejecución de los subprocesos se planea en función de su prioridad. Aunque los subprocesos
se ejecuten dentro del motor en tiempo de ejecución, el sistema operativo asigna espacios de
tiempo de procesador a todos los subprocesos. Los detalles del algoritmo utilizado para
determinar el orden en el que se ejecutan los subprocesos varían con cada sistema operativo. En
algunos sistemas operativos, el subproceso de mayor prioridad (o aquellos subprocesos que
pueden ejecutarse) se programa siempre para ejecutarse primero. Si hay disponibles varios
subprocesos con la misma prioridad, el programador recorre los subprocesos con dicha prioridad
y les concede a cada uno un espacio de tiempo fijo durante el que ejecutarse. Siempre que esté
disponible un subproceso de mayor prioridad para ejecutarse, no se ejecutan los subprocesos de
menor prioridad. Cuando no hay más subprocesos ejecutables con una prioridad dada, el
programador pasa a la siguiente prioridad y programa los subprocesos de dicha prioridad para
ejecutarse. Si un subproceso de prioridad superior pasa a poder ejecutarse, se adelanta al
subproceso de menor prioridad y se permite al subproceso de mayor prioridad volver a
ejecutarse una vez más. Sobre todo, el sistema operativo puede ajustar también prioridades de
subprocesos de forma dinámica cuando una interfaz de usuario de aplicación pasa a ejecutarse
de primer a segundo plano. Otros sistemas operativos podrían usar un algoritmo de
programación diferente.
1.2.5. Cancelar subprocesos de manera cooperativa
Con anterioridad a .NET Framework 4, .NET Framework no proporcionaba ningún medio
integrado para cancelar un subproceso de forma cooperativa una vez iniciado. Sin embargo, en
.NET Framework 4 se pueden utilizar tokens de cancelación para cancelar subprocesos; también
se pueden usar para cancelar objetos System.Threading.Tasks.Task o consultas PLINQ. Aunque
la clase System.Threading.Thread no proporciona compatibilidad integrada para tokens de
cancelación, se puede pasar un token a un procedimiento de subproceso utilizando el constructor
Thread que toma un delegado ParameterizedThreadStart. En el ejemplo siguiente se muestra
cómo hacerlo:
namespace CancelThreads
{
using System;
using System.Threading;

public class ServerClass


{
public static void StaticMethod(object obj)
{
CancellationToken ct = (CancellationToken)obj;
Console.WriteLine("ServerClass.StaticMethod is running another
thread.");
// Simulate work that can be canceled.
while (!ct.IsCancellationRequested)
{
Thread.SpinWait(50000);
}
Console.WriteLine("The worker thread has been canceled.Press key to
exit");
}
}

public class Simple


{
public static void Main(String[] args)
{
// The Simple class controls access to the token source.

MCT: Luis Dueñas Pag 36 de 336


Manual de .NET Framework 4.5

CancellationTokenSource cts = new CancellationTokenSource();


// Allow the UI thread to capture the token source, so that it
// can issue the cancel command.
Thread t1 = new Thread(() =>
{
if (Console.ReadKey().KeyChar == 'c')
cts.Cancel();
});
// ServerClass sees only the token, not the token source.
Thread t2 = new Thread(new ParameterizedThreadStart
(ServerClass.StaticMethod));
// Start the UI thread.
t1.Start();
// Start the worker thread and pass it the token.
t2.Start(cts.Token);
Console.ReadKey();
}
}
}

1.3. Procedimientos recomendados para el subprocesamiento


administrado
El multithreading requiere que la programación sea cuidadosa. La complejidad de muchas tareas
se puede reducir poniendo las solicitudes de ejecución en cola por subprocesos del grupo de
subprocesos. En este tema se tratan situaciones más complicadas, como coordinar el trabajo de
múltiples subprocesos, o controlar los subprocesos que se bloquean.
Nota
En .NET Framework 4, Task Parallel Library (TPL) y PLINQ proporcionan API que reducen
parte de la complejidad y los riesgos que entraña la programación multiproceso.
Interbloqueos y condiciones de carrera
El multithreading resuelve problemas de rendimiento y de capacidad de respuesta, pero al
hacerlo también crea nuevos problemas, como interbloqueos y condiciones de carrera.
Interbloqueos
Un interbloqueo tiene lugar cuando dos subprocesos intentan bloquear un recurso que ya ha
bloqueado uno de estos subprocesos. Ninguno de los subprocesos puede avanzar.
Muchos métodos de las clases del subprocesamiento administrado ofrecen tiempos de espera
que se utilizan para detectar interbloqueos. Por ejemplo, con el siguiente código se intenta
bloquear la instancia en curso. Si el bloqueo no se consigue en 300 milisegundos,
Monitor.TryEnter devuelve el valor false.
if (Monitor.TryEnter(lockObject, 300)) {
try {
// Place code protected by the Monitor here.
}
finally {
Monitor.Exit(this);
}
}
else {
// Code to execute if the attempt times out.
}

Condiciones de carrera
Una condición de carrera es un error que se produce cuando el resultado de un programa
depende del primero de dos o más subprocesos que consiga llegar hasta un bloque específico de
código. Ejecutar el programa muchas veces genera distintos resultados y no es posible predecir
el resultado de una ejecución específica.

MCT: Luis Dueñas Pag 37 de 336


Manual de .NET Framework 4.5

Un ejemplo sencillo de una condición de carrera es el incremento de un campo. Suponga una


clase que tiene un campo privado static (Shared en Visual Basic) que se incrementa cada vez
que se crea una instancia de la clase, mediante código como objCt++; (C#) o objCt += 1 (Visual
Basic). Esta operación requiere cargar el valor de objCt en un registro, incrementar el valor y
almacenarlo en objCt.
En una aplicación multiproceso, un subproceso que realiza los tres pasos puede adelantar al
subproceso que ha cargado e incrementado el valor; cuando el primer subproceso reanuda la
ejecución y almacena su valor, sobrescribe objCt sin tener en cuenta el hecho de que el valor ha
cambiado mientras tanto.
Para evitar fácilmente esta condición de carrera determinada, utilice los métodos de la clase
Interlocked, como Interlocked.Increment.
También se pueden producir condiciones de carrera al sincronizar las actividades de varios
subprocesos. Siempre que escriba una línea de código, debe tener en cuenta qué puede ocurrir si
otro subproceso adelanta a un subproceso antes de ejecutar la línea (o antes de cualquiera de las
instrucciones máquina que forman la línea).
Número de procesadores
La mayoría de los equipos tienen ahora varios procesadores (también llamados núcleos), incluso
los pequeños dispositivos como tabletas y teléfonos. Si está desarrollando software que también
se ejecutará en equipos con un solo procesador, tenga en cuenta que el multithreading resuelve
distintos problemas en equipos con varios procesadores y con un solo procesador.
Equipos multiprocesador
El multithreading proporciona un mayor rendimiento. Diez procesadores pueden hacer diez
veces el trabajo de uno, pero sólo si el trabajo se divide de forma que los diez trabajen al mismo
tiempo; los subprocesos proporcionan una forma fácil de dividir el trabajo y de aprovechar la
eficacia de procesamiento adicional. Si utiliza el multithreading en un equipo multiprocesador:
 El número de subprocesos que se pueden ejecutar de forma simultánea está limitado por
el número de procesadores.
 Sólo se ejecuta un subproceso en segundo plano si el número de subprocesos que se
ejecutan en primer plano es menor que el número de procesadores.
 Cuando se llama al método Thread.Start en un subproceso, ese subproceso se puede
ejecutar o no inmediatamente dependiendo del número de procesadores y de
subprocesos en espera de ejecución.
 Las condiciones de carrera se pueden producir no sólo debido a las prioridades
imprevistas de subprocesos, sino también porque dos subprocesos que se ejecutan en
procesadores diferentes pueden competir para alcanzar el mismo bloque de código.
Equipos de un solo procesador
El multithreading ofrece una gran capacidad de respuesta al usuario del equipo, y utiliza el
tiempo de inactividad para realizar tareas en segundo plano. Si utiliza el multithreading en un
equipo de un solo procesador:
 Sólo se ejecuta un subproceso cada vez.
 Se ejecuta un subproceso en segundo plano sólo cuando el subproceso principal del
usuario está inactivo. Un subproceso en primer plano que se ejecuta continuamente
agota el tiempo de procesador de los subprocesos en segundo plano.
 Cuando se llama al método Thread.Start en un subproceso, ese subproceso no se ejecuta
hasta que el subproceso en curso le cede la ejecución o es relegado por el sistema
operativo.
 Generalmente, las condiciones de carrera se producen debido a que el programador no
tuvo en cuenta el hecho de que un subproceso puede ser adelantado por otro en un

MCT: Luis Dueñas Pag 38 de 336


Manual de .NET Framework 4.5

momento difícil permitiendo, algunas veces, que otro subproceso llegue el primero al
bloque de código.
Miembros estáticos y constructores estáticos
No se inicializa una clase hasta que su constructor de clase (constructorstatic en C#, Shared Sub
New en Visual Basic) haya terminado de ejecutarse. Para evitar la ejecución de código en un
tipo no inicializado, Common Language Runtime bloquea todas las llamadas de otros
subprocesos a los miembros static de la clase (miembrosShared en Visual Basic) hasta que el
constructor de clase termina de ejecutarse.
Por ejemplo, si un constructor de clase inicia un nuevo subproceso, y el procedimiento del
subproceso llama a un miembro static de la clase, el nuevo subproceso se bloquea hasta que el
constructor de clase finalice.
Esto se aplica a cualquier tipo que pueda tener un constructor static.
Recomendaciones generales
Tenga en cuenta las siguientes instrucciones cuando utilice varios subprocesos:
 No utilice Thread.Abort para finalizar otros subprocesos. Una llamada a Abort en otro
subproceso es similar a iniciar una excepción en ese subproceso, sin conocer qué punto
ha alcanzado en su procesamiento.
 No utilice Thread.Suspend ni Thread.Resume para sincronizar las actividades de varios
subprocesos. Utilice Mutex, ManualResetEvent, AutoResetEvent y Monitor.
 No controle la ejecución de subprocesos de trabajo desde el programa principal (con
eventos, por ejemplo). En su lugar, diseñe un programa de forma que los subprocesos de
trabajo sean los que tengan que esperar hasta que haya trabajo disponible, lo ejecuten y
notifiquen su finalización a otras partes del programa. Si los subprocesos de trabajo no
se bloquean, puede ser conveniente usar subprocesos del grupo de subprocesos.
Monitor.PulseAll resulta útil en aquellas situaciones en las que los subprocesos de
trabajo se bloquean.
 No utilice los tipos como objetos de bloqueo. Es decir, evite código como
lock(typeof(X)) en C# o SyncLock(GetType(X)) en Visual Basic o el uso de
Monitor.Enter con objetos Type. Para un tipo determinado, hay sólo una instancia de
System.Type por el dominio de aplicación. Si el tipo que quiere bloquear es público,
codifique uno que su tipo no pueda bloquear, para evitar interbloqueos.
 Tenga cuidado al efectuar bloqueos en instancias, por ejemplo lock(this) en C# o
SyncLock(Me) en Visual Basic. Si otra parte del código de la aplicación, ajeno al tipo,
bloquea el objeto, podrían producirse interbloqueos.
 Asegúrese de que un subproceso que entra en un monitor siempre sale de ese monitor,
aun en el caso de que se produzca una excepción mientras el subproceso se encuentra en
el monitor. La instrucción lock de C# y la instrucción SyncLock de Visual Basic
ofrecen automáticamente este comportamiento mediante un bloque finally que garantiza
la llamada a Monitor.Exit. Si no está seguro de que se llamará a Exit, considere la
posibilidad de cambiar el diseño con el fin de utilizar Mutex. Una zona de exclusión
mutua se libera automáticamente cuando finaliza el subproceso al que pertenece.
 Utilice varios subprocesos para tareas que requieren recursos diferentes, y evite asignar
varios subprocesos a un solo recurso. Por ejemplo, en tareas que impliquen beneficios
de E/S por tener un subproceso propio, ya que ese subproceso se bloquea durante las
operaciones de E/S y, de este modo, permite ejecutar otros subprocesos. Los datos
proporcionados por el usuario son otro recurso que se beneficia de la utilización de un
subproceso dedicado. En un equipo de un solo procesador, una tarea que implica un
cálculo intensivo coexiste con los datos proporcionados por el usuario y con tareas que
implican la E/S, pero varias tareas de cálculo intensivo compiten entre ellas.
 Considere la posibilidad de utilizar métodos de la clase Interlocked para los cambios de
estado simples, en lugar de utilizar la instrucción lock (SyncLock en Visual Basic). La

MCT: Luis Dueñas Pag 39 de 336


Manual de .NET Framework 4.5

instrucción lock es una buena herramienta de uso general, pero la clase Interlocked
genera mejor rendimiento para las actualizaciones que deben ser atómicas.
Internamente, ejecuta un solo prefijo de bloqueo si no hay contención. En las revisiones
de código, inspeccione código similar al que se muestra en los ejemplos siguientes. En
el primer ejemplo, se incrementa una variable de estado:
lock(lockObject)
{
myField++;
}

Para mejorar el rendimiento, utilice el método Increment en lugar de la instrucción lock,


de la siguiente manera:
System.Threading.Interlocked.Increment(myField);
Nota
En la versión 2.0 de .NET Framework, el método Add proporciona actualizaciones
atómicas en incrementos mayores que 1.
En el segundo ejemplo, se actualiza una variable de tipo de referencia sólo si es una
referencia nula (Nothing en Visual Basic).
if (x == null)
{
lock (lockObject)
{
if (x == null)
{
x = y;
}
}
}

Se puede mejorar el rendimiento utilizando el método CompareExchange de la siguiente


manera:
System.Threading.Interlocked.CompareExchange(ref x, y, null);
Nota
En la versión 2.0 de .NET Framework, el método CompareExchange tiene una
sobrecarga genérica que se puede utilizar para el reemplazo con seguridad de tipos de
cualquier tipo de referencia.
Recomendaciones para las bibliotecas de clases
Tenga en cuenta las instrucciones siguientes cuando diseñe bibliotecas de clases para el
multithreading:
 Evite la necesidad de sincronización si es posible. Esto se aplica especialmente en el
caso de código muy utilizado. Por ejemplo, se podría ajustar un algoritmo de modo que
tolere una condición de carrera en lugar de eliminarla. La sincronización innecesaria
disminuye el rendimiento y crea la posibilidad de interbloqueos y condiciones de
carrera.
 Procure que los datos estáticos (Shared en Visual Basic) sean seguros para subprocesos
de manera predeterminada.
 No convierta los datos de instancia en datos seguros para subprocesos de manera
predeterminada. Al agregar bloqueos para crear código seguro para subprocesos, se
reduce el rendimiento, se incrementa la contención de bloqueos y se crea la posibilidad
de que se produzcan interbloqueos. En los modelos de aplicación comunes, sólo un
subproceso a la vez ejecuta código de usuario, lo que minimiza la necesidad de la
seguridad para subprocesos. Por esta razón, las bibliotecas de clases de .NET
Framework no son seguras para subprocesos de forma predeterminada.

MCT: Luis Dueñas Pag 40 de 336


Manual de .NET Framework 4.5

 Evite proporcionar métodos estáticos que alteren el estado estático. En escenarios de


servidor comunes, el estado estático se comparte entre las solicitudes, lo que significa
que varios subprocesos pueden ejecutar a la vez ese código. De este modo, se abre la
posibilidad de errores de subprocesos. Considere la posibilidad de utilizar un modelo de
diseño que encapsule los datos en instancias no compartidas por las solicitudes.
Además, si se sincronizan los datos estáticos, las llamadas entre los métodos estáticos
que modifican el estado pueden generar interbloqueos o sincronización redundante, lo
que afecta negativamente al rendimiento.
1.4. Objetos y características de subprocesos
.NET Framework proporciona una serie de objetos que sirven para crear y administrar
aplicaciones multiproceso. La clase Thread representa los subprocesos administrados. La clase
ThreadPool facilita creación y administración de tareas de multiproceso en segundo plano. La
clase BackgroundWorker hace lo mismo para las tareas que interactúan con la interfaz de
usuario. La clase Timer ejecuta las tareas en segundo plano en intervalos sincronizados.
Además, hay una serie de clases que sincronizan las actividades de los subprocesos, entre las
que se incluyen las clases Semaphore y EventWaitHandle de .NET Framework versión 2.0.
1.4.1. Grupo de subprocesos administrados
La clase ThreadPool proporciona a la aplicación un grupo de subprocesos de trabajo
administrados por el sistema, que le permite concentrarse en tareas de aplicación en lugar de en
la administración de los subprocesos. Si tiene tareas cortas que requieren el procesamiento en
segundo plano, el grupo de subprocesos administrado es una manera fácil de aprovechar los
diversos subprocesos. Por ejemplo, a partir de .NET Framework 4 puede crear objetos Task y
Task<TResult>, que realizan tareas asincrónicas en subprocesos del grupo de subprocesos.
Nota
A partir de .NET Framework 2.0 Service Pack 1, el rendimiento del grupo de subprocesos ha
mejorado significativamente en tres áreas clave que se identificaron como cuellos de botella en
versiones anteriores de .NET Framework: poner tareas en la cola, enviar subprocesos del grupo
de subprocesos y enviar subprocesos de finalización de E/S. Para usar esta funcionalidad, el
destino de la aplicación debe ser .NET Framework 3.5 o posterior.
Para las tareas en segundo plano que interactúan con la interfaz de usuario, .NET Framework
versión 2.0 también proporciona la clase BackgroundWorker, que se comunica mediante
eventos iniciados en el subproceso de la interfaz de usuario.
.NET Framework utiliza subprocesos ThreadPool para muchos fines, incluida la finalización de
E/S asincrónica, devolución de llamadas a temporizadores, operaciones de espera registradas,
llamadas de método asincrónico utilizando delegados y conexiones de socket System.Net.
Cuándo no utilizar los subprocesos ThreadPool
Hay varios escenarios en los que es adecuado crear y administrar sus propios subprocesos en
lugar de utilizar subprocesos ThreadPool:

 Requiere tener un subproceso en primer plano.


 Requiere que un subproceso tenga una prioridad determinada.
 Hay tareas que hacen que el subproceso se bloquee durante los períodos de tiempo
prolongados. El grupo de subprocesos tiene un número máximo de subprocesos, por lo
que un número grande de subprocesos ThreadPool bloqueados podría impedir que se
iniciaran las tareas.
 Es necesario colocar los subprocesos en un contenedor uniproceso. Todos los
subprocesos ThreadPool están en el apartamento multiproceso.

MCT: Luis Dueñas Pag 41 de 336


Manual de .NET Framework 4.5

 Necesita tener una identidad estable asociada al subproceso o dedicar un subproceso a


una tarea.

Características de los grupos de subprocesos


Los subprocesos ThreadPool son subprocesos de fondo. Cada subproceso utiliza el tamaño de
pila predeterminado, se ejecuta con la prioridad predeterminada y está en el apartamento
multiproceso.
Sólo hay un grupo de subprocesos por cada proceso.
Excepciones en los subprocesos ThreadPool
Las excepciones no controladas producidas en subprocesos ThreadPool finalizan el proceso.
Hay tres excepciones a esta regla:
 En un subproceso ThreadPool se produce una excepción ThreadAbortException porque
se llamó a Abort.
 Se produce una excepción AppDomainUnloadedException en un subproceso
ThreadPool, porque se descarga el dominio de aplicación.
 Common Language Runtime o un proceso de host finaliza el subproceso.
Nota
En las versiones 1.0 y 1.1 de .NET Framework, Common Language Runtime intercepta
silenciosamente las excepciones no controladas en subprocesos ThreadPool. Esto podría dañar
el estado de la aplicación y hacer que en el futuro las aplicaciones no respondan, lo que podría
ser muy difícil de depurar.
Número máximo de subprocesos ThreadPool
El número de operaciones que pueden situarse en cola del grupo de subprocesos sólo está
limitado por la memoria disponible; sin embargo, el grupo de subprocesos limita el número de
subprocesos que pueden estar activos simultáneamente en el mismo proceso. A partir de .NET
Framework 4, el tamaño predeterminado del grupo de subprocesos de un proceso depende de
varios factores, como el tamaño del espacio de direcciones virtuales. Un proceso puede llamar al
método GetMaxThreads para determinar el número de subprocesos.
Puede controlar el número máximo de subprocesos utilizando los métodos GetMaxThreads y
SetMaxThreads.
Nota
En las versiones 1.0 y 1.1 de .NET Framework, el tamaño del grupo de subprocesos no se puede
establecer desde código administrado. El código que hospeda Common Language Runtime
puede establecer el tamaño mediante CorSetMaxThreads, definido en mscoree.h.
Valores mínimos del grupo de subprocesos
El grupo de subprocesos proporciona nuevos subprocesos de trabajo o subprocesos de
finalización de E/S a petición hasta que alcanza un mínimo especificado para cada categoría.
Puede usar el método GetMinThreads para obtener estos valores mínimos.
Nota
Cuando la petición es baja, el número real de subprocesos del grupo de subprocesos puede
situarse por debajo de los valores mínimos.
Cuando se alcanza un valor mínimo, el grupo de subprocesos puede crear subprocesos
adicionales o esperar hasta que se completan algunas tareas. A partir de .NET Framework 4, el
grupo de subprocesos crea y destruye los subprocesos de trabajo para optimizar el rendimiento,
que se define como el número de tareas completadas por unidad de tiempo. Es posible que si

MCT: Luis Dueñas Pag 42 de 336


Manual de .NET Framework 4.5

hay muy pocos subprocesos, no se haga un uso óptimo de los recursos disponibles, mientras que
demasiados subprocesos podrían aumentar la contención de recursos.
Precaución
Puede usar el método SetMinThreads para aumentar el número mínimo de subprocesos
inactivos. Sin embargo, si estos valores se incrementan innecesariamente, pueden producirse
problemas de rendimiento. Si se inician demasiadas tareas al mismo tiempo, puede parecer que
todas se ejecutan con lentitud. En la mayoría de los casos, el grupo de subprocesos funcionará
mejor con su propio algoritmo de asignación de subprocesos.
Omitir las comprobaciones de seguridad
El grupo de subprocesos también proporciona los métodos
ThreadPool.UnsafeQueueUserWorkItem y ThreadPool.UnsafeRegisterWaitForSingleObject.
Utilice estos métodos únicamente si está seguro de que pila del llamador no es pertinente para
las comprobaciones de seguridad realizadas durante la ejecución de la tarea en cola.
QueueUserWorkItem y RegisterWaitForSingleObject capturan la pila del llamador, que se
combina en la pila del subproceso del grupo de subprocesos cuando el subproceso empieza a
ejecutar una tarea. Si es necesario realizar una comprobación de seguridad, debe comprobarse
toda la pila. Aunque la comprobación proporciona seguridad, es a costa del rendimiento.
Utilizar el grupo de subprocesos
A partir de .NET Framework 4, el mecanismo más sencillo para usar el grupo de subprocesos
consiste en usar Biblioteca de procesamiento paralelo basado en tareas (TPL). De forma
predeterminada, los tipos bibliotecas paralelas, como Task y Task<TResult>, usan los
subprocesos del grupo de subprocesos para ejecutar tareas. También puede usar el grupo de
subprocesos llamando a ThreadPool. QueueUserWorkItem desde el código administrado (o a
CorQueueUserWorkItem desde el código no administrado) y pasando un delegado
WaitCallback que representa el método que realiza la tarea. Otra forma de usar el grupo de
subprocesos consiste en poner en cola los elementos de trabajo que están relacionados con una
operación de espera usando el método ThreadPool.RegisterWait ForSingleObject y pasando
WaitHandle que, cuando se señala o agota el tiempo de espera, llama al método representado
por el delegado WaitOrTimerCallback. Los subprocesos del grupo de subprocesos se usan para
invocar métodos de devolución de llamada.
Ejemplos de ThreadPool
En los ejemplos de código de esta sección se muestra el grupo de subprocesos usando la clase
Task, el método ThreadPool.QueueUserWorkItem y el método
ThreadPool.RegisterWaitForSingleObject.
 Ejecutar tareas asincrónicas con la biblioteca TPL
 Ejecutar código asincrónicamente con QueueUserWorkItem
 Proporcionar datos de tareas para QueueUserWorkItem
 Usar RegisterWaitForSingleObject
Ejecutar tareas asincrónicas con la biblioteca TPL
En el ejemplo siguiente se muestra cómo se crea y se usa un objeto Task llamando al método
TaskFactory.StartNew.
using System;
using System.Threading;
using System.Threading.Tasks;

class StartNewDemo
{
// Demonstrated features:
// Task ctor()
// Task.Factory

MCT: Luis Dueñas Pag 43 de 336


Manual de .NET Framework 4.5

// Task.Wait()
// Task.RunSynchronously()
// Expected results:
// Task t1 (alpha) is created unstarted.
// Task t2 (beta) is created started.
// Task t1's (alpha) start is held until after t2 (beta) is
started.
// Both tasks t1 (alpha) and t2 (beta) are potentially executed
on
// threads other than the main thread on multi-core machines.
// Task t3 (gamma) is executed synchronously on the main thread.
// Documentation:
// http://msdn.microsoft.com/en-
us/library/system.threading.tasks.task_members(VS.100).aspx
static void Main()
{
Action<object> action = (object obj) =>
{
Console.WriteLine("Task={0}, obj={1}, Thread={2}", Task.CurrentId,
obj.ToString(), Thread.CurrentThread.ManagedThreadId);
};
// Construct an unstarted task
Task t1 = new Task(action, "alpha");
// Cosntruct a started task
Task t2 = Task.Factory.StartNew(action, "beta");
// Block the main thread to demonstate that t2 is executing
t2.Wait();
// Launch t1
t1.Start();
Console.WriteLine("t1 has been launched. (Main Thread={0})",
Thread.CurrentThread.ManagedThreadId);
// Wait for the task to finish.
// You may optionally provide a timeout interval or a cancellation
token
// to mitigate situations when the task takes too long to finish.
t1.Wait();
// Construct an unstarted task
Task t3 = new Task(action, "gamma");
// Run it synchronously
t3.RunSynchronously();
// Although the task was run synchrounously, it is a good practice to
wait
// for it which observes for exceptions potentially thrown by that
task.
t3.Wait();
}
}

Ejecutar código asincrónicamente con QueueUserWorkItem


En el ejemplo siguiente se pone en cola una tarea muy sencilla, representada por el método
ThreadProc, que usa el método QueueUserWorkItem.
using System;
using System.Threading;

public class Example


{
public static void Main()
{
// Queue the task.
ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc));
Console.WriteLine("Main thread does some work, then sleeps.");
// If you comment out the Sleep, the main thread exits before
// the thread pool task runs. The thread pool uses background
// threads, which do not keep the application running. (This
// is a simple example of a race condition.)
Thread.Sleep(1000);
Console.WriteLine("Main thread exits.");

MCT: Luis Dueñas Pag 44 de 336


Manual de .NET Framework 4.5

}
// This thread procedure performs the task.
static void ThreadProc(Object stateInfo)
{
// No state object was passed to QueueUserWorkItem, so
// stateInfo is null.
Console.WriteLine("Hello from the thread pool.");
}
}

Proporcionar datos de tareas para QueueUserWorkItem


En el siguiente ejemplo de código se utiliza el método QueueUserWorkItem para poner en cola
una tarea y proporcionar los datos de esa tarea.
using System;
using System.Threading;

// TaskInfo holds state information for a task that will be


// executed by a ThreadPool thread.
public class TaskInfo
{
// State information for the task. These members
// can be implemented as read-only properties, read/write
// properties with validation, and so on, as required.
public string Boilerplate;
public int Value;

// Public constructor provides an easy way to supply all


// the information needed for the task.
public TaskInfo(string text, int number)
{
Boilerplate = text;
Value = number;
}
}

public class Example


{
public static void Main()
{
// Create an object containing the information needed for the task.
TaskInfo ti = new TaskInfo("This report displays the number {0}.",
42);
// Queue the task and data.
if (ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc), ti))
{
Console.WriteLine("Main thread does some work, then sleeps.");
// If you comment out the Sleep, the main thread exits before
// the ThreadPool task has a chance to run. ThreadPool uses
// background threads, which do not keep the application
// running. (This is a simple example of a race condition.)
Thread.Sleep(1000);
Console.WriteLine("Main thread exits.");
}
else
{
Console.WriteLine("Unable to queue ThreadPool request.");
}
}

// The thread procedure performs the independent task, in this case


// formatting and printing a very simple report.
static void ThreadProc(Object stateInfo)
{
TaskInfo ti = (TaskInfo) stateInfo;
Console.WriteLine(ti.Boilerplate, ti.Value);
}
}

MCT: Luis Dueñas Pag 45 de 336


Manual de .NET Framework 4.5

Usar RegisterWaitForSingleObject
En el ejemplo siguiente se muestran diversas características del subprocesamiento.
 Poner en cola una tarea para su ejecución por medio de subprocesos ThreadPool, con el
método RegisterWaitForSingleObject.
 Señalar una tarea para su ejecución, con AutoResetEvent.
 Controlar los tiempos de espera y las señales con un delegado WaitOrTimerCallback.
 Cancelar una tarea de la cola con RegisteredWaitHandle.
using System;
using System.Threading;

// TaskInfo contains data that will be passed to the callback method.


public class TaskInfo
{
public RegisteredWaitHandle Handle = null;
public string OtherInfo = "default";
}

public class Example


{
public static void Main(string[] args)
{
// The main thread uses AutoResetEvent to signal the
// registered wait handle, which executes the callback
// method.
AutoResetEvent ev = new AutoResetEvent(false);
TaskInfo ti = new TaskInfo();
ti.OtherInfo = "First task";
// The TaskInfo for the task includes the registered wait
// handle returned by RegisterWaitForSingleObject. This
// allows the wait to be terminated when the object has
// been signaled once (see WaitProc).
ti.Handle = ThreadPool.RegisterWaitForSingleObject(ev,
new WaitOrTimerCallback(WaitProc),ti,1000,false);
// The main thread waits three seconds, to demonstrate the
// time-outs on the queued thread, and then signals.
Thread.Sleep(3100);
Console.WriteLine("Main thread signals.");
ev.Set();
// The main thread sleeps, which should give the callback
// method time to execute. If you comment out this line, the
// program usually ends before the ThreadPool thread can execute.
Thread.Sleep(1000);
// If you start a thread yourself, you can wait for it to end
// by calling Thread.Join. This option is not available with
// thread pool threads.
}

// The callback method executes when the registered wait times out,
// or when the WaitHandle (in this case AutoResetEvent) is signaled.
// WaitProc unregisters the WaitHandle the first time the event is
signaled.
public static void WaitProc(object state, bool timedOut)
{
// The state object must be cast to the correct type, because the
// signature of the WaitOrTimerCallback delegate specifies type
Object.
TaskInfo ti = (TaskInfo) state;
string cause = "TIMED OUT";
if (!timedOut)
{
cause = "SIGNALED";
// If the callback method executes because the WaitHandle is
// signaled, stop future execution of the callback method
// by unregistering the WaitHandle.
if (ti.Handle != null) ti.Handle.Unregister(null);

MCT: Luis Dueñas Pag 46 de 336


Manual de .NET Framework 4.5

}
Console.WriteLine("WaitProc( {0} ) executes on thread {1}; cause =
{2}.",
ti.OtherInfo,Thread.CurrentThread.GetHashCode().ToString(),cause);
}
}

1.4.2. Temporizadores
Los temporizadores son objetos pequeños que permiten especificar un delegado para llamarlo en
un momento específico. Un subproceso del grupo realiza la operación de espera.
El uso de la clase Timer es sencillo. Para crear un Timer, debe pasar un delegado
TimerCallback al método de devolución de llamada, un objeto que representa el estado que se
pasará a la devolución de llamada, la hora de compilación inicial y una cifra que representa el
período entre invocaciones de devolución de llamada. Para cancelar un temporizador pendiente,
se debe llamar a la función Timer.Dispose.
Nota
Hay otras dos clases de temporizador. La clase System.Windows.Forms.Timer es un control que
funciona con diseñadores visuales y está pensado para su uso en contextos de interfaz de
usuario; provoca eventos en el subproceso de interfaz de usuario. La clase System.Timers.Timer
deriva de Component, por lo que puede usarse con diseñadores visuales; también provoca
eventos, pero lo hace en un subproceso ThreadPool. La clase System.Threading.Timer realiza
las devoluciones de llamada en un subproceso ThreadPool y no utiliza el modelo de evento para
nada. También proporciona un objeto de estado al método de devolución de llamada, lo que no
hacen otros temporizadores. Es sumamente ligero.
El ejemplo de código siguiente inicia un temporizador que se inicia después de un segundo
(1000 milisegundos) y emite un chasquido por segundo hasta que presione la tecla Entrar. La
variable que contiene la referencia al temporizador es un campo de nivel de clase para garantizar
que el temporizador no está sujeto a la recolección de elementos no utilizados mientras que
todavía está en ejecución.
using System;
using System.Threading;

public class Example


{
private static Timer ticker;

public static void TimerMethod(object state)


{
Console.Write(".");
}

public static void Main()


{
ticker = new Timer(TimerMethod, null, 1000, 1000);
Console.WriteLine("Press the Enter key to end the program.");
Console.ReadLine();
}
}

1.4.3. Monitores
Los objetos Monitor exponen la capacidad de sincronizar el acceso a una región de código
tomando y liberando un bloqueo de un objeto concreto mediante los métodos Monitor.Enter,
Monitor.TryEnter y Monitor.Exit. Una vez que se tenga un bloqueo en una región del código, se
podrán usar los métodos Monitor.Wait, Monitor.Pulse y Monitor.PulseAll. Wait libera el
bloqueo si este se mantiene y espera recibir una notificación. Cuando el método Wait recibe la
notificación, devuelve y obtiene de nuevo el bloqueo. Ambos métodos, Pulse y PulseAll avisan
al siguiente subproceso de la cola de espera de que puede continuar.

MCT: Luis Dueñas Pag 47 de 336


Manual de .NET Framework 4.5

Las instrucciones SyncLock de Visual Basic y lock de C# utilizan Monitor.Enter para realizar el
bloqueo y Monitor.Exit para liberarlo. La ventaja de utilizar las instrucciones de un lenguaje es
que todo lo que esté incluido en el bloqueo lock o SyncLock está incluido en una instrucción
Try. La instrucción Try tiene un bloque Finally para garantizar que se libera el bloqueo.
Monitor bloquea objetos (es decir, tipos de referencia), no tipos de valor. Aunque puede pasar
un tipo de valor a Enter y Exit, la conversión boxing se aplica por separado en cada llamada.
Como cada llamada crea un objeto independiente, Enter nunca se bloquea, y el código que
supuestamente está protegiendo no está en realidad sincronizado. Además, el objeto que se pasa
a Exit es distinto del objeto que se pasa a Enter, por tanto, Monitor inicia una excepción
SynchronizationLockException con el mensaje "El método de sincronización del objeto se ha
llamado desde un bloque de códigos sin sincronizar". En el siguiente ejemplo se ilustran estos
problemas.
try
{
int x = 1;
// The call to Enter() creates a generic synchronizing object for the
value
// of x each time the code is executed, so that Enter never blocks.
Monitor.Enter(x);
try
{
// Code that needs to be protected by the monitor.
}
finally
{
// Always use Finally to ensure that you exit the Monitor.
// The call to Exit() will FAIL!!!
// The synchronizing object created for x in Exit() will be different
// than the object used in Enter(). SynchronizationLockException
// will be thrown.
Monitor.Exit(x);
}
}
catch (SynchronizationLockException SyncEx)
{
Console.WriteLine("A SynchronizationLockException occurred. Message:");
Console.WriteLine(SyncEx.Message);
}

Aunque se puede aplicar una conversión boxing a una variable de tipo de valor antes de llamar a
Enter y a Exit, tal y como se muestra en el ejemplo siguiente, y pasar el mismo objeto al que se
le ha aplicado la conversión boxing a los dos métodos, hacerlo no reporta ningún beneficio. Los
cambios en la variable no se reflejan en la copia a la que se aplica la conversión boxing y no hay
ninguna forma de cambiar el valor de esta copia.
int x = 1;
object o = x;

Monitor.Enter(o);
try
{
// Code that needs to be protected by the monitor.
}
finally
{
// Always use Finally to ensure that you exit the Monitor.
Monitor.Exit(o);
}

Es importante tener en cuenta la distinción entre el uso de objetos Monitor y WaitHandle. Los
objetos Monitor están estrictamente administrados, son totalmente portátiles y podrían ser más
eficaces en lo que respecta a los requisitos de recursos del sistema operativo. Los objetos
WaitHandle representan objetos de espera del sistema operativo, son útiles para la

MCT: Luis Dueñas Pag 48 de 336


Manual de .NET Framework 4.5

sincronización entre código administrado y no administrado, y exponen algunas características


avanzadas del sistema operativo, como la capacidad de esperar varios objetos a la vez.
En el ejemplo de código siguiente se muestra el uso combinado de la clase Monitor
(implementada con las instrucciones del compilador lock y SyncLock ), la clase Interlocked y la
clase AutoResetEvent.
using System;
using System.Threading;

// Note: The class whose internal public member is the synchronizing


// method is not public; none of the client code takes a lock on the
// Resource object.The member of the nonpublic class takes the lock on
// itself. Written this way, malicious code cannot take a lock on a public
object.
class SyncResource
{
public void Access(Int32 threadNum)
{
// Uses Monitor class to enforce synchronization.
lock (this)
{
// Synchronized: Despite the next conditional, each thread
// waits on its predecessor.
if (threadNum % 2 == 0)
{
Thread.Sleep(2000);
}
Console.WriteLine("Start Synched Resource access (Thread={0})",
threadNum);
Thread.Sleep(200);
Console.WriteLine("Stop Synched Resource access (Thread={0})",
threadNum);
}
}
}

// Without the lock, the method is called in the order in which threads reach
it.
class UnSyncResource
{
public void Access(Int32 threadNum)
{
// Does not use Monitor class to enforce synchronization.
// The next call throws the thread order.
if (threadNum % 2 == 0)
{
Thread.Sleep(2000);
}
Console.WriteLine("Start UnSynched Resource access (Thread={0})",
threadNum);
Thread.Sleep(200);
Console.WriteLine("Stop UnSynched Resource access (Thread={0})",
threadNum);
}
}

public class App


{
static Int32 numAsyncOps = 5;
static AutoResetEvent asyncOpsAreDone = new AutoResetEvent(false);
static SyncResource SyncRes = new SyncResource();
static UnSyncResource UnSyncRes = new UnSyncResource();

public static void Main()


{
for (Int32 threadNum = 0; threadNum < 5; threadNum++)
{

MCT: Luis Dueñas Pag 49 de 336


Manual de .NET Framework 4.5

ThreadPool.QueueUserWorkItem(new WaitCallback(SyncUpdateResource),
threadNum);
}
// Wait until this WaitHandle is signaled.
asyncOpsAreDone.WaitOne();
Console.WriteLine("\t\nAll synchronized operations have
completed.\t\n");
// Reset the thread count for unsynchronized calls.
numAsyncOps = 5;
for (Int32 threadNum = 0; threadNum < 5; threadNum++)
{
ThreadPool.QueueUserWorkItem(new
WaitCallback(UnSyncUpdateResource),
threadNum);
}
// Wait until this WaitHandle is signaled.
asyncOpsAreDone.WaitOne();
Console.WriteLine("\t\nAll unsynchronized thread operations have
completed.");
}

// The callback method's signature MUST match that of a System.Threading.


// TimerCallback delegate (it takes an Object parameter and returns void).
static void SyncUpdateResource(Object state)
{
// This calls the internal synchronized method, passing a thread
number.
SyncRes.Access((Int32) state);
// Count down the number of methods that the threads have called.
// This must be synchronized, however; you cannot know which thread
// will access the value **before** another thread's incremented
// value has been stored into the variable.
if (Interlocked.Decrement(ref numAsyncOps) == 0)
{
// Announce to Main that in fact all thread calls are done.
asyncOpsAreDone.Set();
}
}

// The callback method's signature MUST match that of a


// System.Threading.TimerCallback delegate (it takes an Object
// parameter and returns void).
static void UnSyncUpdateResource(Object state)
{
// This calls the unsynchronized method, passing a thread number.
UnSyncRes.Access((Int32) state);
// Count down the number of methods that the threads have called.
// This must be synchronized, however; you cannot know which thread
// will access the value **before** another thread's incremented
// value has been stored into the variable.
if (Interlocked.Decrement(ref numAsyncOps) == 0)
{
// Announce to Main that in fact all thread calls are done.
asyncOpsAreDone.Set();
}
}
}

1.4.4. Controladores de espera


La clase WaitHandle encapsula identificadores de sincronización de Win32 y se utiliza para
representar todos los objetos de sincronización en el motor en tiempo de ejecución que permiten
varias operaciones de espera.
La propia clase WaitHandle es abstracta. Además de las clases derivadas, tiene varios métodos
estáticos que habilitan la espera de varios eventos. Las clases derivadas de WaitHandle incluyen
lo siguiente:

MCT: Luis Dueñas Pag 50 de 336


Manual de .NET Framework 4.5

 Clase Mutex Vea Exclusiones mutuas (mutex).


 La clase EventWaitHandle y sus clases derivadas, AutoResetEvent y
ManualResetEvent. La clase EventWaitHandle es nueva en la versión 2.0 de .NET
Framework. Vea EventWaitHandle, AutoResetEvent, CountdownEvent,
ManualResetEvent.
 La clase Semaphore es nueva en la versión 2.0 de .NET Framework.
Dado que la clase WaitHandle deriva de MarshalByRefObject, estas clases se pueden utilizar
para sincronizar las actividades de subprocesos que salen de los límites del dominio de
aplicación.
Los subprocesos pueden bloquear en un identificador de espera individual llamando al método
de instancia WaitOne. Además, la clase WaitHandle tiene métodos estáticos sobrecargados para
esperar hasta que se hayan señalado todos los controladores de espera del conjunto especificado
(WaitAll) o hasta que se haya señalado cualquiera de los identificadores de espera de un
conjunto especificado (WaitAny). Las sobrecargas de estos métodos proporcionan intervalos de
espera para salir del período de espera y la oportunidad de salir de un contexto de sincronización
antes de entrar en la espera, permitiendo que otros subprocesos utilicen el contexto de
sincronización.
En la versión 2.0 de .NET Framework, los indicadores de espera también tienen el método
estático SignalAndWait, que permite que un subproceso señale un identificador de espera y
espere inmediatamente a otro.
Las clases derivadas de WaitHandle difieren en su afinidad de subprocesos. Los identificadores
de espera de eventos (EventWaitHandle, AutoResetEvent y ManualResetEvent) y los semáforos
no tienen afinidad de subprocesos. Cualquier subproceso puede señalar un identificador de
espera de evento o un semáforo. Las exclusiones mutuas, por otro lado, sí tienen afinidad de
subprocesos. El subproceso propietario de una exclusión mutua debe liberarlo; se produce una
excepción si un subproceso llama al método ReleaseMutex en una expresión mutua que no le
pertenece.
1.4.5. EventWaitHandle, AutoResetEvent, CountdownEvent,
ManualReset Event
Los identificadores de espera de evento permiten a los subprocesos sincronizar las actividades
señalizándose entre sí y esperando señales de los demás. Estos eventos de sincronización se
basan en identificadores de espera Win32 y se pueden dividir en dos tipos: los que se
restablecen automáticamente cuando reciben una señal y los que se restablecen manualmente.
Los identificadores de espera de evento son útiles en muchos de los mismos escenarios de
sincronización que los de la clase Monitor. Los identificadores de espera de evento son a
menudo más fáciles de utilizar que los métodos Monitor.Wait y Monitor.Pulse y proporcionan
un mayor control sobre la señalización. Los identificadores de espera de evento con nombre
también se pueden utilizar para sincronizar las actividades entre los dominios de aplicación y
procesos, mientras que los monitores son locales a un dominio de aplicación.
1.4.5.1. EventWaitHandle
La clase EventWaitHandle permite que los subprocesos se comuniquen entre sí a través de
señales y a través de la espera de señales. Los controladores de espera de eventos (a los que
también se conoce simplemente como eventos) son controladores de espera que pueden
señalizarse para liberar uno o varios subprocesos en espera. Un controlador de espera de
eventos, una vez señalizado, se restablece manual o automáticamente. La clase
EventWaitHandle puede representar un controlador de espera de eventos (evento local) o un
controlador de espera de eventos del sistema con nombre (evento con nombre o evento del
sistema, visible para todos los procesos).
Nota

MCT: Luis Dueñas Pag 51 de 336


Manual de .NET Framework 4.5

Los controladores de espera de eventos no son eventos en el sentido en que se utiliza este
término en .NET Framework; no hay delegados ni controladores de eventos implicados. Se
utiliza el término "evento" para describirlos porque tradicionalmente se les conocía como
eventos del sistema operativo y porque el acto de señalizar el controlador de espera indica a los
subprocesos en espera que se ha producido un evento.
Tanto los controladores de espera de eventos locales como los de eventos con nombre utilizan
objetos de sincronización del sistema, que están protegidos mediante contenedores
SafeWaitHandle para garantizar que se liberen los recursos. Puede utilizar el método
IDisposable.Dispose para liberar inmediatamente los recursos cuando haya terminado de utilizar
el objeto.
Controladores de espera de eventos de restablecimiento automático
Puede crear un evento de restablecimiento automático especificando
EventResetMode.AutoReset al crear el objeto EventWaitHandle. Como su propio nombre
indica, este evento de sincronización se restablece automáticamente cuando se señaliza, después
de liberar un único subproceso en espera. Señalice el evento llamando a su método Set.
Los eventos de restablecimiento automático se utilizan normalmente para proporcionar acceso
exclusivo a un recurso a un único subproceso cada vez. Un subproceso solicita el recurso
llamando al método WaitOne. Si no hay ningún otro subproceso que contenga el controlador de
espera, el método devolverá true y el subproceso de llamada tomará el control del recurso.
Importante
Como ocurre con todos los mecanismos de sincronización, deberá asegurarse de que todas las
rutas de acceso al código se mantengan a la espera en el controlador de espera adecuado antes
de obtener acceso a un recurso protegido. La sincronización de subprocesos es operación
cooperativa.
Si un evento de restablecimiento automático se señaliza cuando no hay ningún subproceso en
espera, permanecerá señalizado hasta que algún subproceso intente esperar en él. El evento
liberará el subproceso e, inmediatamente después, se restablecerá, de modo que bloqueará los
subprocesos posteriores.
Controladores de espera de eventos de restablecimiento manual
Puede crear un evento de restablecimiento manual especificando EventResetMode.ManualReset
al crear el objeto EventWaitHandle. Cuando su propio nombre indica, este evento de
sincronización debe restablecerse manualmente tras su señalización. Hasta que no se restablezca
mediante una llamada al método Reset, los subprocesos que se mantengan a la espera en el
controlador de eventos se ejecutarán inmediatamente, sin bloquearse.
El funcionamiento de un evento de restablecimiento manual se asemeja al de la puerta de un
establo. Si el evento no está señalizado, los subprocesos en espera se bloquean mutuamente,
como los caballos de un establo. Cuando el evento se señaliza mediante una llamada al método
Set, todos los subprocesos en espera quedan libres para su ejecución. El evento permanece
señalizado hasta que se llama al método Reset. Esto hace del evento de restablecimiento manual
una forma ideal de retener subprocesos que deben esperar a que otro subproceso finalice una
tarea.
Como en el ejemplo de los caballos que salen del establo, el sistema operativo tarda cierto
tiempo en programar los subprocesos liberados y reanudar su ejecución. Si se llama al método
Reset antes de que se haya reanudado la ejecución de todos los subprocesos, los subprocesos
restantes volverán a bloquearse. No se puede determinar qué procesos se reanudarán y cuáles se
bloquearán ya que depende de una serie de factores aleatorios, como la carga del sistema, el
número de subprocesos en espera de programación, etc. Esto no supone ningún problema si el
subproceso que señaliza el evento finaliza tras la señalización, que es el patrón de uso más
común. Si desea que el subproceso que señalizaba el evento inicie una nueva tarea una vez

MCT: Luis Dueñas Pag 52 de 336


Manual de .NET Framework 4.5

reanudados todos los subprocesos en espera, deberá bloquearlo hasta que se hayan reanudado
todos los subprocesos en espera. De lo contrario, se producirá una condición de carrera y el
comportamiento del código será imprevisible.
Características comunes de los eventos automáticos y manuales
Normalmente, uno o varios subprocesos se bloquean en EventWaitHandle hasta que un
subproceso desbloqueado llama al método Set, que libera uno de los subprocesos en espera (en
el caso de los eventos de restablecimiento automático) o todos los subprocesos (en el caso de los
eventos de restablecimiento manual). Un subproceso puede señalizar un controlador
EventWaitHandle y, a continuación, bloquearse en el mismo, como una operación atómica,
llamando al método WaitHandle.SignalAndWait estático.
Los objetos EventWaitHandle pueden utilizarse con los métodos WaitHandle.WaitAll y
WaitHandle.WaitAny estáticos. Como las clases EventWaitHandle y Mutex se derivan de
WaitHandle, se pueden utilizar ambas clases con estos métodos.
Eventos con nombre
El sistema operativo Windows permite que los controladores de espera de eventos tengan
nombres. Un evento con nombre afecta a todo el sistema, es decir, una vez que se crea, es
visible para todos los subprocesos en todos los procesos. Por tanto, los eventos con nombre
pueden utilizarse para sincronizar las actividades de los procesos y de los subprocesos.
Se puede crear un objeto EventWaitHandle que represente un evento con nombre del sistema
utilizando uno de los constructores que especifica un nombre de evento.
Nota
Como los eventos con nombre afectan a todo el sistema, es posible disponer de varios objetos
EventWaitHandle que representen al mismo evento con nombre. Cada vez que se llame a un
constructor, o al método OpenExisting, se creará un nuevo objeto EventWaitHandle. Si se
especifica repetidamente el mismo nombre, se crean varios objetos que representan el mismo
evento con nombre.
Se recomienda utilizar con precaución los eventos con nombre. Como son válidos para todo el
sistema, si otro proceso utiliza el mismo nombre podrían bloquearse inesperadamente los
subprocesos. Cualquier código malintencionado ejecutado en el mismo equipo podría utilizar
esto como base de un ataque por denegación de servicio.
Utilice la seguridad de control de acceso para proteger un objeto EventWaitHandle que
represente un evento con nombre, preferiblemente utilizando un constructor que especifique un
objeto EventWaitHandleSecurity. También puede aplicar la seguridad de control de acceso
mediante el método SetAccessControl, pero daría cabida a un margen de vulnerabilidad entre el
momento de creación del controlador de espera de eventos y el momento de su protección. La
protección de eventos por medio de la seguridad de control de acceso contribuye a evitar
ataques malintencionados, pero no resuelve el problema de conflictos de nombres involuntarios.
Nota
A diferencia de la clase EventWaitHandle, las clases derivadas AutoResetEvent y ManualReset
Event sólo pueden representar controladores de espera locales. No pueden representar eventos
con nombre del sistema.

1.4.5.2. AutoResetEvent
La clase AutoResetEvent representa un evento de identificador de espera local que se restablece
automáticamente cuando se le señala, después de liberar un subproceso en espera único. Esta
clase es un caso especial de su clase base, EventWaitHandle. Consulte la documentación
conceptual EventWaitHandle para obtener información sobre el uso y las características de los
eventos de restablecimiento automático.

MCT: Luis Dueñas Pag 53 de 336


Manual de .NET Framework 4.5

Un objeto AutoResetEvent se restablece automáticamente a no señalado por el sistema una vez


que se ha liberado un subproceso en espera único. Si no hay subprocesos en espera, el objeto del
evento sigue teniendo el estado señalizado. AutoResetEvent corresponde a una llamada
CreateEvent de Win32, especificando false para el argumento bManualReset.
1.4.5.3. ManualResetEvent y ManualResetEventSlim
La clase System.Threading.ManualResetEvent representa un evento de identificador de espera
local que se debe restablecer manualmente después de ser señalizado. Esta clase es un caso
especial de su clase base, System.Threading.EventWaitHandle. Consulte EventWaitHandle en la
documentación conceptual para conocer el uso y las características de los eventos que se
restablecen manualmente.
Un objeto ManualResetEvent permanece señalizado hasta que se llame al método
EventWaitHandle. Reset. Se pueden liberar cualquier número de subprocesos en espera, o de
subprocesos que esperan en el evento después de haber sido señalizado, mientras el estado del
objeto es señalizado. ManualResetEvent corresponde a una llamada CreateEvent de Win32,
especificando true para el argumento bManualReset.
En .NET Framework 4, se puede utilizar la clase System.Threading.ManualResetEventSlim
para mejorar el rendimiento cuando se prevé que los tiempos de espera sean muy cortos y
cuando el evento no cruza los límites de un proceso. ManualResetEventSlim utiliza giros de
ocupado durante un breve intervalo de tiempo mientras espera a que se señale el evento. Cuando
los tiempos de espera son cortos, los giros puedes ser mucho menos costosos que las esperas
con identificadores de espera. Sin embargo, si no se señala el evento dentro de un período de
tiempo determinado, ManualResetEventSlim recurre a una espera de identificador de evento
normal.
1.4.5.4. CountdownEvent
System.Threading.CountdownEvent es una primitiva de sincronización que desbloquea los
subprocesos en espera después de haber sido señalada cierto número de veces. CountdownEvent
se ha diseñado para escenarios en los que, de lo contrario, habría que utilizar ManualResetEvent
o ManualResetEventSlim y disminuir manualmente una variable antes de señalar el evento. Por
ejemplo, en un escenario de bifurcación/combinación, puede crear un CountdownEvent que
tiene un recuento de señales de 5 y, a continuación, iniciar cinco elementos de trabajo en el
grupo de subprocesos y hacer que cada elemento de trabajo llame a Signal cuando se complete.
Cada llamada a Signal disminuye el recuento de señales en 1. En el subproceso principal, la
llamada a Wait se bloqueará hasta que el recuento de señales sea cero.
Nota
Para el código que no tiene que interactuar con las API de sincronización de .NET Framework
heredadas, considere el uso de objetos System.Threading.Tasks.Task o del método Invoke para
un planteamiento aún más sencillo para expresar el paralelismo bifurcación/combinación.
CountdownEvent tiene estas características adicionales:
 La operación de espera puede cancelarse con los token de cancelación.
 Se puede incrementar su recuento de señales una vez creada la instancia.
 Se pueden reutilizar las instancias después de que Wait haya vuelto mediante la llamada
al método Reset.
 Las instancias exponen un objeto WaitHandle para la integración con otras API de
sincronización de .NET Framework, como WaitAll.
Uso básico
En el ejemplo siguiente se muestra la forma de utilizar un CountdownEvent con elementos de
trabajo ThreadPool.
IEnumerable<Data> source = GetData();

MCT: Luis Dueñas Pag 54 de 336


Manual de .NET Framework 4.5

using (CountdownEvent e = new CountdownEvent(1))


{
// fork work:
foreach (Data element in source)
{
// Dynamically increment signal count.
e.AddCount();
ThreadPool.QueueUserWorkItem(delegate(object state)
{
try
{
ProcessData(state);
}
finally
{
e.Signal();
}
},
element);
}
e.Signal();
// The first element could be run on this thread. Join with work.
e.Wait();
}
// .,.

CountdownEvent con cancelación


En el siguiente ejemplo se muestra cómo cancelar la operación de espera en CountdownEvent
utilizando un token de cancelación. El modelo básico sigue el modelo de la cancelación
unificada, que se presentó en .NET Framework 4.
class CancelableCountdowEvent
{
class Data
{
public int Num { get; set; }
public Data(int i) { Num = i; }
public Data() { }
}

class DataWithToken
{
public CancellationToken Token { get; set; }
public Data Data { get; private set; }
public DataWithToken(Data data, CancellationToken ct)
{
this.Data = data;
this.Token = ct;
}
}

static IEnumerable<Data> GetData()


{
return new List<Data>() { new Data(1), new Data(2), new Data(3), new
Data(4),
new Data(5) };
}

static void ProcessData(object obj)


{
DataWithToken dataWithToken = (DataWithToken)obj;
if (dataWithToken.Token.IsCancellationRequested)
{
Console.WriteLine("Canceled before starting {0}",
dataWithToken.Data.Num);
return;
}

MCT: Luis Dueñas Pag 55 de 336


Manual de .NET Framework 4.5

for (int i = 0; i < 10000; i++)


{
if (dataWithToken.Token.IsCancellationRequested)
{
Console.WriteLine("Cancelling while executing {0}",
dataWithToken.Data.Num);
return;
}
// Increase this value to slow down the program.
Thread.SpinWait(100000);
}
Console.WriteLine("Processed {0}", dataWithToken.Data.Num);
}

static void Main(string[] args)


{
EventWithCancel();
Console.WriteLine("Press enter to exit.");
Console.ReadLine();
}

static void EventWithCancel()


{
IEnumerable<Data> source = GetData();
CancellationTokenSource cts = new CancellationTokenSource();
//Enable cancellation request from a simple UI thread.
Task.Factory.StartNew(() =>
{
if (Console.ReadKey().KeyChar == 'c')
cts.Cancel();
});
// Event must have a count of at least 1
CountdownEvent e = new CountdownEvent(1);
// fork work:
foreach (Data element in source)
{
DataWithToken item = new DataWithToken(element, cts.Token);
// Dynamically increment signal count.
e.AddCount();
ThreadPool.QueueUserWorkItem(delegate(object state)
{
ProcessData(state);
if (!cts.Token.IsCancellationRequested)
e.Signal();
},
item);
}
// Decrement the signal count by the one we added in the constructor.
e.Signal();
// The first element could be run on this thread.
// Join with work or catch cancellation.
try
{
e.Wait(cts.Token);
}
catch (OperationCanceledException oce)
{
if (oce.CancellationToken == cts.Token)
{
Console.WriteLine("User canceled.");
}
else throw; //We don't know who canceled us!
}
e.Dispose();
//...
} //end method
} //end class

MCT: Luis Dueñas Pag 56 de 336


Manual de .NET Framework 4.5

Observe que la operación de espera no cancela los subprocesos que la señalan. Normalmente, la
cancelación se aplica a una operación lógica, lo que puede incluir esperar el evento y todos los
elementos de trabajo que la espera esté sincronizando. En este ejemplo, a cada elemento de
trabajo se pasa una copia del mismo token de cancelación para que pueda responder a la
solicitud de cancelación.
1.4.6. Exclusiones mutuas (mutex)
Puede utilizar un objeto Mutex para proporcionar acceso exclusivo a un recurso. La clase Mutex
utiliza más recursos del sistema que la clase Monitor, pero pueden calcularse las referencias de
la misma a través de los límites del dominio de aplicación, puede utilizarse con varias esperas y
puede utilizarse para sincronizar subprocesos de distintos procesos. Encontrará una comparación
de los mecanismos de sincronización administrados en Información general sobre los primitivos
de sincronización.
Usar exclusiones mutuas
Un subproceso llama al método WaitOne de una exclusión mutua para solicitar su propiedad. La
llamada se bloquea hasta que la exclusión mutua queda disponible o hasta que transcurre el
intervalo de tiempo de espera opcional. Si una exclusión mutua no pertenece a ningún
subproceso, el estado de la misma se señaliza.
Un subproceso libera una exclusión mutua llamando a su método ReleaseMutex. Las
exclusiones mutuas cuentan con afinidad de subproceso; es decir, el subproceso que posee la
exclusión mutua es el único que puede liberarla. Si un subproceso libera una exclusión mutua
que no posee, se produce una excepción ApplicationException en el subproceso.
Como la clase Mutex se deriva de WaitHandle, también puede llamar al método estático
WaitAll o WaitAny de WaitHandle para solicitar la propiedad de Mutex en combinación con
otros controladores de espera.
Si un subproceso posee un objeto Mutex, dicho subproceso puede especificar el mismo Mutex
en llamadas repetidas de solicitud de espera sin bloquear su ejecución; no obstante, deberá
liberar Mutex las veces que sean necesarias para liberar su propiedad.
Exclusiones mutuas abandonadas
Si un subproceso finaliza sin liberar Mutex, la exclusión mutua se considera abandonada. Esto
normalmente pone de manifiesto un error de programación grave porque el recurso al que
protege la exclusión mutua podría quedarse en un estado incoherente. En la versión 2.0 de .NET
Framework, se produce una excepción AbandonedMutexException en el siguiente subproceso
que adquiere la exclusión mutua.
Nota
En las versiones 1.0 y 1.1 de .NET Framework, un objeto Mutex abandonado se establece en
estado señalado y el siguiente subproceso en espera obtiene su propiedad. Si no hay ningún
subproceso en espera, Mutex permanece en estado señalado. No se produce ninguna excepción.
En el caso de una exclusión mutua en todo el sistema, una exclusión mutua abandonada podría
indicar que una aplicación ha finalizado de forma abrupta (por ejemplo, mediante el
Administrador de tareas de Windows).
Exclusiones mutuas locales y del sistema
Las exclusiones mutuas son de dos tipos: exclusiones mutuas locales y exclusiones mutuas del
sistema con nombre. Si crea un objeto Mutex mediante el uso de un constructor que acepta un
nombre, quedará asociado a un objeto del sistema operativo con ese nombre. Las exclusiones
mutuas del sistema con nombre son visibles en todo el sistema operativo y pueden utilizarse
para sincronizar las actividades de los procesos. Puede crear varios objetos Mutex que

MCT: Luis Dueñas Pag 57 de 336


Manual de .NET Framework 4.5

representen la misma exclusión mutua del sistema con nombre y puede utilizar el método
OpenExisting para abrir una exclusión mutua del sistema con nombre existente.
Una exclusión mutua local sólo existe dentro de su proceso. Puede utilizarla cualquier
subproceso del proceso que tenga una referencia al objeto Mutex local. Cada objeto Mutex es
una exclusión mutua local independiente.
Seguridad de control de acceso para exclusiones mutuas del sistema
La versión 2.0 de .NET Framework proporciona la posibilidad de consultar y establecer
seguridad de control de acceso de Windows para los objetos del sistema con nombre. Es
recomendable proteger las exclusiones mutuas del sistema desde el momento de su creación
porque los objetos del sistema son globales y, por lo tanto, un código distinto del suyo propio es
capaz de bloquearlas.
Para obtener información sobre la seguridad de control de acceso para las exclusiones mutuas,
vea las clases MutexSecurity y MutexAccessRule, la enumeración MutexRights, los métodos
GetAccessControl, SetAccessControl y OpenExisting de la clase Mutex y el constructor
Mutex(Boolean, String, Boolean, MutexSecurity).
1.4.7. Operaciones de bloqueo
La clase Interlocked proporciona métodos que sincronizan el acceso a una variable compartida
por varios subprocesos. Los subprocesos de diferentes procesos pueden utilizar este mecanismo
si la variable está en la memoria compartida. Las operaciones de bloqueo son atómicas, es decir,
el conjunto de la operación constituye una unidad que ninguna otra operación de bloqueo puede
interrumpir en la misma variable. Esto último tiene importancia en sistemas operativos con
multithreading preferente, donde se puede suspender un subproceso después de cargar un valor
desde una dirección de memoria, pero antes de tener oportunidad de alterarlo y almacenarlo.
La clase Interlocked proporciona las siguientes operaciones:
 En la versión 2.0 de .NET Framework, el método Add agrega un valor entero a una
variable y devuelve el nuevo valor de la variable.
 En la versión 2.0 de .NET Framework, el método Read lee un valor entero de 64 bits
como una operación atómica. Esto resulta útil en sistemas operativos de 32 bits, donde
leer un entero de 64 bits no constituye habitualmente una operación atómica.
 Los métodos Increment y Decrement incrementan o decrementan una variable y
devuelven el valor resultante.
 El método Exchange realiza un intercambio atómico del valor en la variable
especificada, devolviendo dicho valor y reemplazándolo por un valor nuevo. En la
versión 2.0 de .NET Framework, puede utilizarse una sobrecarga genérica de este
método para realizar este intercambio en una variable con cualquier tipo de referencia.
 El método CompareExchange también intercambia dos valores, pero en función del
resultado de una comparación. En la versión 2.0 de .NET Framework, puede utilizarse
una sobrecarga genérica de este método para realizar este intercambio en una variable
con cualquier tipo de referencia.
En los procesadores modernos, los métodos de la clase Interlocked a menudo pueden
implementarse mediante una única instrucción. Así, proporcionan una sincronización con un
rendimiento muy elevado y pueden utilizarse para compilar mecanismos de sincronización de
nivel superior, como bloqueos circulares.
Ejemplo de CompareExchange
El método CompareExchange se puede utilizar para proteger cálculos más complicados que un
simple incremento y decremento. En el siguiente ejemplo se muestra un método seguro para
subprocesos que se agrega a un total actualizado almacenado como un número de punto flotante.
(En el caso de los números enteros, el método Add constituye una solución más sencilla.) Para
obtener ejemplos de código completos, vea las sobrecargas de CompareExchange que toma

MCT: Luis Dueñas Pag 58 de 336


Manual de .NET Framework 4.5

argumentos de punto flotante de precisión sencilla y doble (CompareExchange(Single, Single,


Single) y CompareExchange(Double, Double, Double)).
using System;
using System.Threading;

public class ThreadSafe


{
// totalValue contains a running total that can be updated by multiple
threads.
// It must be protected from unsynchronized access.
private double totalValue = 0;

// The Total property returns the running total.


public double Total
{
get { return totalValue; }
}

// AddToTotal safely adds a value to the running total.


public double AddToTotal(double addend)
{
double initialValue, computedValue;
do
{
// Save the current running total in a local variable.
initialValue = totalValue;
// Add the new value to the running total.
computedValue = initialValue + addend;
// CompareExchange compares totalValue to initialValue. If
// they are not equal, then another thread has updated the
// running total since this loop started. CompareExchange
// does not update totalValue. CompareExchange returns the
// contents of totalValue, which do not equal initialValue,
// so the loop executes again.
}
while (initialValue != Interlocked.CompareExchange(
ref totalValue, computedValue, initialValue));
// If no other thread updated the running total, then
// totalValue and initialValue are equal when CompareExchange
// compares them, and computedValue is stored in totalValue.
// CompareExchange returns the value that was in totalValue
// before the update, which is equal to initialValue, so the
// loop ends.
// The function returns computedValue, not totalValue, because
// totalValue could be changed by another thread between
// the time the loop ends and the function returns.
return computedValue;
}
}

Sobrecargas sin tipo de Exchange y CompareExchange


Los métodos Exchange y CompareExchange disponen de sobrecargas que aceptan argumentos
de tipo Object. El primer argumento de cada una de estas sobrecargas es ref Object (ByRef …
As Object en Visual Basic) y la seguridad de tipos requiere que se pase la variable a este
argumento para que sea estrictamente de tipo Object; no se puede transformar simplemente el
primer argumento a tipo Object cuando se realiza la llamada a estos métodos.
Nota
En la versión 2.0 de .NET Framework, utilice las sobrecargas genéricas de los métodos
Exchange y CompareExchange para intercambiar variables fuertemente tipadas.
En el siguiente ejemplo de código se muestra una propiedad de tipo ClassA que sólo puede
establecerse una vez, ya que debería implementarse en la versión 1.0 o 1.1 de .NET Framework.

MCT: Luis Dueñas Pag 59 de 336


Manual de .NET Framework 4.5

public class ClassB


{
// The private field that stores the value for the ClassA property is
intialized
// to null. It is set once, from any of several threads. The field must
// be of type Object, so that CompareExchange can be used to assign the
value.
// If the field is used within the body of class Test, it must be cast to
// type ClassA.
private object classAValue = null;

// This property can be set once to an instance of ClassA. Attempts to set


it
// again cause an exception to be thrown.
public ClassA ClassA
{
get
{
return (ClassA) classAValue;
}
set
{
// CompareExchange compares the value in classAValue
// to null. The new value assigned to the ClassA
// property, which is in the special variable 'value',
// is placed in classAValue only if classAValue is
// equal to null.
if (null != Interlocked.CompareExchange(ref classAValue,
(Object) value, null))
{
// CompareExchange returns the original value of
// classAValue; if it is not null, then a value
// was already assigned, and CompareExchange did not
// replace the original value. Throw an exception to
// indicate that an error occurred.
throw new ApplicationException("ClassA was already set.");
}
}
}
}

1.4.8. Bloqueos de lector y escritor


La clase ReaderWriterLockSlim habilita a varios subprocesos para leer un recurso de forma
simultánea, aunque para escribir en el recurso, el subproceso debe esperar a un bloqueo
exclusivo.
Dentro de la aplicación se puede utilizar un ReaderWriterLockSlim para proporcionar una
sincronización conjunta entre los subprocesos que tienen acceso a un recurso compartido. Los
bloqueos se toman en el mismo ReaderWriterLockSlim.
Al igual que con cualquier mecanismo de sincronización de subprocesos, se debe comprobar
que ningún subproceso omite el bloqueo proporcionado por ReaderWriterLockSlim. Una
manera de asegurarse es diseñar una clase que encapsule el recurso compartido. Esta clase
proporcionaría los miembros que tienen acceso al recurso compartido privado y que utilizan un
ReaderWriterLockSlim privado para la sincronización. Para obtener un ejemplo, vea el código
de ejemplo que se proporciona para la clase ReaderWriterLockSlim. ReaderWriterLockSlim es
bastante eficaz para utilizarse en la sincronización de objetos individuales.
Organice su aplicación con el fin de reducir al mínimo la duración de las operaciones de lectura
y escritura. Las operaciones de escritura de larga duración afectan al rendimiento porque el
bloqueo de escritura es exclusivo. Las operaciones de lectura de larga duración bloquean el
sistema de escritura en espera y, si hay al menos un subproceso en espera de acceso de escritura,
entonces los subprocesos que soliciten acceso de lectura también se bloquearán.

MCT: Luis Dueñas Pag 60 de 336


Manual de .NET Framework 4.5

Nota
.NET Framework dispone de dos bloqueos de lector y escritor, ReaderWriterLockSlim y
ReaderWriterLock. ReaderWriterLockSlim se recomienda para todos los trabajos de desarrollo
nuevos. ReaderWriterLockSlim es similar a ReaderWriterLock, pero tiene reglas simplificadas
para la recursividad y para actualizar y degradar el estado del bloqueo. ReaderWriterLockSlim
evita muchos casos de interbloqueo potencial. Además, el rendimiento de
ReaderWriterLockSlim es significativamente mejor que ReaderWriterLock.

1.4.9. Semaphore y SemaphoreSlim


La clase System.Threading.Semaphore representa un semáforo local o con nombre (para todo el
sistema). Es un contenedor fino alrededor del objeto semáforo de Win32. Los semáforos de
Win32 son semáforos con recuento, que se pueden utilizar para controlar el acceso a un grupo
de recursos.
La clase SemaphoreSlim representa un semáforo ligero y rápido que se puede utilizar para
esperar dentro de un proceso único cuando se prevé que los tiempos de espera sean muy cortos.
SemaphoreSlim confía en la medida de lo posible en las primitivas de sincronización
proporcionadas por Common Language Runtime (CLR). Sin embargo, también proporciona de
forma diferida identificadores de espera inicializados y basados en kernel si son necesarios para
permitir la espera en varios semáforos. SemaphoreSlim también admite el uso de tokens de
cancelación, pero no admite semáforos con nombre o el uso de un identificador de espera para
sincronización.
Administrar un recurso limitado
Los subprocesos entran en el semáforo llamando al método WaitOne, que se hereda de la clase
WaitHandle. Cuando la llamada vuelve, disminuye el recuento en el semáforo. Cuando un
subproceso solicita la entrada y el recuento es cero, el subproceso se bloquea. Cuando los
subprocesos liberan el semáforo llamando al método Semaphore.Release, se permite la entrada a
los subprocesos bloqueados. No hay ningún orden garantizado, como primero en entrar, primero
en salir (FIFO) o último en entrar, primero en salir (LIFO), por el que los subprocesos
bloqueados entren en el semáforo.
Un subproceso puede entrar varias veces en el semáforo llamando repetidamente al método
WaitOne. Para liberar el semáforo, el subproceso puede llamar a la sobrecarga del método
Release() el mismo número de veces o, si no, puede llamar a la sobrecarga del método
Release(Int32) y especificar el número de entradas que quiere liberar.
Los semáforos y la identidad del subproceso
La clase Semaphore no exige la identidad del subproceso en las llamadas a los métodos
WaitOne y Release. Por ejemplo, un escenario habitual de uso de los semáforos implica la
existencia de un subproceso productor y un subproceso consumidor, donde uno de los
subprocesos siempre incrementa el recuento del semáforo y el otro siempre lo disminuye.
Es responsabilidad del programador garantizar que un subproceso no libere el semáforo
demasiadas veces. Por ejemplo, imagine un semáforo que tiene un recuento máximo de dos y en
el que entran un subproceso A y un subproceso B. Si un error de programación del subproceso
B hace que llame a Release dos veces, las dos llamadas tienen éxito. El recuento del semáforo
está completo y cuando finalmente el subproceso A llama al método Release, se produce una
excepción SemaphoreFullException.
Semáforos con nombre
El sistema operativo Windows permite que los semáforos tengan nombres. Un semáforo con
nombre es un semáforo para todo el sistema. Es decir, una vez creado el semáforo con nombre,
éste es visible para todos los subprocesos de todos los procesos. Así, se puede utilizar un
semáforo con nombre para sincronizar las actividades tanto de procesos como de subprocesos.

MCT: Luis Dueñas Pag 61 de 336


Manual de .NET Framework 4.5

Puede crear un objeto Semaphore que represente un semáforo de sistema con nombre utilizando
uno de los constructores que especifica un nombre.
Nota
Dado que los semáforos con nombre son semáforos para todo el sistema, es posible tener varios
objetos Semaphore que representen el mismo semáforo con nombre. Cada vez que se llama a un
constructor o al método Semaphore.OpenExisting, se crea un nuevo objeto Semaphore. Si se
especifica el mismo nombre repetidas veces, se crean varios objetos que representan el mismo
semáforo con nombre.
Tenga el cuidado al utilizar los semáforos con nombre. Dado que son semáforos para todo el
sistema, puede ocurrir que otro proceso que utilice el mismo nombre entre inesperadamente en
el semáforo. Cualquier código malintencionado ejecutado en el mismo equipo podría utilizar
esto como base de un ataque por denegación de servicio.
Utilice la seguridad de control de acceso para proteger un objeto Semaphore que represente un
semáforo con nombre, preferentemente utilizando un constructor que especifique un objeto
System.Security.AccessControl.SemaphoreSecurity. También puede aplicar la seguridad de
control de acceso mediante el método Semaphore.SetAccessControl, aunque este sistema dejará
un espacio de vulnerabilidad entre el momento en que se crea el semáforo y el momento en que
se protege. Proteger los semáforos con seguridad de control de acceso ayuda a evitar ataques
malintencionados, pero no resuelve el problema de los conflictos de nombres no intencionados.
1.4.10. Información general sobre los primitivos de sincronización
.NET Framework proporciona un intervalo de primitivos de sincronización para controlar las
interacciones de subprocesos y evitar las condiciones de carrera. Éstos se pueden dividir
básicamente en tres categorías: operaciones de bloqueo, señalización e interbloqueo.
La definición de estas categorías no es clara ni nítida: algunos mecanismos de sincronización
tienen características de varias categorías; los eventos que liberan un único subproceso a la vez
actúan funcionalmente como bloqueos; la liberación de cualquier bloqueo se puede considerar
como una señal y las operaciones de interbloqueo se pueden usar para construir bloqueos. Sin
embargo, las categorías siguen siendo útiles.
Es importante recordar que la sincronización de subprocesos es cooperativa. Incluso si un
subproceso omite un mecanismo de sincronización y tiene acceso directamente al recurso
protegido, ese mecanismo de la sincronización no puede ser eficaz.
Bloqueo
Los bloqueos proporcionan el control de un recurso a un subproceso cada vez, o a un número
especificado de subprocesos. Un subproceso que solicita un bloqueo exclusivo cuando el
bloqueo está en uso queda bloqueado hasta que el bloqueo está disponible.
Bloqueos exclusivos
La forma más sencilla de bloqueo es la instrucción lock de C# (SyncLock en Visual Basic), que
controla el acceso a un bloque de código. Este tipo de bloque frecuentemente se suele
denominar sección crítica. La instrucción lock se implementa utilizando los métodos Enter y
Exit de la clase Monitor, y utiliza try…catch…finally para garantizar que se libera el bloqueo.
En general, utilizar la instrucción lock para proteger pequeños bloques de código, sin abarcar
nunca más de un único método, es la mejor manera de usar la clase Monitor. Aunque eficaz, la
clase Monitor es propensa a que se produzcan bloqueos huérfanos e interbloqueos.
Clase Monitor
La clase Monitor proporciona una funcionalidad adicional, que se puede utilizar junto con la
instrucción lock:

MCT: Luis Dueñas Pag 62 de 336


Manual de .NET Framework 4.5

 El método TryEnter permite un subproceso que está bloqueado en espera de que el


recurso se interrumpa una vez transcurrido un intervalo de tiempo especificado.
Devuelve un valor booleano que indica la finalización correcta o incorrecta, que se
puede utilizar para detectar y evitar posibles interbloqueos.
 Un subproceso de una sección crítica llama al método Wait. Deja el control del recurso
y se bloquea hasta que el recurso vuelve a estar disponible.
 Los métodos Pulse y PulseAll permiten que un subproceso que esté a punto de liberar el
bloqueo o de llamar a Wait sitúe uno o más subprocesos en la cola de subprocesos
listos, de manera que puedan adquirir el bloqueo.
Los tiempos de espera en sobrecargas de método Wait permiten a los subprocesos en espera
situarse en la cola de subprocesos listos.
La clase Monitor puede proporcionar el bloqueo en varios dominios de aplicación si el objeto
utilizado para el bloqueo deriva de MarshalByRefObject.
Monitor tiene afinidad de subprocesos. Es decir, un subproceso en el que entró el monitor debe
salir llamando a Exit o Wait.
No se pueden crear instancias de la clase Monitor. Sus métodos son estáticos (Shared en Visual
Basic) y actúan en un objeto de bloqueo instanciable.
Clase Mutex
Los subprocesos solicitan una Mutex llamando a una sobrecarga de su método WaitOne. Se
proporcionan sobrecargas con tiempos de espera para permitir a los subprocesos abandonar la
espera. A diferencia de la clase Monitor, una exclusión mutua (mutex) puede ser local o global.
Las exclusiones mutuas globales, también denominadas mutex con nombre, son visibles en todo
el sistema operativo y se pueden utilizar para sincronizar subprocesos en varios procesos o
dominios de aplicación. Las exclusiones mutuas locales derivan de MarshalByRefObject y se
pueden utilizar superando los límites del dominio de aplicación.
Además, Mutex deriva de WaitHandle, lo que significa que se puede utilizar con los
mecanismos de señalización proporcionados por WaitHandle, como por ejemplo los métodos
WaitAll, WaitAny y SignalAndWait.
Como Monitor, Mutex tiene afinidad de subprocesos. A diferencia de Monitor, un Mutex es un
objeto instanciable.
Clase SpinLock
A partir de .NET Framework 4, puede utilizar la clase SpinLock cuando la sobrecarga requerida
por Monitor disminuya el rendimiento. Si SpinLock encuentra una sección crítica bloqueada,
simplemente gira en un bucle hasta que el bloqueo esté de nuevo disponible. Si el bloqueo se
mantiene durante un tiempo muy corto, el giro puede proporcionar un mejor rendimiento que el
bloqueo. Sin embargo, si el bloqueo se mantiene durante más de unas decenas de ciclos,
SpinLock se ejecuta igual de bien que Monitor, pero utilizará más ciclos de CPU y puede
disminuir por lo tanto el rendimiento de otros subprocesos o procesos.
Otros bloqueos
No es necesario que los bloqueos no sean exclusivos. Con frecuencia resulta útil permitir el
acceso simultáneo a un recurso a un número limitado de subprocesos. Los semáforos y bloqueos
de lector-escritor están diseñados para controlar este tipo de acceso a recursos agrupados.
Clase ReaderWriterLock
La clase ReaderWriterLockSlim está dirigida al caso en el que un subproceso que cambia los
datos, el sistema de escritura, debe tener acceso exclusivo a un recurso. Cuando el sistema de
escritura no está activo, cualquier número de lectores puede obtener acceso al recurso
(llamando, por ejemplo, al método EnterReadLock). Cuando un subproceso solicita el acceso
exclusivo (llamando, por ejemplo, al método EnterWriteLock), las solicitudes posteriores del

MCT: Luis Dueñas Pag 63 de 336


Manual de .NET Framework 4.5

lector se bloquean hasta que todos los lectores existentes salen del bloqueo y el sistema de
escritura entra y sale del bloqueo.
ReaderWriterLockSlim tiene afinidad de subprocesos.
Clase Semaphore
La clase Semaphore permite a un número especificado de subprocesos tener acceso a un
recurso. Los subprocesos adicionales que solicitan el recurso se bloquean hasta que un
subproceso libera el semáforo.
Como la clase Mutex, Semaphore deriva de WaitHandle. También, al igual que Mutex, un
Semaphore puede ser local o global. Se puede utilizar más allá de los límites del dominio de
aplicación.
A diferencia de Monitor, Mutex y ReaderWriterLock, Semaphore no tienen afinidad de
subprocesos. Esto significa se puede utilizar en escenarios en los que un subproceso adquiere el
semáforo y otro lo libera.
System.Threading.SemaphoreSlim es un semáforo ligero para sincronización dentro del límite
de un único proceso.
Señalización
La manera más sencilla de esperar una señal de otro subproceso es llamar al método Join, que se
bloquea hasta que se complete el otro subproceso. Join tiene dos sobrecargas que permiten al
subproceso bloqueado interrumpir la espera una vez transcurrido un intervalo especificado.
Los identificadores de espera proporcionan un conjunto mucho más rico de capacidades de
espera y señalización.
Controladores de espera
Los identificadores de espera derivan de la clase WaitHandle, que a su vez deriva de
MarshalByRef Object. Así, los identificadores de espera se pueden utilizar para sincronizar las
actividades de los subprocesos fuera de los límites del dominio de aplicación.
Los subprocesos se bloquean en los identificadores de espera llamando al método de instancia
WaitOne o uno de los métodos estáticos WaitAll, WaitAny o SignalAndWait. La forma de
liberación depende de qué método se llamó y del tipo de identificadores de espera.
Identificadores de espera de evento
Los identificadores de espera de evento incluyen la clase EventWaitHandle y sus clases
derivadas, AutoResetEvent y ManualResetEvent. Los subprocesos se liberan desde un
identificador de espera de eventos cuando el identificador de espera de eventos está señalado
llamando a su método Set o utilizando el método SignalAndWait.
Los identificadores de espera de eventos se pueden restablecer automáticamente, como un
torniquete que sólo permite una lectura cada vez que se señala, o se debe restablecer
manualmente, como una puerta que está cerrada hasta que se señala y luego queda abierta hasta
que alguien la cierra. Cuando sus nombres implican, AutoResetEvent y ManualResetEvent
representan el primer caso y el último, respectivamente.
System.Threading.ManualResetEventSlim es un evento ligero para sincronización dentro del
límite de un único proceso.
EventWaitHandle puede representar cualquier tipo de evento y puede ser local o global. Las
clases derivadas AutoResetEvent y ManualResetEvent siempre son locales.
Los identificadores de espera de evento no tienen afinidad de subprocesos. Cualquier
subproceso puede señalar un identificador de espera de evento.

MCT: Luis Dueñas Pag 64 de 336


Manual de .NET Framework 4.5

Clases Mutex y Semaphore


Dado que las clases Mutex y Semaphore derivan de WaitHandle, se pueden utilizar con los
métodos estáticos de WaitHandle. Por ejemplo, un subproceso puede utilizar el método WaitAll
para esperar hasta que se cumplen las tres condiciones siguientes: se señala un
EventWaitHandle, se libera una Mutex y se libera un Semaphore. De forma parecida, un
subproceso puede utilizar el método WaitAny para esperar hasta que se cumple cualquiera de
esas condiciones.
En el caso de una Mutex o un Semaphore, ser señalizados significa ser liberados. Si cualquiera
de los tipos se utiliza como el primer argumento del método SignalAndWait, se libera. En el
caso de Mutex, que tiene afinidad de subprocesos, se inicia una excepción si el subproceso que
llama no es el propietario de la exclusión mutua. Como se ha indicado antes, los semáforos no
tienen afinidad de subprocesos.
Barrera
La clase Barrier proporciona una manera de sincronizar varios subprocesos de forma cíclica
para que todos se bloqueen en el mismo punto y esperen a que se completen los demás
subprocesos. Una barrera es útil cuando uno o más subprocesos requieren los resultados de otro
subproceso antes de proseguir con la siguiente fase de un algoritmo.
Tipos de sincronización ligeros
A partir de .NET Framework 4, puede utilizar primitivas de sincronización que proporcionan un
rendimiento rápido al evitar, dentro de lo posible, una costosa dependencia de los objetos de
kernel de Win32 como, por ejemplo, los identificadores de espera. En general, estos tipos se
deben utilizar cuando los tiempos de espera son cortos y solo si, una vez probados los tipos de
sincronización originales, se consideren poco satisfactorios. Los tipos ligeros no se pueden
utilizar en escenarios que requieran una comunicación entre procesos.
 System.Threading.SemaphoreSlim es una versión ligera de
System.Threading.Semaphore.
 System.Threading.ManualResetEventSlim es una versión ligera de
System.Threading.Manual ResetEvent.
 System.Threading.CountdownEvent representa un evento que aparece señalado cuando
su recuento es cero.
 System.Threading.Barrier permite a varios subprocesos sincronizarse unos con otros sin
necesidad de un control por parte de un subproceso principal. Una barrera impide que
continúe cada subproceso hasta que todos los subprocesos hayan alcanzado un punto
especificado.
SpinWait
A partir de .NET Framework 4, puede utilizar la estructura System.Threading.SpinWait cuando
un subproceso tenga que esperar a que se señale un evento o se cumpla una condición, pero
siempre que se prevea que el tiempo de espera real sea menor que el tiempo de espera requerido
utilizando un identificador de espera o bloqueando el subproceso actual. Al utilizar SpinWait,
puede especificar un período de tiempo corto para girar durante la espera y generar luego (por
ejemplo, al esperar o estar en suspensión) solo si la condición no se cumplió en el tiempo
especificado.
Operaciones de interbloqueo
Las operaciones interbloqueadas son operaciones atómicas simples realizadas en una ubicación
de memoria por métodos estáticos de la clase Interlocked. Esas operaciones atómicas incluyen
suma, incremento y decremento, intercambio, intercambio condicional que depende de una
comparación, y operaciones de lectura para los valores de 64 bits en plataformas de 32 bits.
Nota

MCT: Luis Dueñas Pag 65 de 336


Manual de .NET Framework 4.5

La garantía de atomicidad está limitada a operaciones individuales; cuando varias operaciones


se deben realizar como una unidad, se debe utilizar un mecanismo de sincronización más
amplio.
Aunque ninguna de estas operaciones son bloqueos o señales, se pueden utilizar para construir
bloqueos y señales. Dado que son nativas del sistema operativo Windows, las operaciones
interbloqueadas son sumamente rápidas.
Las operaciones interbloqueadas se pueden utilizar con garantías de memoria volátil para
escribir aplicaciones que poseen una simultaneidad sin bloqueos eficaz. Sin embargo, requieren
una sofisticada programación a bajo nivel por lo que, para la mayoría de los usos, los bloqueos
simples son una mejor opción.
1.4.11. Barrier
Una barrera es un primitiva de sincronización definida por el usuario que permite que varios
subprocesos (denominados participantes) trabajar simultáneamente en un algoritmo en fases.
Cada participante se ejecuta hasta que alcanza el punto de la barrera en el código. La barrera
representa el fin de una fase de trabajo. Cuando un participante alcanza la barrera, se bloquea
hasta que todos los participantes hayan alcanzado la misma barrera. Después de que todos los
participantes han alcanzado la barrera, se puede invocar, si se desea, una acción de fase
posterior. Esta acción de fase posterior se puede utilizar para que un solo subproceso realice
acciones mientras los demás permanecen bloqueados. Una vez ejecutada la acción, se
desbloquean todos los participantes.
El siguiente fragmento de código muestra un modelo de barrera básico.
// Create the Barrier object, and supply a post-phase delegate
// to be invoked at the end of each phase.
Barrier barrier = new Barrier(2, (bar) =>
{
// Examine results from all threads, determine
// whether to continue, create inputs for next phase, etc.
if (someCondition) success = true;
});
// Define the work that each thread will perform. (Threads do not
// have to all execute the same method.)
void CrunchNumbers(int partitionNum)
{
// Up to System.Int64.MaxValue phases are supported. We assume
// in this code that the problem will be solved before that.
while (success == false)
{
// Begin phase:
// Process data here on each thread, and optionally
// store results, for example:
results[partitionNum] = ProcessData(data[partitionNum]);
// End phase:
// After all threads arrive,post-phase delegate
// is invoked, then threads are unblocked. Overloads
// accept a timeout value and/or CancellationToken.
barrier.SignalAndWait();
}
}

// Perform n tasks to run in in parallel. For simplicity


// all threads execute the same method in this example.
static void Main()
{
var app = new BarrierDemo();
Thread t1 = new Thread(() => app.CrunchNumbers(0));
Thread t2 = new Thread(() => app.CrunchNumbers(1));
t1.Start();
t2.Start();
}

MCT: Luis Dueñas Pag 66 de 336


Manual de .NET Framework 4.5

Agregar y quitar participantes


Al crear Barrier, especifique el número de participantes. También puede agregar o quitar
participantes dinámicamente en cualquier momento. Por ejemplo, si un participante resuelve su
parte del problema, se puede almacenar el resultado, detener la ejecución de ese subproceso y
llamar a RemoveParticipant para disminuir el número de participantes de la barrera. Al agregar
un participante llamando a AddParticipant, el valor devuelto especifica el número de la fase
actual, lo que puede resultar útil para inicializar el trabajo del nuevo participante.
Barreras rotas
Se pueden producir interbloqueos si un participante no consigue alcanzar la barrera. Para evitar
estos interbloqueos, utilice las sobrecargas del método SignalAndWait a fin de especificar un
tiempo de espera y un token de cancelación. Estas sobrecargas devuelven un valor Boolean que
cada participante puede comprobar antes de continuar a la fase siguiente.
Excepciones de fase posterior
Si el delegado de la fase posterior produce una excepción, se encapsula en un objeto
BarrierPostPhase Exception que se propaga a continuación a todos los participantes.
Diferencias entre las barreras y ContinueWhenAll
Las barreras resultan especialmente útiles cuando los subprocesos realizan varias fases en
bucles. Si el código requiere solamente una o dos fases de trabajo, puede ser mejor utilizar
objetos System.Threading.Tasks.Task con cualquier tipo de unión implícita, como las
siguientes:
 ContinueWhenAll
 Invoke
 ForEach
 For
1.4.11.1. Cómo: Sincronizar operaciones simultáneas con una clase
Barrier
En el ejemplo siguiente se muestra cómo sincronizar tareas simultáneas con Barrier.
Ejemplo
El siguiente programa tiene como propósito contar cuántas iteraciones (o fases) se requieren
para que dos subprocesos busquen cada uno su mitad de la solución en la misma fase utilizando
un algoritmo de aleatoriedad para reorganizar las palabras. Después de que cada subproceso
haya organizado sus palabras, en la fase posterior a la barrera se comparan los dos resultados
para determinar si todas las palabras de la frase están correctamente ordenadas.
//#define TRACE
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace BarrierSimple
{
class Program
{
static string[] words1 = new string[] { "brown", "jumped", "the",
"fox",
"quick"};
static string[] words2 = new string[] { "dog", "lazy","the","over"};
static string solution = "the quick brown fox jumped over the lazy
dog.";

MCT: Luis Dueñas Pag 67 de 336


Manual de .NET Framework 4.5

static bool success = false;


static Barrier barrier = new Barrier(2, (b) =>
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < words1.Length; i++)
{
sb.Append(words1[i]);
sb.Append(" ");
}
for (int i = 0; i < words2.Length; i++)
{
sb.Append(words2[i]);
if(i < words2.Length - 1)
sb.Append(" ");
}
sb.Append(".");
#if TRACE
System.Diagnostics.Trace.WriteLine(sb.ToString());
#endif
Console.CursorLeft = 0;
Console.Write("Current phase: {0}", barrier.CurrentPhaseNumber);
if (String.CompareOrdinal(solution, sb.ToString()) == 0)
{
success = true;
Console.WriteLine("\r\nThe solution was found in {0}
attempts",
barrier.CurrentPhaseNumber);
}
});

static void Main(string[] args)


{
Thread t1 = new Thread(() => Solve(words1));
Thread t2 = new Thread(() => Solve(words2));
t1.Start();
t2.Start();
// Keep the console window open.
Console.ReadLine();
}

// Use Knuth-Fisher-Yates shuffle to randomly reorder each array.


// For simplicity, we require that both wordArrays be solved in the
same
// phase. Success of right or left side only is not stored and does
not count.
static void Solve(string[] wordArray)
{
while(success == false)
{
Random random = new Random();
for (int i = wordArray.Length - 1; i > 0; i--)
{
int swapIndex = random.Next(i + 1);
string temp = wordArray[i];
wordArray[i] = wordArray[swapIndex];
wordArray[swapIndex] = temp;
}
// We need to stop here to examine results
// of all thread activity. This is done in the post-phase
// delegate that is defined in the Barrier constructor.
barrier.SignalAndWait();
}
}
}
}

Barrier es un objeto que impide que las tareas individuales de una operación paralela continúen
hasta que todas las tareas alcancen la barrera. Es útil cuando una operación paralela tiene lugar

MCT: Luis Dueñas Pag 68 de 336


Manual de .NET Framework 4.5

en fases y cada fase requiere sincronización entre las tareas. En este ejemplo hay dos fases en la
operación. En la primera fase, cada tarea rellena su sección del búfer con datos. Cuando cada
tarea termina de rellenar su sección, la tarea señaliza a la barrera que está lista para continuar y
espera. Cuando todas las tareas han señalizado la barrera, se desbloquean y comienza la segunda
fase. La barrera es necesaria porque la segunda fase requiere que cada tarea obtenga acceso a
todos los datos generados hasta este momento. Sin la barrera, las primeras tareas en completarse
podrían intentar leer de búferes que otras tareas no han rellenado todavía. Es posible sincronizar
cualquier número de fases de esta manera.
1.4.12. SpinLock
La estructura SpinLock es una primitiva de sincronización de exclusión mutua y bajo nivel que
itera en ciclos mientras espera adquirir un bloqueo. En equipos con varios núcleos, en que los
períodos de tiempo de espera deben ser breves y en que la contención es mínima, el
comportamiento de SpinLock puede ser mejor que el de otros tipos de bloqueos. Sin embargo,
se recomienda utilizar SpinLock solamente si se determina mediante la generación de perfiles
que los métodos System.Threading.Monitor o Interlocked están reduciendo el rendimiento del
programa de forma significativa.
SpinLock puede proporcionar el intervalo de tiempo del subproceso aunque no haya adquirido
el bloqueo todavía. El motivo es evitar la inversión de la prioridad del subproceso y permitir el
progreso del recolector de elementos no utilizados. Cuando se utiliza SpinLock, conviene
asegurarse de que ningún subproceso mantenga el bloqueo durante más de un brevísimo
intervalo de tiempo, y que ningún subproceso se pueda bloquear mientras mantiene el bloqueo.
Como SpinLock es un tipo de valor, se debe pasar explícitamente por referencia si se pretende
que las dos copias hagan referencia al mismo bloqueo.
SpinLock admite un modo de seguimiento de subprocesos que se puede utilizar durante la fase
de desarrollo para ayudar a realizar el seguimiento del subproceso que está manteniendo el
bloqueo en un momento concreto. El modo de seguimiento de subprocesos es muy útil para la
depuración, pero se recomienda desactivarlo en la versión de lanzamiento del programa porque
puede reducir el rendimiento.
1.4.12.1. Cómo: Utilizar SpinLock para la sincronización de bajo nivel
En el siguiente ejemplo se muestra cómo utilizar SpinLock.
Ejemplo
En este ejemplo, la sección crítica realiza una cantidad de trabajo mínima, lo que lo convierte en
un buen candidato para SpinLock. Al aumentar ligeramente el trabajo aumenta el rendimiento
de SpinLock en comparación con un bloqueo estándar. Sin embargo, hay un punto en el que un
bloqueo por subproceso es más caro que un bloqueo estándar. Se puede usar la nueva
funcionalidad de generación de perfiles de simultaneidad de las Herramientas de generación de
perfiles para ver qué tipo de bloqueo proporciona mayor rendimiento en su programa.
class SpinLockDemo2
{
const int N = 100000;
static Queue<Data> _queue = new Queue<Data>();
static object _lock = new Object();
static SpinLock _spinlock = new SpinLock();

class Data
{
public string Name { get; set; }
public double Number { get; set; }
}

static void Main(string[] args)


{

MCT: Luis Dueñas Pag 69 de 336


Manual de .NET Framework 4.5

// First use a standard lock for comparison purposes.


UseLock();
_queue.Clear();
UseSpinLock();
Console.WriteLine("Press a key");
Console.ReadKey();
}

private static void UpdateWithSpinLock(Data d, int i)


{
bool lockTaken = false;
try
{
_spinlock.Enter(ref lockTaken);
_queue.Enqueue( d );
}
finally
{
if (lockTaken) _spinlock.Exit(false);
}
}

private static void UseSpinLock()


{
Stopwatch sw = Stopwatch.StartNew();
Parallel.Invoke(
() => {
for (int i = 0; i < N; i++)
{
UpdateWithSpinLock(new Data() { Name = i.ToString(),
Number = i }, i);
}
},
() => {
for (int i = 0; i < N; i++)
{
UpdateWithSpinLock(new Data() { Name = i.ToString(),
Number = i }, i);
}
}
);
sw.Stop();
Console.WriteLine("elapsed ms with spinlock: {0}",
sw.ElapsedMilliseconds);
}

static void UpdateWithLock(Data d, int i)


{
lock (_lock)
{
_queue.Enqueue(d);
}
}

private static void UseLock()


{
Stopwatch sw = Stopwatch.StartNew();
Parallel.Invoke(
() => {
for (int i = 0; i < N; i++)
{
UpdateWithLock(new Data() { Name = i.ToString(), Number =
i }, i);
}
},
() => {
for (int i = 0; i < N; i++)

MCT: Luis Dueñas Pag 70 de 336


Manual de .NET Framework 4.5

{
UpdateWithLock(new Data() { Name = i.ToString(), Number =
i }, i);
}
}
);
sw.Stop();
Console.WriteLine("elapsed ms with lock: {0}",
sw.ElapsedMilliseconds);
}
}

SpinLock puede resultar útil cuando un bloqueo en un recurso compartido no se va a mantener


durante mucho tiempo. En esos casos, en equipos con varios núcleos puede ser eficaz que el
subproceso bloqueado gire durante unos ciclos hasta que se libere el bloqueo. Al girar, el
subproceso no se bloquea, lo cual es un proceso que consume muchos recursos de la CPU.
SpinLock dejará de girar en ciertas condiciones para evitar el colapso de los procesadores
lógicos o la inversión de la prioridad en sistemas con Hyper-Threading.
En este ejemplo se usa la clase System.Collections.Generic.Queue<T>, que necesita
sincronización de usuarios para el acceso multiproceso. En aplicaciones destinadas a .NET
Framework 4, otra opción consiste en usar
System.Collections.Concurrent.ConcurrentQueue<T>, que no necesita ningún bloqueo de
usuario.
Fíjese en el uso de false (False en Visual Basic) en la llamada a Exit. Este valor proporciona el
mayor rendimiento. Especifique true (True) en las arquitecturas IA64 para utilizar la barrera de
memoria; de este modo, se vacían los búferes de escritura a fin de garantizar que el bloqueo está
disponible para otros subprocesos que van a salir.
1.4.12.2. Cómo: Habilitar el modo de seguimiento de subproceso en el
bloqueo SpinLock
System.Threading.SpinLock es un bloqueo de exclusión mutua de bajo nivel que se puede
utilizar para los escenarios que tienen los tiempos de espera muy breves. SpinLock no permite
volver a entrar. Después de que un subproceso entra en el bloqueo, debe salir correctamente de
él antes de poder volver a entrar. Normalmente, cualquier intento de volver a entrar en el
bloqueo produciría un interbloqueo; los interbloqueos pueden resultar muy difíciles de depurar.
Para ayudar en el desarrollo, System.Threading. SpinLock admite un modo de seguimiento de
subprocesos que produce una excepción cuando un subproceso intenta volver a entrar en un
bloqueo en que ya se encuentra. Esto permite localizar con mayor facilidad el punto en el que no
se ha salido correctamente del bloqueo. Para activar el modo de seguimiento de subprocesos,
puede utilizar el constructor SpinLock que toma un parámetro de entrada Boolean y pasar un
argumento de true. Después de completar las fases de desarrollo y pruebas, desactive modo de
seguimiento de subprocesos para mejorar el rendimiento.
Ejemplo
En el siguiente ejemplo se muestra el modo de seguimiento de subprocesos. Las líneas que salen
correctamente del bloqueo están marcadas como comentario para simular un error de código que
produce uno de los siguientes resultados:
 Se produce una excepción si SpinLock se creó utilizando un argumento de true (True en
Visual Basic).
 Se produce un interbloqueo si SpinLock se creó utilizando un argumento de false (False
en Visual Basic).
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using System.Threading;

MCT: Luis Dueñas Pag 71 de 336


Manual de .NET Framework 4.5

using System.Threading.Tasks;

namespace SpinLockDemo
{
public class SpinLockTest
{
// Specify true to enable thread tracking. This will cause
// exception to be thrown when the first thread attempts to reenter
the lock.
// Specify false to cause deadlock due to coding error below.
private static SpinLock _spinLock = new SpinLock(true);

static void Main()


{
Parallel.Invoke(
() => DoWork(),
() => DoWork(),
() => DoWork(),
() => DoWork()
);
Console.WriteLine("Press any key.");
Console.ReadKey();
}

public static void DoWork()


{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++)
{
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
// do work here protected by the lock
Thread.SpinWait(50000);
sb.Append(Thread.CurrentThread.ManagedThreadId);
sb.Append(" Entered-");
}
catch (LockRecursionException ex)
{
Console.WriteLine("Thread {0} attempted to reenter the
lock",
Thread.CurrentThread.ManagedThreadId);
throw;
}
finally
{
// INTENTIONAL CODING ERROR TO DEMONSTRATE THREAD
TRACKING!
// UNCOMMENT THE LINES FOR CORRECT SPINLOCK BEHAVIOR
// Commenting out these lines causes the same thread
// to attempt to reenter the lock. If the SpinLock was
// created with thread tracking enabled, the exception
// is thrown. Otherwise the spinlock deadlocks.
if (lockTaken)
{
// _spinLock.Exit(false);
// sb.Append("Exited ");
}
}
// Output for diagnostic display.
if(i % 4 != 0)
Console.Write(sb.ToString());
else
Console.WriteLine(sb.ToString());
sb.Clear();
}
}

MCT: Luis Dueñas Pag 72 de 336


Manual de .NET Framework 4.5

}
}

1.4.13. Tokens de cancelación


CancellationToken habilita la cancelación cooperativa entre subprocesos, elementos de trabajo
de grupo de subprocesos y objetos Task. Un objeto crea un token de cancelación utilizando
CancellationTokenSource y, a continuación, pasa el token de cancelación a cualquier número de
subprocesos u objetos que deben recibir una notificación de cancelación. El token no se puede
utilizar para iniciar la cancelación. Cuando el objeto propietario llama a Cancel en
CancellationTokenSource, la propiedad IsCancellationRequested en cada copia del token de
cancelación se establece en true. Los objetos que reciben la notificación pueden responder de la
manera adecuada.
1.4.14. SpinWait
System.Threading.SpinWait es un tipo de sincronización ligero que se puede utilizar en
escenarios de bajo nivel para evitar los costosos cambios de contexto y las transiciones del
kernel que se requieren para los eventos de kernel. En los equipos de varios núcleos, cuando no
se espera que un recurso sea retenido durante períodos de tiempo prolongados, puede ser más
eficaz que un subproceso en espera gire en modo usuario durante unas docenas o centenares de
ciclos y, a continuación, vuelva a intentar adquirir el recurso. Si el recurso está disponible
después de girar, habremos ahorrado varios miles de ciclos. Si el recurso todavía no está
disponible, solamente habremos gastado unos cuantos ciclos e igualmente podemos entrar en
una espera basada en kernel. Esta combinación de giro y espera se denomina en ocasiones
operación de espera de dos fases.
SpinWait se ha diseñado para utilizarlo en combinación con los tipos de .NET Framework que
contienen eventos de kernel, como ManualResetEvent. SpinWait también se puede utilizar por
sí solo para la funcionalidad del giro básica en un único programa.
SpinWait es más que un bucle vacío sin más. Se ha implementado con todo cuidado para
proporcionar un comportamiento de giro correcto para el caso general e iniciará por sí mismo
cambio de contexto si gira durante el tiempo suficiente (aproximadamente, el tiempo necesario
para una transición del kernel). Por ejemplo, en los equipos de un núcleo, SpinWait cede de
inmediato el intervalo de tiempo del subproceso, porque al girar se bloquea el progreso de
avance en todos los subprocesos. SpinWait también cede el paso incluso en equipos de varios
núcleos, para evitar que el subproceso en espera bloquee otros subprocesos de más prioridad o
el recolector de elementos no utilizados. Por consiguiente, si utiliza SpinWait en una operación
de espera de dos fases, recomendamos que invoque la espera del kernel antes de que SpinWait
inicie un cambio de contexto. SpinWait proporciona la propiedad NextSpinWillYield, que
puede comprobar antes de cada llamada a SpinOnce. Cuando la propiedad devuelve true, inicie
su propia operación de espera.
Si no está realizando una operación de espera de dos fases, sino que se limita a girar hasta que
se cumpla alguna condición, puede permitir que SpinWait realice sus cambios de contexto para
que se comporte correctamente en el entorno del sistema operativo Windows. El siguiente
ejemplo básico muestra un objeto SpinWait en una pila sin bloqueo. Si necesita una pila segura
para subprocesos de alto rendimiento, considere la posibilidad de usar
System.Collections.Concurrent.ConcurrentStack<T>.
public class LockFreeStack<T>
{
private volatile Node m_head;
private class Node { public Node Next; public T Value; }

public void Push(T item)


{
var spin = new SpinWait();
Node node = new Node { Value = item }, head;
while (true)

MCT: Luis Dueñas Pag 73 de 336


Manual de .NET Framework 4.5

{
head = m_head;
node.Next = head;
if (Interlocked.CompareExchange(ref m_head, node, head) == head)
break;
spin.SpinOnce();
}
}

public bool TryPop(out T result)


{
result = default(T);
var spin = new SpinWait();
Node head;
while (true)
{
head = m_head;
if (head == null) return false;
if (Interlocked.CompareExchange(ref m_head, head.Next, head) ==
head)
{
result = head.Value;
return true;
}
spin.SpinOnce();
}
}
}

1.4.14.1. Cómo: Usar SpinWait para implementar una operación de


espera de dos fases
En el siguiente ejemplo se muestra cómo utilizar un objeto System.Threading.SpinWait para
implementar una operación de espera de dos fases. En la primera fase, el objeto de
sincronización, Latch, gira durante unos ciclos mientras comprueba si el bloqueo está
disponible. En la segunda fase, si el bloqueo está disponible, el método Wait vuelve sin utilizar
System.Threading.ManualResetEvent para realizar su espera; de lo contrario, Wait realiza la
espera.
Ejemplo
En este ejemplo, se muestra una implementación muy básica de una primitiva de sincronización
de Latch. Puede utilizar esta estructura de datos mientras se prevé que los tiempos de espera
sean muy cortos. El único fin de este ejemplo es usarlo para realizar una demostración. Si
necesita una funcionalidad de tipo bloqueo temporal en el programa, considere la posibilidad de
utilizar System.Threading.ManualReset EventSlim.
#define LOGGING
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace CDS_Spinwait
{
class Latch
{
// 0 = unset, 1 = set
private volatile int m_state = 0;
private ManualResetEvent m_ev = new ManualResetEvent(false);

#if LOGGING
// For fast logging with minimal impact on latch behavior. Spin counts

MCT: Luis Dueñas Pag 74 de 336


Manual de .NET Framework 4.5

// greater than 20 might be encountered depending on machine config.


private int[] spinCountLog = new int[20];
private volatile int totalKernelWaits = 0;

public void PrintLog()


{
for (int i = 0; i < spinCountLog.Length; i++)
{
Console.WriteLine("Wait succeeded with spin count of {0} on
{1}
attempts", i, spinCountLog[i]);
}
Console.WriteLine("Wait used the kernel event on {0} attempts.",
totalKernelWaits);
Console.WriteLine("Logging complete");
}
#endif

public void Set()


{
// Trace.WriteLine("Set");
m_state = 1;
m_ev.Set();
}

public void Wait()


{
Trace.WriteLine("Wait timeout infinite");
Wait(Timeout.Infinite);
}

public bool Wait(int timeout)


{
// Allocated on the stack.
SpinWait spinner = new SpinWait();
Stopwatch watch;
while (m_state == 0)
{
// Lazily allocate and start stopwatch to track timeout.
watch = Stopwatch.StartNew();
// Spin only until the SpinWait is ready
// to initiate its own context switch.
if (!spinner.NextSpinWillYield)
{
spinner.SpinOnce();
}
// Rather than let SpinWait do a context switch now,
// we initiate the kernel Wait operation, because
// we plan on doing this anyway.
else
{
totalKernelWaits++;
// Account for elapsed time.
int realTimeout = timeout -
(int)watch.ElapsedMilliseconds;
// Do the wait.
if (realTimeout <= 0 || !m_ev.WaitOne(realTimeout))
{
Trace.WriteLine("wait timed out.");
return false;
}
}
}
// Take the latch.
m_state = 0;
// totalWaits++;

#if LOGGING

MCT: Luis Dueñas Pag 75 de 336


Manual de .NET Framework 4.5

spinCountLog[spinner.Count]++;
#endif
return true;
}
}

class Program
{
static Latch latch = new Latch();
static int count = 2;
static CancellationTokenSource cts = new CancellationTokenSource();

static void TestMethod()


{
while (!cts.IsCancellationRequested)
{
// Obtain the latch.
if (latch.Wait(50))
{
// Do the work. Here we vary the workload a slight amount
// to help cause varying spin counts in latch.
double d = 0;
if (count % 2 != 0)
{
d = Math.Sqrt(count);
}
count++;
// Release the latch.
latch.Set();
}
}
}

static void Main(string[] args)


{
// Demonstrate latch with a simple scenario:
// two threads updating a shared integer and
// accessing a shared StringBuilder. Both operations
// are relatively fast, which enables the latch to
// demonstrate successful waits by spinning only.
latch.Set();
// UI thread. Press 'c' to cancel the loop.
Task.Factory.StartNew(() =>
{
Console.WriteLine("Press 'c' to cancel.");
if (Console.ReadKey().KeyChar == 'c')
{
cts.Cancel();
}
});
Parallel.Invoke(
() => TestMethod(),
() => TestMethod(),
() => TestMethod()
);
#if LOGGING
latch.PrintLog();
#endif
Console.WriteLine("\r\nPress the Enter Key.");
Console.ReadLine();
}
}
}

El bloqueo temporal utiliza el objeto SpinWait para girar en su posición solo hasta que la
llamada siguiente a SpinOnce haga que SpinWait genere el intervalo de tiempo del subproceso.
A partir de ese momento, el bloqueo temporal produce su propio cambio de contexto llamando a
WaitOne en ManualResetEvent y pasando el resto del valor del tiempo de espera.

MCT: Luis Dueñas Pag 76 de 336


Manual de .NET Framework 4.5

El resultado del registro muestra con qué frecuencia Latch pudo aumentar el rendimiento
adquiriendo el bloqueo sin utilizar ManualResetEvent.

MCT: Luis Dueñas Pag 77 de 336


Manual de .NET Framework 4.5

2. Modelos para la programación asincrónica


.NET Framework ofrece tres patrones para realizar operaciones asincrónicas:
 El modelo de programación asincrónica (APM) (también denominado patrón
IAsyncResult), donde las operaciones asincrónicas requieren los métodos Begin y End
(por ejemplo, BeginWrite y EndWrite para las operaciones de escritura asincrónicas).
No se recomienda este patrón en desarrollos nuevos.
 El patrón asincrónico basado en eventos (EAP), que requiere un método que tiene el
sufijo Async y uno o más eventos, tipos delegado de controlador de eventos y tipos
derivados de EventArg. EAP apareció por primera vez en .NET Framework 2.0. No es
recomendable en desarrollos nuevos.
 Modelo asincrónico basado en tareas (TAP), que usa un solo método para representar el
inicio y la finalización de una operación asincrónica. TAP apareció por primera vez en
.NET Framework 4 y es el enfoque recomendado para la programación asincrónica de
.NET Framework.
Comparar patrones
Para una comparación rápida de cómo modelan los tres patrones las operaciones asincrónicas,
considere un método Read que lee cierta cantidad de datos en un búfer proporcionado
empezando desde un desplazamiento especificado:
public class MyClass
{
public int Read(byte [] buffer, int offset, int count);
}

El equivalente de APM de este método expondría los métodos BeginRead y EndRead:


public class MyClass
{
public IAsyncResult BeginRead(
byte [] buffer, int offset, int count,
AsyncCallback callback, object state);
public int EndRead(IAsyncResult asyncResult);
}

El equivalente de EAP expondría el siguiente conjunto de tipos y de miembros:


public class MyClass
{
public void ReadAsync(byte [] buffer, int offset, int count);
public event ReadCompletedEventHandler ReadCompleted;
}

El equivalente de TAP expondría el siguiente método ReadAsync:


public class MyClass
{
public Task<int> ReadAsync(byte [] buffer, int offset, int count);
}

Para una visión completa de TAP, APM y EAP, consulte los vínculos proporcionados en la
siguiente sección.
2.1. Modelo asincrónico basado en tareas (TAP)
El modelo asincrónico basado en tareas (TAP) se basa en los tipos Task y Task<TResult> del
espacio de nombres System.Threading.Tasks, que se usan para representar operaciones
asincrónicas arbitrarias. TAP es el modelo asincrónico de diseño recomendado para el nuevo
desarrollo.
Nombres, parámetros y tipos de valores devueltos

MCT: Luis Dueñas Pag 78 de 336


Manual de .NET Framework 4.5

TAP usa un solo método para representar el inicio y la finalización de una operación
asincrónica. Esto contrasta con el modelo de programación asincrónica (APM o IAsyncResult),
que necesita métodos Begin y End y, con el modelo asincrónico basado en eventos (EAP), que
necesita un método que tenga el sufijo Async y también necesita uno o más eventos, tipos de
delegado de controlador de eventos y tipos derivados de EventArg. Los métodos asincrónicos de
TAP incluyen el sufijo Async después del nombre de la operación; por ejemplo, GetAsync para
una operación Get. Si va a agregar un método de TAP a una clase que ya contiene ese nombre
de método con el sufijo Async, use el sufijo TaskAsync en su lugar. Por ejemplo, si la clase ya
tiene un método GetAsync, use el nombre GetTaskAsync.
El método de TAP devuelve Task o Task<TResult>, en función de si el método sincrónico
correspondiente devuelve void o un tipo TResult.
Los parámetros de un método de TAP deben coincidir con los parámetros de su homólogo
sincrónico y se deben proporcionar en el mismo orden. Sin embargo, los parámetros out y ref
están exentos de esta regla y se deben evitar completamente. En su lugar, los datos que se
hubieran devuelto con un parámetro out o ref se deben devolver como parte del tipo TResult
devuelto por Task<TResult> y deben usar una tupla o una estructura de datos personalizada para
incluir varios valores. Los métodos que están dedicados exclusivamente a la creación,
manipulación o combinación de tareas (donde el intento asincrónico del método está claro en el
nombre del método o en el nombre del tipo al que el método pertenece) no necesitan seguir este
modelo de nombres; esos métodos se conocen a menudo como combinadores (también
denominados elementos de combinación). Los ejemplos de combinadores incluyen WhenAll y
WhenAny, y se describen en la sección que describe los combinadores integrados basados en
tareas titulados Utilizar los elementos de combinación basados en tareas integradas del artículo
Utilizar el modelo asincrónico basado en tareas.
Iniciar una operación asincrónica
Un método asincrónico basado en TAP puede hacer una pequeña cantidad de trabajo
sincrónicamente, como validar argumentos e iniciar la operación asincrónica, antes de que
devuelva la tarea resultante. El trabajo sincrónico debe reducirse al mínimo de modo que el
método asincrónico pueda volver rápidamente. Entre las razones para un retorno rápido se
incluyen las siguientes:
 Los métodos asincrónicos se pueden invocar desde subprocesos de la interfaz de usuario
(UI) y cualquier trabajo sincrónico de ejecución prolongada puede dañar la capacidad
de respuesta de la aplicación.
 Se pueden iniciar varios métodos asincrónicos simultáneamente. Por tanto, cualquier
trabajo de ejecución prolongada en la parte sincrónica de un método asincrónico puede
retrasar el inicio de otras operaciones asincrónicas, lo que reduce las ventajas de la
simultaneidad.
En algunos casos, la cantidad de trabajo necesario para completar la operación es menor que la
cantidad de trabajo necesario para iniciar la operación de forma asincrónica. La lectura de una
secuencia donde la operación de lectura se puede satisfacer mediante datos que ya están
almacenados en búfer en la memoria es un ejemplo de este escenario. En casos como este, la
operación puede completarse sincrónicamente y puede devolver una tarea que ya se ha
completado.
Excepciones
Un método asincrónico debe generar una excepción fuera de la llamada de método asincrónico
solo como respuesta a un error de uso. Los errores de uso nunca deben producirse en código de
producción. Por ejemplo, si al pasar una referencia nula (Nothing en Visual Basic) como uno de
los argumentos del método se produce un estado de error (representado normalmente por una
excepción ArgumentNull Exception), puede modificar el código de llamada para asegurarse de
que nunca se pase una referencia nula. Para todos los demás errores, las excepciones que se
producen cuando se ejecuta un método asincrónico deben asignarse a la tarea devuelta, aunque

MCT: Luis Dueñas Pag 79 de 336


Manual de .NET Framework 4.5

el método asincrónico se complete sincrónicamente antes de que se devuelva la tarea.


Normalmente, una tarea contiene como máximo una excepción. Sin embargo, si la tarea
representa varias operaciones (por ejemplo, WhenAll), se pueden asociar varias excepciones a
una única tarea.
Entorno de destino
Cuando implementa un método de TAP, puede determinar dónde se produce la ejecución
asincrónica. Puede elegir ejecutar la carga de trabajo en el grupo de subprocesos, implementarla
mediante E/S asincrónica (sin enlazarse a un subproceso para la mayoría de la ejecución de la
operación), ejecutarla en un subproceso concreto (como el subproceso de la interfaz de usuario)
o usar cualquier número de contextos posibles. Un método de TAP puede no tener nada que
ejecutar y puede devolver simplemente Task, que representa la existencia de una condición en
otra parte del sistema (por ejemplo, una tarea que representa los datos que llegan a una
estructura de datos en cola). El llamador del método de TAP puede bloquear la espera hasta que
se complete el método de TAP esperando sincrónicamente la tarea resultante o puede ejecutar
código adicional (de continuación) cuando la operación asincrónica se complete. El creador del
código de continuación tiene control sobre lo que ese código ejecuta. Puede crear el código de
continuación explícitamente, mediante métodos de la clase Task (por ejemplo, ContinueWith) o
implícitamente, usando la compatibilidad con lenguaje sobre las continuaciones (por ejemplo,
await en C#, Await en Visual Basic, AwaitValue en F#).
Estado de la tarea
La clase Task proporciona un ciclo de vida para las operaciones asincrónicas y ese ciclo se
representa mediante la enumeración TaskStatus. Para admitir los casos extremos de tipos que se
derivan de Task y Task<TResult>, y para admitir la separación de la construcción de la
programación, la clase Task expone un método Start. Las tareas creadas por los constructores
públicos Task se denominan tareas en frío, porque inician su ciclo de vida en el estado Created
no programado y solo se programan cuando se llama a Start en estas instancias. Todas las demás
tareas inician su ciclo de vida en un estado activo, lo que significa que las operaciones
asincrónicas que representan ya se han iniciado y su estado de la tarea es un valor de
enumeración distinto de TaskStatus.Created. Todas las tareas que se devuelven de métodos de
TAP deben estar activas. Si un método de TAP usa internamente el constructor de una tarea para
crear instancias de la tarea que se va a devolver, el método de TAP debe llamar a Start en el
objeto Task antes de devolverlo. Los consumidores de un método de TAP pueden suponer con
seguridad que la tarea devuelta está activa y no deben intentar llamar a Start en ningún Task que
se devuelve de un método de TAP. La llamada a Start en una tarea activa produce una
excepción InvalidOperationException.
Cancelación (opcional)
En TAP, la cancelación es opcional tanto para los implementadores de método asincrónico
como para los consumidores de este método. Si una operación permite la cancelación, expone
una sobrecarga del método asincrónico que acepta un token de cancelación (instancia de
CancellationToken). Por convención, el parámetro se denomina cancellationToken.
public Task ReadAsync(byte [] buffer, int offset, int count, CancellationToken
cancellationToken);

La operación asincrónica supervisa este token para las solicitudes de cancelación. Si


recibe una solicitud de cancelación, puede elegir admitir esa solicitud y cancelar la
operación. Si la solicitud de cancelación hace que el trabajo finalice prematuramente, el
método de TAP devuelve una tarea que finaliza en el estado Canceled; no hay ningún
resultado disponible y no se produce ninguna excepción. El estado Canceled se
considera un estado final (completado) para una tarea, junto con los estados Faulted y
RanToCompletion. Por tanto, si una tarea está en el estado Canceled, su propiedad
IsCompleted devuelve true. Cuando una tarea se completa en el estado Canceled,

MCT: Luis Dueñas Pag 80 de 336


Manual de .NET Framework 4.5

cualquier continuación registrada con la tarea se programa o se ejecuta, a menos que se


especificara la opción de continuación como NotOnCanceled para rechazar la
continuación. Cualquier código que espera de forma asincrónica una tarea cancelada
mediante el uso de características del lenguaje sigue ejecutándose pero recibe un objeto
OperationCanceled Exception o una excepción derivada del mismo. El código que se
bloquea sincrónicamente en espera de la tarea mediante métodos como Wait y WaitAll
también continúa ejecutándose con una excepción.
Si un token de cancelación ha solicitado la cancelación antes de que se llame al método de TAP
que acepta ese token, el método de TAP debe devolver una tarea Canceled. Sin embargo, si se
solicita la cancelación mientras la operación asincrónica se ejecuta, la operación asincrónica no
necesita aceptar la solicitud de cancelación. La tarea devuelta debe finalizar en el estado
Canceled sólo si la operación termina como resultado de la solicitud de cancelación. Si se
solicita la cancelación pero aún se produce un resultado o una excepción, la tarea debe finalizar
en el estado RanToCompletion o Faulted. Para los métodos asincrónicos usados por un
desarrollador que desea la cancelación principalmente, no tiene que proporcionar una
sobrecarga que no acepte un token de cancelación. Para los métodos que no pueden cancelarse,
no proporcione sobrecargas que acepten un token de cancelación; esto ayuda a indicar al
llamador si el método de destino es realmente cancelable. El código de consumidor que no
desea la cancelación puede llamar a un método que acepta un objeto CancellationToken y
proporciona None como valor del argumento. None es funcionalmente equivalente al objeto
CancellationToken predeterminado.
Informe de progreso (opcional)
Algunas operaciones asincrónicas se benefician de proporcionar notificaciones de progreso; se
suelen usar para actualizar una interfaz de usuario con información sobre el progreso de la
operación asincrónica. En TAP, el progreso se controla a través de una interfaz IProgress<T>, la
cual se pasa al método asincrónico como parámetro denominado progress. Proporcionar la
interfaz de progreso cuando se llama al método asincrónico ayuda a eliminar condiciones de
carrera resultantes de un uso incorrecto (es decir, cuando los controladores de eventos
registrados incorrectamente después del inicio de la operación pueden perder actualizaciones).
Lo que es más importante, la interfaz de progreso admite implementaciones diferentes de
progreso, según determina el código de uso. Por ejemplo, el código de uso puede encargarse
solo de la última actualización de progreso, puede que desee almacenar en búfer todas las
actualizaciones o invocar una acción para cada actualización, o puede que desee controlar si se
calculan las referencias de la invocación en un subproceso determinado. Todas estas opciones se
pueden lograr utilizando otra implementación de la interfaz, personalizada según las
necesidades particulares del consumidor. Como ocurre con la cancelación, las implementaciones
de TAP deben proporcionar un parámetro IProgress<T> solo si la API admite notificaciones de
progreso. Por ejemplo, si el método ReadAsync anteriormente mencionado en este artículo
puede informar del progreso intermedio en forma de número de bytes leídos hasta el momento,
la devolución de llamada de progreso puede ser una interfaz IProgress<T>:
public Task ReadAsync(byte[] buffer, int offset, int count, IProgress<long>
progress);

Si un método FindFilesAsync devuelve una lista de todos los archivos que reúnen un patrón
particular de búsqueda, la devolución de progreso puede proporcionar una estimación del
porcentaje de trabajo completado así como el conjunto de resultados parciales. Puede hacerlo
con una tupla:
public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(string pattern,
IProgress<Tuple<double,ReadOnlyCollection<List<FileInfo>>>> progress);

o con un tipo de datos que es específico de la API:


public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(string pattern,
IProgress<FindFilesProgressInfo> progress);

MCT: Luis Dueñas Pag 81 de 336


Manual de .NET Framework 4.5

En este último caso, el tipo de datos especial suele tener el sufijo ProgressInfo.
Si las implementaciones de TAP proporcionan sobrecargas que aceptan un parámetro progress,
deben permitir que el argumento sea null, en cuyo caso no se notifica ningún progreso. Las
implementaciones de TAP deben notificar el progreso al objeto Progress<T> sincrónicamente,
lo cual permite al método asincrónico proporcionar rápidamente el progreso y hacer que el
consumidor del progreso determine cómo y dónde es mejor controlar la información. Por
ejemplo, la instancia de progreso puede elegir hacerse con las devoluciones de llamada y
provocar eventos en un contexto capturado de sincronización.
Implementaciones IProgress<t>
.NET Framework 4.5 proporciona una única implementación de IProgress<T>: Progress<T>. Se
declara la clase Progress<T> como se indica a continuación:
public class Progress<T> : IProgress<T>
{
public Progress();
public Progress(Action<T> handler);
protected virtual void OnReport(T value);
public event EventHandler<T> ProgressChanged;
}

Una instancia de Progress<T> expone un evento ProgressChanged, que se provoca cada vez que
la operación asincrónica informe de una actualización de progreso. El evento ProgressChanged
se genera en el objeto SynchronizationContext que se capturó cuando se creó la instancia de
Progress<T>. Si no había ningún contexto de sincronización disponible, se usa un contexto
predeterminado destinado al grupo de subprocesos. Los controladores pueden registrarse con
este evento. Un único controlador también puede ser proporcionado al constructor Progress<T>
por comodidad y se comporta como un controlador de eventos para el evento ProgressChanged.
Las actualizaciones de progreso se generan de forma asincrónica para evitar retrasar la
operación asincrónica mientras los controladores de eventos se ejecutan. Otra implementación
IProgress<T> podría elegir aplicarse para diferentes semánticas.
Elegir las sobrecargas que se van a proporcionar
Si una implementación TAP utiliza CancellationToken opcional y los parámetros opcionales
IProgress <T>, podría requerir hasta cuatro sobrecargas:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…, CancellationToken cancellationToken,
IProgress<T> progress);

Sin embargo, muchas implementaciones TAP no proporcionan ni la cancelación ni las


capacidades de progreso, por lo que requieren un único método:
public Task MethodNameAsync(…);

Si una implementación de TAP admite cancelación o progreso pero no ambos, puede


proporcionar dos sobrecargas:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
// … or …
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, IProgress<T> progress);

Si una implementación TAP admite cancelación y progreso, puede exponer las cuatro
sobrecargas. Sin embargo, puede proporcionar sólo los dos siguientes:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…,
CancellationToken cancellationToken, IProgress<T> progress);

MCT: Luis Dueñas Pag 82 de 336


Manual de .NET Framework 4.5

Para compensar las dos combinaciones intermedias que faltan, los desarrolladores pueden pasar
None o un CancellationToken predeterminado para el parámetro cancellationToken y null para
el parámetro progress.
Si se espera que cada uso del método TAP admita cancelación o progreso, puede omitir las
sobrecargas que no acepten el parámetro pertinente.
Si se decide exponer varias sobrecargas para crear la cancelación o para el progreso opcional,
las sobrecargas que no admitan cancelación o progreso deben comportarse como si pasaran
None para cancelación o null para el progreso en la sobrecarga que admite ambas.
2.1.1. Implementar el modelo asincrónico basado en tareas
Puede implementar el modelo asincrónico basado en tareas (TAP) de tres maneras: mediante los
compiladores de C# y Visual Basic en Visual Studio, manualmente o mediante una combinación
del compilador y métodos manuales. En las siguientes secciones se describe cada método con
detalle. Puede usar el modelo de TAP para implementar operaciones asincrónicas enlazadas a
cálculos y enlazadas a E/S; en la sección Cargas de trabajo se describe cada tipo de operación.
Generar métodos de TAP
Usar los compiladores
En Visual Studio 2012 y .NET Framework 4.5, cualquier método que tenga la palabra clave
async (Async en Visual Basic) se considera un método asincrónico, y los compiladores de C# y
Visual Basic realizan las transformaciones necesarias para implementar el método de forma
asincrónica mediante TAP. Un método asincrónico debe devolver un objeto Task o
Task<TResult>. En el último caso, el cuerpo de la función debe devolver TResult y el
compilador asegura que este resultado está disponible a través del objeto de la tarea resultante.
Del mismo modo, se calculan en la tarea de salida las referencias de cualquier excepción no
controlada dentro del cuerpo del método y esto hace que la tarea resultante finalice en el estado
Faulted. La excepción es cuando un objeto OperationCanceledException (o un tipo derivado) no
está controlado, en cuyo caso la tarea resultante finaliza en el estado Canceled.
Generar métodos de TAP manualmente
Puede implementar el modelo de TAP manualmente para tener un mejor control sobre la
implementación. El compilador se basa en la superficie pública expuesta del espacio de nombres
System.Threading.Tasks y los tipos auxiliares del espacio de nombres
System.Runtime.CompilerServices. Para implementar TAP personalmente, cree un objeto
TaskCompletionSource<TResult>, realice la operación asincrónica y, cuando se complete,
llame al método SetResult, SetException o SetCanceled, o a la versión Try de uno de estos
métodos. Cuando implementa un método de TAP manualmente, debe completar la tarea
resultante cuando la operación asincrónica representada se complete. Por ejemplo:
public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset
, int count, object state)
{
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, ar =>
{
try { tcs.SetResult(stream.EndRead(ar)); }
catch (Exception exc) { tcs.SetException(exc); }
}, state);
return tcs.Task;
}

Enfoque híbrido
Puede resultar útil implementar el modelo de TAP manualmente pero delegar la lógica básica de
la implementación en el compilador. Por ejemplo, quizás desee usar el enfoque híbrido cuando
desee comprobar argumentos fuera de un método asincrónico generado por el compilador de

MCT: Luis Dueñas Pag 83 de 336


Manual de .NET Framework 4.5

forma que las excepciones puedan salir del llamador directo del método en lugar de exponerse a
través del objeto Task:
public Task<int> MethodAsync(string input)
{
if (input == null) throw new ArgumentNullException("input");
return MethodAsyncInternal(input);
}

private async Task<int> MethodAsyncInternal(string input)


{
… // code that uses await
}

Otro caso donde es útil esa delegación es cuando implementa la optimización de acceso rápido y
desea devolver una tarea almacenada en memoria caché.
Cargas de trabajo
Puede implementar operaciones asincrónicas enlazadas a cálculos y enlazadas a E/S como
métodos de TAP. Sin embargo, cuando los métodos de TAP se exponen públicamente desde una
biblioteca, solo se deben suministrar para cargas de trabajo que impliquen operaciones
enlazadas a E/S (también pueden implican cálculos, pero no deben ser estrictamente de cálculo).
Si un método está enlazado a cálculos puramente, solo se debe exponer como una
implementación sincrónica; el código que lo usa puede elegir si ajustar una invocación de ese
método sincrónico en una tarea para descargar el trabajo en otro subproceso o para lograr el
paralelismo.
Tareas enlazadas a cálculos
La clase Task es idónea para representar operaciones intensivas en cálculos. De forma
predeterminada, se beneficia de la compatibilidad especial dentro de la clase ThreadPool para
proporcionar una ejecución eficaz, y también proporciona un buen control sobre cuándo, dónde
y cómo se ejecutan los cálculos asincrónicos. Puede generar tareas enlazadas a cálculos de las
maneras siguientes:
 En .NET Framework 4, use el método TaskFactory.StartNew, que acepta un delegado
(normalmente Action<T> o Func<TResult>) que se va a ejecutar de forma asincrónica.
Si proporciona un delegado de Action<T>, el método devuelve un objeto Task que
representa la ejecución asincrónica de ese delegado. Si proporciona un delegado de
Func<TResult>, el método devuelve un objeto Task<TResult>. Las sobrecargas del
método StartNew aceptan un token de cancelación (CancellationToken), las opciones de
creación de la tarea (TaskCreationOptions) y un programador de tareas
(TaskScheduler), todo lo cual proporciona un control específico sobre la programación
y la ejecución de la tarea. Una instancia de generador que tiene como destino el
programador de tareas actual está disponible como una propiedad estática (Factory) de
la clase Task; por ejemplo: Task.Factory.StartNew(…).
 En .NET Framework 4.5, use el método estático Task.Run como acceso directo a
TaskFactory.StartNew. Puede usar Run para iniciar fácilmente una tarea enlazada a
cálculos destinada al grupo de subprocesos. En .NET Framework 4.5, este es el
mecanismo preferido para iniciar una tarea enlazada a cálculos. Use StartNew
directamente solo cuando desee un mayor control sobre la tarea.
 Use los constructores del tipo Task o el método Start si desea generar y programar la
tarea por separado. Los métodos públicos solo deben devolver tareas que ya se han
iniciado.
 Use las sobrecargas del método Task.ContinueWith. Este método crea una nueva tarea
que se programa cuando se completa otra tarea. Algunas de las sobrecargas de
ContinueWith aceptan un token de cancelación, opciones de continuación y un
programador de tareas para tener un mejor control sobre la programación y la ejecución
de la tarea de continuación.

MCT: Luis Dueñas Pag 84 de 336


Manual de .NET Framework 4.5

 Utilice los métodos TaskFactory.ContinueWhenAll y TaskFactory.ContinueWhenAny.


Estos métodos crean una nueva tarea que se programa cuando se completa todo o
cualquier conjunto de tareas proporcionado. Estos métodos también proporcionan
sobrecargas para controlar la programación y la ejecución de estas tareas.
En las tareas enlazadas a cálculos, el sistema puede evitar la ejecución de una tarea programada
si recibe una solicitud de cancelación antes de que comience la ejecución de la tarea. Por tanto,
si proporciona un token de cancelación (objeto CancellationToken), puede pasar ese token al
código asincrónico que supervisa el token. También puede proporcionar el token a uno de los
métodos mencionados previamente como StartNew o Run para que el runtime de Task también
supervise el token.
Por ejemplo, considere un método asincrónico que presenta una imagen. El cuerpo de la tarea
puede sondear el token de cancelación para que el código pueda salir pronto si llega una
solicitud de cancelación durante la representación. Además, si la solicitud de cancelación llega
antes de que se inicie la representación, deseará evitar la operación de representación:
internal Task<Bitmap> RenderAsync(
ImageData data, CancellationToken cancellationToken)
{
return Task.Run(() =>
{
var bmp = new Bitmap(data.Width, data.Height);
for(int y=0; y<data.Height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
for(int x=0; x<data.Width; x++)
{
… // render pixel [x,y] into bmp
}
}
return bmp;
}, cancellationToken);
}

Las tareas enlazadas a cálculos finalizan en un estado Canceled si se cumple al menos una de las
condiciones siguientes:
 Llega una solicitud de cancelación a través del objeto CancellationToken, que se
proporciona como argumento al método de creación (por ejemplo, StartNew o Run)
antes de que la tarea cambie al estado Running.
 Una excepción OperationCanceledException no está controlada dentro del cuerpo de
esta tarea, esa excepción contiene el mismo objeto CancellationToken que se pasa a la
tarea y ese token muestra que se solicitó la cancelación.
Si hay otra excepción no controlada en el cuerpo de la tarea, la tarea finaliza en el estado
Faulted y cualquier intento de esperar en la tarea u obtener acceso a su resultado produce una
excepción.
Tareas enlazadas a E/S
Para crear una tarea que no se deba respaldar directamente por un subproceso durante toda su
ejecución, use el tipo TaskCompletionSource<TResult>. Este tipo expone una propiedad Task
que devuelve una instancia asociada de Task<TResult>. El ciclo de vida de esta tarea se
controla mediante métodos TaskCompletionSource<TResult> como SetResult, SetException,
SetCanceled y sus variantes de TrySet.
Suponga que desea crear una tarea que se completará después de un período de tiempo
especificado. Por ejemplo, puede que desee retrasar una actividad en la interfaz de usuario. La
clase System.Threading.Timer ya proporciona la capacidad de invocar de forma asincrónica un
delegado después de un período de tiempo especificado, y mediante
TaskCompletionSource<TResult> puede colocar un objeto Task<TResult> delante del
temporizador, por ejemplo:

MCT: Luis Dueñas Pag 85 de 336


Manual de .NET Framework 4.5

public static Task<DateTimeOffset> Delay(int millisecondsTimeout)


{
TaskCompletionSource<DateTimeOffset> tcs = null;
Timer timer = null;
timer = new Timer(delegate
{
timer.Dispose();
tcs.TrySetResult(DateTimeOffset.UtcNow);
}, null, Timeout.Infinite, Timeout.Infinite);
tcs = new TaskCompletionSource<DateTimeOffset>(timer);
timer.Change(millisecondsTimeout, Timeout.Infinite);
return tcs.Task;
}

A partir de .NET Framework 4.5, el método Task.Delay se proporciona con este propósito y
puede usarlo dentro de otro método asincrónico, por ejemplo, para implementar un bucle
asincrónico de sondeo:
public static async Task Poll(Uri url, CancellationToken cancellationToken,
IProgress<bool> progress)
{
while(true)
{
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
bool success = false;
try
{
await DownloadStringAsync(url);
success = true;
}
catch { /* ignore errors */ }
progress.Report(success);
}
}

La clase TaskCompletionSource<TResult> no tiene ningún homólogo no genérico. Sin


embargo, Task<TResult> deriva de Task, por lo que puede usar el objeto genérico
TaskCompletionSource <TResult> para los métodos enlazados a E/S que simplemente
devuelven una tarea. Para ello, puede usar un origen con un TResult ficticio (Boolean es una
buena opción predeterminada, pero si le preocupa que el usuario de Task lo convierta en tipos
inferiores a un objeto Task<TResult>, puede usar un tipo TResult privado en su lugar). Por
ejemplo, el método Delay del ejemplo anterior devuelve la hora actual junto con desplazamiento
resultante (Task<DateTimeOffset>). Si el valor de resultado es innecesario, el método podría
codificarse en su lugar como sigue (observe el cambio del tipo de valor devuelto y el cambio del
argumento a TrySetResult):
public static Task<bool> Delay(int millisecondsTimeout)
{
TaskCompletionSource<bool> tcs = null;
Timer timer = null;
timer = new Timer(delegate
{
timer.Dispose();
tcs.TrySetResult(true);
}, null, Timeout.Infinite, Timeout.Infinite);
tcs = new TaskCompletionSource<bool>(timer);
timer.Change(millisecondsTimeout, Timeout.Infinite);
return tcs.Task;
}

Tareas enlazadas a cálculos y enlazadas a E/S mixtas


Los métodos asincrónicos no se limitan solo a operaciones enlazadas a cálculos o enlazadas a
E/S pero pueden representar una combinación de ambas. De hecho, se suelen combinar varias
operaciones asincrónicas en operaciones mixtas mayores. Por ejemplo, el método RenderAsync
del ejemplo anterior realizaba una operación intensiva en cálculos para presentar una imagen

MCT: Luis Dueñas Pag 86 de 336


Manual de .NET Framework 4.5

basada en imageData de entrada. Este imageData podría proceder de un servicio Web al que
tiene acceso de forma asincrónica:
public async Task<Bitmap> DownloadDataAndRenderImageAsync(
CancellationToken cancellationToken)
{
var imageData = await DownloadImageDataAsync(cancellationToken);
return await RenderAsync(imageData, cancellationToken);
}

En este ejemplo también se muestra cómo se puede incluir en un subproceso un único token de
cancelación mediante varias operaciones asincrónicas.
2.1.2. Utilizar el modelo asincrónico basado en tareas
Cuando se utiliza el patrón asincrónico basado en tareas (TAP) para ejecutar operaciones
asincrónicas, se pueden utilizar devoluciones de llamada para conseguir esperas sin bloqueos.
Para las tareas, esto se logra con métodos como Task.ContinueWith. La compatibilidad
asincrónica basada en lenguajes oculta devoluciones de llamada al permitir que las operaciones
asincrónicas sean esperadas dentro del flujo de control normal y el código generado por el
compilador proporcione esta misma compatibilidad a nivel de API.
Suspender la ejecución con Await
Empezando con .NET Framework 4.5, se puede utilizar la palabra clave await (Referencia de
C#) en C# y Await (Operador) (Visual Basic) en Visual Basic para esperar asincrónicamente a
Task y a los objetos Task<TResult>. Cuando se está esperando una Task, la expresión await es
de tipo void. Cuando se está esperando una Task<TResult>, la expresión await es de tipo
TResult. Una expresión await debe aparecer en el cuerpo de un método asincrónico. Para
obtener más información sobre la compatibilidad del lenguaje C# y Visual Basic en .NET
Framework 4.5, consulte las especificaciones del lenguaje C# y Visual Basic.
Más en detalle, la función de espera instala una devolución de llamada en la tarea mediante una
continuación. Esta devolución de llamada reanuda el método asincrónico en el momento de
suspensión. Cuando se reanuda el método asincrónico, si la operación en espera se completa
correctamente y fue Task<TResult>, se devuelve su TResult. Si Task o Task<TResult>
esperado terminó en el estado Canceled, se lanza una excepción OperationCanceledException.
Si Task o Task<TResult> esperado terminó en el estado Faulted, se lanza la excepción que
produjo el error. Task puede darse como resultado de varias excepciones, pero sólo una de estas
excepciones se propaga. Sin embargo, la propiedad Task.Exception devuelve una excepción
AggregateException que contiene todos los errores.
Si un contexto de sincronización (objetoSynchronizationContext) está asociado al subproceso
que estaba ejecutando el método asincrónico en el momento de la suspensión (por ejemplo, si la
propiedad SynchronizationContext.Current no es null), el método asincrónico se reanuda en el
mismo contexto de sincronización utilizando el método Post del contexto. De lo contrario, se
basa en el programador de tareas (objetoTaskScheduler) que estaba en curso en el momento de
la suspensión. Normalmente, éste es el programador de tareas predeterminado
(TaskScheduler.Default), que tiene como destino el grupo de subprocesos. Este programador de
tareas determina si la operación asincrónica esperada debe reanudarse donde se completó o si la
reanudación debe programarse. El programador predeterminado suele permitir que la
continuación se ejecute en el subproceso que la operación en espera completó.
Cuando se invoca un método asincrónico, se ejecuta sincrónicamente el cuerpo de la función
hasta la primera expresión en espera de una instancia esperable que aún no se ha completado,
punto en el que la invocación vuelve al llamador. Si el método asincrónico no devuelve void, se
devuelve un objeto Task o Task<TResult> para representar el cálculo en curso. En un método
asincrónico que no es void, si se encuentra una instrucción de retorno, o se alcanza el final del
cuerpo del método, la tarea se completa en el estado final RanToCompletion. Si una excepción
no controlada hace que el control deje el cuerpo del método asincrónico, la tarea termina en el

MCT: Luis Dueñas Pag 87 de 336


Manual de .NET Framework 4.5

estado Faulted. Si la excepción es OperationCanceled Exception, la tarea finaliza en el estado


Canceled. De esta manera, el resultado o la excepción se publican finalmente.
Hay muchas variaciones importantes de este comportamiento. Por razones de rendimiento, si
una tarea ya se ha completado cuando se espera, el control no se cede y la función sigue
ejecutándose. Además, volver al contexto original no siempre es el comportamiento deseado y
se puede modificar; esto se describe con más detalle en la sección siguiente.
Configurar la suspension y reanudación con Yield y ConfigureAwait
Varios son los métodos que proporcionan mayor control sobre la ejecución de un método
asincrónico. Por ejemplo, se puede utilizar el método Task.Yield para incluir un punto de
producción en el método asincrónico:
public class Task : …
{
public static YieldAwaitable Yield();

}

Esto es equivalente a enviar de forma asincrónica o a programar de nuevo el contexto actual.


Task.Run(async delegate
{
for(int i=0; i<1000000; i++)
{
await Task.Yield(); // fork the continuation into a separate work item
...
}
});

También se puede utilizar el método Task.ConfigureAwait para un mejor control sobre la


suspensión y la reanudación de un método asincrónico. Como se ha mencionado anteriormente,
de manera predeterminada el contexto actual se captura en el momento en que se suspende un
método asincrónico y ese contexto capturado se utiliza para invocar la continuación del método
asincrónico en la reanudación. En muchos casos, éste es el comportamiento exacto que se desea.
En otros casos, puede no interesar saber el contexto de continuación y se puede mejorar el
rendimiento si se evita que tales elementos vuelvan al contexto original. Para habilitar esto,
utilice el método Task.ConfigureAwait para informar a la operación en espera de que no capture
ni reanude el contexto, sino que continúe la ejecución siempre que la operación asincrónica en
espera se complete:
await someTask.ConfigureAwait(continueOnCapturedContext:false);

Cancelar una operación asincrónica


Empezando con .NET Framework 4, los métodos TAP que admiten cancelación proporcionan al
menos una sobrecarga que acepta un token de cancelación (objeto CancellationToken ).
El token de cancelación se crea desde el origen de tokens de cancelación
(objetoCancellationTokenSource ). La propiedad Token del origen devuelve el token de
cancelación que se notificará cuando se llame al método Cancel del origen. Por ejemplo, si se
desea descargar una sola página web y poder cancelar la operación, se crea un objeto
CancellationTokenSource, se pasa el token al método TAP y después se llama al método Cancel
cuando esté preparado para cancelar la operación:
var cts = new CancellationTokenSource();
string result = await DownloadStringAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();

Para cancelar llamadas asincrónicas múltiples, se puede pasar el mismo token a todas las
invocaciones:
var cts = new CancellationTokenSource();

MCT: Luis Dueñas Pag 88 de 336


Manual de .NET Framework 4.5

IList<string> results = await Task.WhenAll(from url in urls select Downloa


dStringAsync(url, cts.Token));
// at some point later, potentially on another thread

cts.Cancel();

O bien, se puede pasar el mismo token a un subconjunto selectivo de operaciones:


var cts = new CancellationTokenSource();
byte [] data = await DownloadDataAsync(url, cts.Token);
await SaveToDiskAsync(outputPath, data, CancellationToken.None);
… // at some point later, potentially on another thread
cts.Cancel();

Las solicitudes de cancelación se pueden iniciar desde cualquier subproceso.


Se puede pasar el valor CancellationToken.None a cualquier método que acepte un token de
cancelación para indicar que la cancelación nunca se solicitará. Esto hace que la propiedad
CancellationToken. CanBeCanceled devuelva false y consiguientemente el método llamado
pueda optimizarse. Con fines de prueba, también se puede pasar un token precancelado de
cancelación del que se creen instancias utilizando el constructor que acepta un valor booleano
para indicar si el token debería comenzar en un estado ya cancelado o no cancelable.
Este tratamiento de la cancelación tiene varias ventajas:
 Se puede pasar el mismo token de cancelación a cualquier número de operaciones
asincrónicas o sincrónicas.
 La misma solicitud de cancelación puede extenderse a cualquier número de oyentes.
 El desarrollador de la API asincrónica tiene completo control de si la cancelación puede
ser solicitada y cuándo puede tener lugar.
 El consumidor de la API puede determinar selectivamente las llamadas asincrónicas a
las que las solicitudes de cancelación se propagarán.
Supervisar el progreso
Algunos métodos asincrónicos presentan su progreso a través de una interfaz de progreso que
pasa por el método asincrónico. Por ejemplo, piense en una función que de forma asincrónica
descarga una cadena de texto y en el camino provoca actualizaciones de progreso que incluyen
el porcentaje de descarga que se ha completado hasta el momento. Este método se puede utilizar
en una aplicación de Windows Presentation Foundation (WPF) como se indica a continuación:
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
try
{
txtResult.Text = await DownloadStringAsync(txtUrl.Text,
new Progress<int>(p => pbDownloadProgress.Value = p));
}
finally { btnDownload.IsEnabled = true; }
}

Utilizar los elementos de combinación basados en tareas integradas


El espacio de nombres System.Threading.Tasks incluye varios métodos para componer y
trabajar con tareas.
Task.Run
La clase Task incluye varios métodos Run que permiten con facilidad librarse de trabajo como
un Task o Task<TResult> al grupo de subprocesos, por ejemplo:
public async void button1_Click(object sender, EventArgs e)
{
textBox1.Text = await Task.Run(() =>
{

MCT: Luis Dueñas Pag 89 de 336


Manual de .NET Framework 4.5

// … do compute-bound work here


return answer;
});
}

Algunos de estos métodos Run tales como la sobrecarga de Task.Run(Func<Task>) existen


como una versión reducida del método TaskFactory.StartNew. Otras sobrecargas, como
Task.Run(Func<Task>), permiten que la espera se utilice dentro del trabajo descargado, por
ejemplo:
public async void button1_Click(object sender, EventArgs e)
{
pictureBox1.Image = await Task.Run(async() =>
{
using(Bitmap bmp1 = await DownloadFirstImageAsync())
using(Bitmap bmp2 = await DownloadSecondImageAsync())
return Mashup(bmp1, bmp2);
});
}

Dichas sobrecargas son lógicamente equivalentes a utilizar el método TaskFactory.StartNew en


conjunción con el método de extensión Unwrap en la biblioteca de tareas paralelas.
Task.FromResult
Para esos escenarios en los que los datos pueden estar ya disponibles y simplemente necesitan
ser devueltos desde un método que devuelve tareas subido en un FromResult<TResult>, el
método Task<TResult> puede ser utilizado:
public Task<int> GetValueAsync(string key)
{
int cachedValue;
return TryGetCachedValue(out cachedValue) ?
Task.FromResult(cachedValue) :
GetValueAsyncInternal();
}

private async Task<int> GetValueAsyncInternal(string key)


{

}

Task.WhenAll
Utilice el método WhenAll de forma asincrónica para atender varias operaciones asincrónicas
que se representan como tareas. El método tiene múltiples sobrecargas que admiten un conjunto
de tareas no genéricas o un conjunto no uniforme de tareas genéricas (por ejemplo, tareas que
esperan asincrónicamente varias operaciones de retorno tipo void, o tareas que esperan
asincrónicamente múltiples métodos de valores de retorno en los que cada valor puede ser de
diferente tipo) así como también admiten un conjunto uniforme de tareas genéricas (tales como
esperar asincrónicamente varios métodos de retorno TResult).
Digamos que se desea enviar un correo electrónico a varios clientes Se puede solapar el envío
de mensajes para que no se tenga que esperar a que un mensaje se complete antes de enviar el
siguiente. También se puede averiguar qué operaciones de envío se han completado y si se ha
producido algún error.
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);

Este código no controla explícitamente las excepciones que se pueden producir, sino que
permite que las excepciones se propaguen fuera de await en la tarea resultante de WhenAll. Para
controlar las excepciones, se puede usar el siguiente código:
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
try
{

MCT: Luis Dueñas Pag 90 de 336


Manual de .NET Framework 4.5

await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
...
}

En este caso, si una operación asincrónica da error, todas las excepciones se consolidarán en una
excepción AggregateException, que se almacena en Task que se devuelve desde el método
WhenAll. Sin embargo, sólo una de esas excepciones se propaga por la palabra clave await . Si
se desean examinar todas las excepciones, se puede volver a escribir el código anterior como
sigue:
Task [] asyncOps = (from addr in addrs select SendMailAsync(addr)).ToArray();
try
{
await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
foreach(Task faulted in asyncOps.Where(t => t.IsFaulted))
{
… // work with faulted and faulted.Exception
}
}

Considere el caso de cómo descargar varios archivos de la web de forma asincrónica. En este
caso, todas las operaciones asincrónicas tienen tipos de resultado homogéneos y el acceso a los
resultados es simple:
string [] pages = await Task.WhenAll(from url in urls select
DownloadStringAsync(url));

Como en el caso anterior que se devuelve void, las mismas técnicas de control de excepciones
se pueden utilizar aquí:
Task [] asyncOps = (from url in urls select
DownloadStringAsync(url)).ToArray();
try
{
string [] pages = await Task.WhenAll(asyncOps);
...
}
catch(Exception exc)
{
foreach(Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
{
… // work with faulted and faulted.Exception
}
}

Task.WhenAny
Se puede usar el método WhenAny para esperar de forma asincrónica a una de las múltiples
operaciones asincrónicas representadas como tareas a completar. Este método tiene cuatro usos
principales:
 Redundancia: Efectuar múltiples veces una operación y seleccionar la que se complete
primero (por ejemplo: conectar con múltiples servicios web de cotización de valores
que producirán un solo resultado y seleccionar el que se complete más rápidamente).
 Intercalado: Iniciar múltiples operaciones y esperar a que todas se completen, pero
procesarlas a medida que se van completando.
 Regulación: Permitir que las operaciones adicionales comiencen cuando las otras se
completen. Esto es una extensión del escenario de intercalado.
 Rescate anticipado: Por ejemplo, una operación representada por la tarea t1 se puede
agrupar en una tarea WhenAny con otra tarea t2 y se puede esperar la tarea WhenAny.

MCT: Luis Dueñas Pag 91 de 336


Manual de .NET Framework 4.5

La tarea t2 podría representar un tiempo de espera, o cancelación, o alguna otra señal


que haga que la tarea WhenAny se complete antes de completarse t1.
Redundancia
Considere un caso en el que se desea tomar una decisión sobre si adquirir un valor o no. Hay
varios servicios web de recomendación de valores en los que se confía, pero dependiendo de la
carga diaria, cada uno de los servicios puede resultar bastante lento en momentos diferentes. Se
puede usar el método WhenAny para recibir una notificación cuando cualquier operación se
complete:
var recommendations = new List<Task<bool>>()
{
GetBuyRecommendation1Async(symbol),
GetBuyRecommendation2Async(symbol),
GetBuyRecommendation3Async(symbol)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
if (await recommendation) BuyStock(symbol);

A diferencia de WhenAll, que devuelve los resultados empaquetados de todas las tareas que se
completan correctamente, WhenAny devuelve la tarea que se completó. Si una tarea da error, es
importante saber dónde está el error, y si acaba correctamente, es importante saber a qué tarea
está asociado el valor devuelto. Por consiguiente, es necesario tener acceso al resultado de la
tarea devuelta, o esperar aún más, como se muestra en este ejemplo.
Al igual que con WhenAll, se necesita poder alojar excepciones. Debido a que se recibe de
vuelta la tarea completada, se puede esperar que la tarea devuelta tenga los errores propagados,
y hacer try/catch sobre estos correctamente; por ejemplo:
Task<bool> [] recommendations = …;
while(recommendations.Count > 0)
{
Task<bool> recommendation = await Task.WhenAny(recommendations);
try
{
if (await recommendation) BuyStock(symbol);
break;
}
catch(WebException exc)
{
recommendations.Remove(recommendation);
}
}

Además, aún en el caso de que una primera tarea se complete correctamente, las tareas
posteriores pueden producir errores. En este punto, son varias las opciones para abordar las
excepciones: se puede esperar a que se completen todas las tareas lanzadas, en cuyo caso se
puede usar el método WhenAll; o bien, se puede decidir que todas las excepciones son
importantes y se deben registrar. Para ello, se pueden utilizar directamente las continuaciones
para recibir una notificación cuando las tareas estén completadas de forma asincrónica:
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => { if (t.IsFaulted) Log(t.Exception); });
}

O bien
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}

o incluso:

MCT: Luis Dueñas Pag 92 de 336


Manual de .NET Framework 4.5

private static async void LogCompletionIfFailed(IEnumerable<Task> tasks)


{
foreach(var task in tasks)
{
try { await task; }
catch(Exception exc) { Log(exc); }
}
}

LogCompletionIfFailed(recommendations);

Finalmente, se puede desear cancelar todas las operaciones restantes.


var cts = new CancellationTokenSource();
var recommendations = new List<Task<bool>>()
{
GetBuyRecommendation1Async(symbol, cts.Token),
GetBuyRecommendation2Async(symbol, cts.Token),
GetBuyRecommendation3Async(symbol, cts.Token)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
cts.Cancel();
if (await recommendation) BuyStock(symbol);

Intercalado
Considere el caso en el que se descarguen imágenes de la web y se procese cada imagen (por
ejemplo, agregar la imagen a un control de UI). Se tiene que hacer el procesado secuencialmente
en el subproceso de UI, pero se desea que las imágenes se descarguen de forma simultánea en la
medida de lo posible. Además, no se desea esperar a agregar las imágenes a la UI hasta que se
hayan descargado todas (se desean agregar a medida que se completan):
List<Task<Bitmap>> imageTasks = (from imageUrl in urls select
GetBitmapAsync(imageUrl)).ToList();
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch{}
}

El mismo intercalado también se puede aplicar a un escenario que implica procesamiento de


cómputo intensivo en el ThreadPool de imágenes descargadas; por ejemplo:
List<Task<Bitmap>> imageTasks = (from imageUrl in urls select
GetBitmapAsync(imageUrl)
.ContinueWith(t => ConvertImage(t.Result)).ToList();
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch{}
}

Limitación de peticiones
Piense en el ejemplo de intercalado, excepto que el usuario ahora está descargando tantas
imágenes que las descargas necesitan que se limiten explícitamente; por ejemplo, sólo se desea

MCT: Luis Dueñas Pag 93 de 336


Manual de .NET Framework 4.5

realizar un número específico de descargas simultáneamente. Para lograr esto, se puede iniciar
un subconjunto de las operaciones asincrónicas. A medida que las operaciones se completan, se
pueden iniciar operaciones adicionales para reemplazarlas:
const int CONCURRENCY_LEVEL = 15;
Uri [] urls = …;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while(nextIndex < CONCURRENCY_LEVEL && nextIndex < urls.Length)
{
imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
nextIndex++;
}
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch(Exception exc) { Log(exc); }
if (nextIndex < urls.Length)
{
imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
nextIndex++;
}
}

Rescate temprano
Considere que se está esperando asincrónicamente que una operación se complete mientras
simultáneamente se responde a la solicitud de cancelación de un usuario (por ejemplo, un
usuario hace clic en el botón de cancelación). En el siguiente código se muestra este escenario:
private CancellationTokenSource m_cts;

public void btnCancel_Click(object sender, EventArgs e)


{
if (m_cts != null) m_cts.Cancel();
}

public async void btnRun_Click(object sender, EventArgs e)


{
m_cts = new CancellationTokenSource();
btnRun.Enabled = false;
try
{
Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text);
await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
if (imageDownload.IsCompleted)
{
Bitmap image = await imageDownload;
panel.AddImage(image);
}
else imageDownload.ContinueWith(t => Log(t));
}
finally { btnRun.Enabled = true; }
}

private static async Task UntilCompletionOrCancellation(


Task asyncOp, CancellationToken ct)
{
var tcs = new TaskCompletionSource<bool>();
using(ct.Register(() => tcs.TrySetResult(true)))
await Task.WhenAny(asyncOp, tcs.Task);
return asyncOp;

MCT: Luis Dueñas Pag 94 de 336


Manual de .NET Framework 4.5

Esta implementación vuelve a habilitar la interfaz de usuario en cuanto decidamos salir pero no
cancela las operaciones asincrónicas subyacentes. Otra alternativa sería cancelar las operaciones
pendientes cuando decidamos salir, aunque no se restaure la interfaz de usuario hasta que las
operaciones se completen efectivamente, posiblemente por una finalización temprana debido a
una solicitud de cancelación:
private CancellationTokenSource m_cts;

public async void btnRun_Click(object sender, EventArgs e)


{
m_cts = new CancellationTokenSource();
btnRun.Enabled = false;
try
{
Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text, m_cts.Token);
await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
Bitmap image = await imageDownload;
panel.AddImage(image);
}
catch(OperationCanceledException) {}
finally { btnRun.Enabled = true; }
}

Otro ejemplo de rescate temprano implica usar el método WhenAny junto con el método Delay
como se discute en la siguiente sección.
Task.Delay
Se puede usar el método Delay para introducir pausas en una ejecución asincrónica del método.
Esto es útil para muchos tipos de funcionalidad, como compilar los bucles de sondeo y retrasar
el control de datos proporcionados por el usuario durante un período de tiempo predeterminado.
El método Delay puede también resultar útil junto con WhenAny para implementar tiempos de
espera.
Si una tarea que forma parte de una operación asincrónica mayor (por ejemplo, un servicio web
ASP.NET) tarda demasiado tiempo en completarse, la operación global podría sufrir,
especialmente si no llega a completarse nunca. Por esta razón, es importante poder tener
tiempos muertos cuando se espera en una operación asincrónica. Los métodos sincrónicos Wait,
WaitAll y WaitAny aceptan valores de tiempo de espera, pero los métodos
ContinueWhenAll/WhenAny y el mencionado previamente WhenAll/ WhenAny
correspondientes no. En su lugar, se puede usar Delay y WhenAny en conjunto para
implementar un tiempo de espera.
Por ejemplo, en la aplicación de interfaz de usuario, digamos que se desea descargar una imagen
y deshabilitar la interfaz de usuario mientras se descarga dicha imagen. Sin embargo, si la
descarga lleva mucho tiempo, se desea rehabilitar la interfaz de usuario y la descarga debe
descartarse:
public async void btnDownload_Click(object sender, EventArgs e)
{
btnDownload.Enabled = false;
try
{
Task<Bitmap> download = GetBitmapAsync(url);
if (download == await Task.WhenAny(download, Task.Delay(3000)))
{
Bitmap bmp = await download;
pictureBox.Image = bmp;
status.Text = “Downloaded”;
}
else
{
pictureBox.Image = null;

MCT: Luis Dueñas Pag 95 de 336


Manual de .NET Framework 4.5

status.Text = “Timed out”;


var ignored = download.ContinueWith(t => Trace(“Task finally
completed”));
}
}
finally { btnDownload.Enabled = true; }
}

Lo mismo ocurre con las descargas múltiples, puesto que WhenAll devuelve una tarea:
public async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.Enabled = false;
try
{
Task<Bitmap[]> downloads =
Task.WhenAll(from url in urls select GetBitmapAsync(url));
if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
{
foreach(var bmp in downloads) panel.AddImage(bmp);
status.Text = “Downloaded”;
}
else
{
status.Text = “Timed out”;
downloads.ContinueWith(t => Log(t));
}
}
finally { btnDownload.Enabled = true; }
}

Compilar elementos de combinación basados en tareas


Dado que una tarea es capaz de representar completamente una operación asincrónica y
proporcionar funciones sincrónicas y asincrónicas para combinar con la operación, recuperar los
resultados, etc., es posible compilar las bibliotecas útiles de los combinadores que constituyen
las tareas para compilar modelos más grandes. Como se ha visto en la sección anterior, .NET
Framework incluye varios combinadores integrados, pero también se pueden compilar los
propios. Las siguientes secciones ofrecen varios ejemplos de posibles métodos de elementos de
combinación.
RetryOnFault
En muchas situaciones, se puede desear reintentar una operación si un intento previo da error.
Para el código sincrónico, se podría compilar un método auxiliar como RetryOnFault en el
siguiente ejemplo para lograrlo:
public static T RetryOnFault<T>(
Func<T> function, int maxTries)
{
for(int i=0; i<maxTries; i++)
{
try { return function(); }
catch { if (i == maxTries-1) throw; }
}
return default(T);
}

Se puede compilar un método auxiliar casi idéntico para las operaciones asincrónicas
implementadas con el TAP y que devuelvan tareas:
public static async Task<T> RetryOnFault<T>(
Func<Task<T>> function, int maxTries)
{
for(int i=0; i<maxTries; i++)
{
try { return await function().ConfigureAwait(false); }
catch { if (i == maxTries-1) throw; }

MCT: Luis Dueñas Pag 96 de 336


Manual de .NET Framework 4.5

}
return default(T);
}

Se puede usar este combinador para encapsular reintentos en la lógica de la aplicación; por
ejemplo:
// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
() => DownloadStringAsync(url), 3);

Se podría extender la función RetryOnFault más adelante: Por ejemplo, la función podría
aceptar otro Func<Task> que se invocará entre reintentos para determinar cuándo probar la
operación otra vez; por ejemplo:
public static async Task<T> RetryOnFault<T>(
Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
for(int i=0; i<maxTries; i++)
{
try { return await function().ConfigureAwait(false); }
catch { if (i == maxTries-1) throw; }
await retryWhen().ConfigureAwait(false);
}
return default(T);
}

Se podría usar la función como se muestra para esperar un segundo antes de reintentar la
operación:
// Download the URL, trying up to three times in case of failure,
// and delaying for a second between retries
string pageContents = await RetryOnFault(
() => DownloadStringAsync(url), 3, () => Task.Delay(1000));

NeedOnlyOne
A veces, se puede aprovechar la redundancia para mejorar la latencia y las posibilidades de
éxito de una operación. Piense en los múltiples servicios web que proporcionan cotizaciones,
pero que, durante varias horas del día cada uno de los servicios puede proporcionar diferentes
niveles de calidad y de tiempos de respuesta. Para tratar con estas fluctuaciones, se pueden
mandar solicitudes a todos los servicios web y tan pronto como se reciba una respuesta de una,
cancelar las solicitudes restantes. Se puede implementar una función auxiliar para facilitar la
implementación de este patrón común para iniciar múltiples operaciones, esperar alguna y
después de cancelar el resto: La función NeedOnlyOne en el siguiente ejemplo muestra este
escenario:
public static async Task<T> NeedOnlyOne(
params Func<CancellationToken,Task<T>> [] functions)
{
var cts = new CancellationTokenSource();
var tasks = (from function in functions
select function(cts.Token)).ToArray();
var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
cts.Cancel();
foreach(var task in tasks)
{
var ignored = task.ContinueWith(
t => Log(t), TaskContinuationOptions.OnlyOnFaulted);
}
return completed;
}

Esta función se puede utilizar como se indica a continuación:


double currentPrice = await NeedOnlyOne(
ct => GetCurrentPriceFromServer1Async(“msft”, ct),
ct => GetCurrentPriceFromServer2Async(“msft”, ct),

MCT: Luis Dueñas Pag 97 de 336


Manual de .NET Framework 4.5

ct => GetCurrentPriceFromServer3Async(“msft”, ct));

Operaciones de intercalado
Existe un potencial problema de rendimiento si se usa el método WhenAny para admitir un
escenario de intercalado cuando se utilizan conjuntos de tareas muy grandes. Cada llamada a
WhenAny da como resultado una continuación que se registra con cada tarea. Para un número N
de tareas, esto da como resultado O(N2) continuaciones creadas sobre el tiempo de vida de la
operación de intercalado. Si se está trabajando con un gran número de tareas, se puede usar un
combinador (Interleaved en el siguiente ejemplo) para solucionar el problema de rendimiento:
static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
var inputTasks = tasks.ToList();
var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
select new TaskCompletionSource<T>()).ToList();
int nextTaskIndex = -1;
foreach (var inputTask in inputTasks)
{
inputTask.ContinueWith(completed =>
{
var source = sources[Interlocked.Increment(ref nextTaskIndex)];
if (completed.IsFaulted)
source.TrySetException(completed.Exception.InnerExceptions);
else if (completed.IsCanceled)
source.TrySetCanceled();
else
source.TrySetResult(completed.Result);
}, CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
return from source in sources
select source.Task;
}

Se podría utilizar el combinador para procesar los resultados de tareas a medida que éstas se
completan; por ejemplo:
IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
int result = await task;

}

WhenAllOrFirstException
En ciertos escenarios de dispersión/recolección, quizás se desee esperar a todas las tareas de un
conjunto, a menos que una de ellas dé error, en cuyo caso se desee detener la espera tan pronto
se produzca la excepción. Se puede conseguir esto con un método combinador como
WhenAllOrFirstException en el siguiente ejemplo:
public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
var inputs = tasks.ToList();
var ce = new CountdownEvent(inputs.Count);
var tcs = new TaskCompletionSource<T[]>();
Action<Task> onCompleted = (Task completed) =>
{
if (completed.IsFaulted)
tcs.TrySetException(completed.Exception.InnerExceptions);
if (ce.Signal() && !tcs.Task.IsCompleted)
tcs.TrySetResult(inputs.Select(t => t.Result).ToArray());
};
foreach (var t in inputs) t.ContinueWith(onCompleted);
return tcs.Task;
}

MCT: Luis Dueñas Pag 98 de 336


Manual de .NET Framework 4.5

Compilando estructuras de datos basadas en tareas


Además de la capacidad de compilar elementos de combinación basados en tareas
personalizadas, el hecho de tener una estructura de datos en Task y Task<TResult> que
representa tanto los resultados de una operación asincrónica y la sincronización necesaria para
unirlo hace que sea un tipo muy eficaz para compilar las estructuras de datos personalizadas que
se utilizarán en escenarios asincrónicos.
AsyncCache
Un aspecto importante de una tarea es que se puede distribuir a todos los consumidores que la
esperan, que registran continuaciones con ella, que obtienen su resultado o excepciones (en el
caso de Task<TResult>), etc. Esto hace que Task y Task<TResult> sean perfectamente aptos
para su uso en una infraestructura de almacenamiento en caché asincrónica. He aquí un ejemplo
de una pequeña pero eficaz memoria caché asincrónica compilada sobre Task<TResult>:
public class AsyncCache<TKey, TValue>
{
private readonly Func<TKey, Task<TValue>> _valueFactory;
private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;

public AsyncCache(Func<TKey, Task<TValue>> valueFactory)


{
if (valueFactory == null) throw new ArgumentNullException("loader");
_valueFactory = valueFactory;
_map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
}

public Task<TValue> this[TKey key]


{
get
{
if (key == null) throw new ArgumentNullException("key");
return _map.GetOrAdd(key, toAdd =>
new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
}
}
}

La clase AsyncCache<TKey,TValue> acepta como delegado de su constructor una función que


toma un TKey y devuelve un Task<TResult>. Cualquier valor al que se ha obtenido acceso
previamente desde la memoria caché se almacena en el diccionario interno y AsyncCache
asegura que sólo una tarea se genera por clave, aun cuando se obtenga acceso a la memoria
caché simultáneamente.
Por ejemplo, se puede construir una memoria caché para páginas web descargadas:
private AsyncCache<string,string> m_webPages =
new AsyncCache<string,string>(DownloadStringAsync);

Se puede usar esta memoria caché en métodos asincrónicos siempre que se necesiten los
contenidos de una página web. La clase AsyncCache garantiza que se estén descargando tan
pocas páginas como sea posible y guarda en memoria caché los resultados.
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
try
{
txtContents.Text = await m_webPages["http://www.microsoft.com"];
}
finally { btnDownload.IsEnabled = true; }
}

MCT: Luis Dueñas Pag 99 de 336


Manual de .NET Framework 4.5

AsyncProducerConsumerCollection
También se pueden utilizar tareas para compilar las estructuras de datos con el fin de coordinar
las actividades asincrónicas entre sí. Piense en uno de los patrones paralelos clásicos de diseño:
productor/consumidor. En este patrón, los productores generan datos que los consumidores
adquieren y los productores y consumidores pueden ejecutarlos en paralelo. Por ejemplo, el
consumidor procesa el elemento 1, que ha sido generado previamente por un productor que está
ahora produciendo el elemento 2. Para el patrón productor/consumidor, siempre se necesita
alguna estructura de datos para almacenar el trabajo creado por los productores de forma que se
puedan notificar los nuevos datos a los consumidores y éstos puedan encontrarlos cuándo estén
disponibles.
A continuación se muestra una simple estructura de datos compilada sobre una tarea que habilita
los métodos asincrónicos que se utilizarán como productores y consumidores:
public class AsyncProducerConsumerCollection<T>
{
private readonly Queue<T> m_collection = new Queue<T>();
private readonly Queue<TaskCompletionSource<T>> m_waiting =
new Queue<TaskCompletionSource<T>>();

public void Add(T item)


{
TaskCompletionSource<T> tcs = null;
lock (m_collection)
{
if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
else m_collection.Enqueue(item);
}
if (tcs != null) tcs.TrySetResult(item);
}

public Task<T> Take()


{
lock (m_collection)
{
if (m_collection.Count > 0)
{
return Task.FromResult(m_collection.Dequeue());
}
else
{
var tcs = new TaskCompletionSource<T>();
m_waiting.Enqueue(tcs);
return tcs.Task;
}
}
}
}

Con esa estructura de datos en su lugar, se puede escribir código como el siguiente:
private static AsyncProducerConsumerCollection<int> m_data = …;

private static async Task ConsumerAsync()
{
while(true)
{
int nextItem = await m_data.Take();
ProcessNextItem(nextItem);
}
}

private static void Produce(int data)
{
m_data.Add(data);
}

MCT: Luis Dueñas Pag 100 de 336


Manual de .NET Framework 4.5

El espacio de nombres System.Threading.Tasks.Dataflow incluye el tipo BufferBlock<T>, que


se puede usar de una manera similar, pero sin tener que construir un tipo de colecciones
personalizado:
private static BufferBlock<int> m_data = …;

private static async Task ConsumerAsync()
{
while(true)
{
int nextItem = await m_data.ReceiveAsync();
ProcessNextItem(nextItem);
}
}

private static void Produce(int data)
{
m_data.Post(data);
}
Nota
El espacio de nombres System.Threading.Tasks.Dataflow está disponible en el .NET
Framework 4.5 por medio de NuGet. Para instalar el ensamblado que contiene el espacio de
nombres System.Threading. Tasks.Dataflow , abra su proyecto en Visual Studio 2012, elija
Administrar paquetes NuGet desde el menú del proyecto y busque en línea el paquete
Microsoft.Tpl.Dataflow.

2.1.3. Interoperabilidad con los modelos asincrónicos y otros tipos


.NET Framework 1.0 presentó el modelo IAsyncResult, conocido también como Modelo de
programación asincrónica (APM), o el modelo de Begin/End. .NET Framework 2.0 agregó
Publicar datos de símbolos. A partir de .NET Framework 4, Modelo asincrónico basado en
tareas (TAP) reemplaza a APM y EAP, pero proporciona la capacidad de compilar fácilmente
rutinas de migración de los modelos anteriores:
 Tareas y APM (de APM a TAP o de TAP a APM)
 Tareas y EAP
 Tareas y controladores de espera (de identificadores de espera a TAP o de TAP a
identificadores de espera)
Tareas y el modelo de programación asincrónica (APM)
De APM a TAP
Como el modelo Modelo de programación asincrónica (APM) es muy estructurado, es muy fácil
compilar un contenedor para exponer una implementación de APM como una implementación
de TAP. De hecho, .NET Framework 4 incluye rutinas auxiliares en forma de sobrecargas del
método FromAsync para proporcionar esta traducción.
Considere la clase Stream y sus métodos BeginRead/EndRead, que representan el equivalente
de APM al método sincrónico Read:
public int Read(byte [] buffer, int offset, int count);

public IAsyncResult BeginRead(byte [] buffer, int offset, int count,
AsyncCallback callback, object state);
public int EndRead(IAsyncResult asyncResult);

Puede usar el método FromAsync para implementar un contenedor de TAP para este método
como sigue:
public static Task<int> ReadAsync(
this Stream stream, byte [] buffer, int offset, int count)
{
if (stream == null) throw new ArgumentNullException(“stream”);

MCT: Luis Dueñas Pag 101 de 336


Manual de .NET Framework 4.5

return Task<int>.Factory.FromAsync(stream.BeginRead, stream.EndRead,


buffer, offset, count, null);
}

Esta implementación es similar a la siguiente:


public static Task<int> ReadAsync(
this Stream stream, byte [] buffer, int offset, int count)
{
if (stream == null) throw new ArgumentNullException("stream");
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, iar =>
{
try { tcs.TrySetResult(stream.EndRead(iar)); }
catch(OperationCanceledException) { tcs.TrySetCanceled(); }
catch(Exception exc) { tcs.TrySetException(exc); }
}, null);
return tcs.Task;
}

De TAP a APM
Si la infraestructura existente espera el modelo APM, también deseará realizar una
implementación de TAP y usarla donde se espere una implementación de APM. Como las tareas
se pueden componer y la clase Task implementa IAsyncResult, puede usar una función auxiliar
sencilla para ello. En el código siguiente se usa una extensión de la clase Task<TResult>, pero
puede usar una función casi idéntica para las tareas no genéricas.
public static IAsyncResult AsApm<T>(
this Task<T> task, AsyncCallback callback, object state)
{
if (task == null) throw new ArgumentNullException(“task”);
var tcs = new TaskCompletionSource<T>(state);
task.ContinueWith(t =>
{
if (t.IsFaulted) tcs.TrySetException(t.Exception.InnerExceptions)
else if (t.IsCanceled) tcs.TrySetCanceled();
else tcs.TrySetResult(t.Result);
if (callback != null) callback(tcs.Task);
}, TaskScheduler.Default);
return tcs.Task;
}

Ahora, considere un caso donde tiene la implementación siguiente de TAP:


public static Task<string> DownloadStringAsync(Uri url);

y desea proporcionar esta implementación de APM:


public IAsyncResult BeginDownloadString(
Uri url, AsyncCallback callback, object state);
public string EndDownloadString(IAsyncResult asyncResult);

En el código siguiente se muestra una migración a APM:


public IAsyncResult BeginDownloadString(
Uri url, AsyncCallback callback, object state)
{
return DownloadStringAsync(url).AsApm(callback, state);
}

public string EndDownloadString(IAsyncResult asyncResult)


{
return ((Task<string>)asyncResult).Result;
}

Tareas y el modelo asincrónico basado en eventos (EAP)


Ajustar una implementación de Publicar datos de símbolos es más complejo que ajustar un
modelo de APM, ya que el modelo de EAP tiene más variación y menos estructura que el

MCT: Luis Dueñas Pag 102 de 336


Manual de .NET Framework 4.5

modelo de APM. Para demostrarlo, en el código siguiente se ajusta el método


DownloadStringAsync. DownloadStringAsync acepta un URI, genera el evento
DownloadProgressChanged durante la descarga para informar de varias estadísticas sobre el
progreso y genera el evento DownloadStringCompleted cuando termina. El resultado final es
una cadena que incluye el contenido de la página en el URI especificado.
public static Task<string> DownloadStringAsync(Uri url)
{
var tcs = new TaskCompletionSource<string>();
var wc = new WebClient();
wc.DownloadStringCompleted += (s,e) =>
{
if (e.Error != null) tcs.TrySetException(e.Error);
else if (e.Cancelled) tcs.TrySetCanceled();
else tcs.TrySetResult(e.Result);
};
wc.DownloadStringAsync(url);
return tcs.Task;
}

Tareas e identificadores de espera


De identificadores de espera a TAP
Aunque los identificadores de espera no implementan un modelo asincrónico, los
desarrolladores avanzados pueden usar la clase WaitHandle y el método
ThreadPool.RegisterWaitForSingleObject para las notificaciones asincrónicas cuando se
establece un identificador de espera. Puede ajustar RegisterWaitForSingleObject para habilitar
una alternativa basada en tareas en cualquier espera sincrónica en un identificador de espera:
public static Task WaitOneAsync(this WaitHandle waitHandle)
{
if (waitHandle == null) throw new ArgumentNullException("waitHandle");
var tcs = new TaskCompletionSource<bool>();
var rwh = ThreadPool.RegisterWaitForSingleObject(waitHandle,
delegate { tcs.TrySetResult(true); }, null, -1, true);
var t = tcs.Task;
t.ContinueWith(_ => rwh.Unregister(null));
return t;
}

Con este método se pueden usar las implementaciones existentes WaitHandle en métodos
asincrónicos. Por ejemplo, si desea restringir el número de operaciones asincrónicas que se
ejecutan en un momento dado, puede usar un semáforo (objeto System.Threading.Semaphore).
Se puede restringir a N el número de operaciones que se ejecutan en paralelo, inicializando el
recuento del semáforo a N, esperando al semáforo cuando se desee realizar una operación y
liberando el semáforo cuando se haya terminado:
static Semaphore m_throttle = new Semaphore(N, N);

static async Task DoOperation()


{
await m_throttle.WaitOneAsync();
… // do work
m_throttle.ReleaseOne();
}

También se puede compilar un semáforo asincrónico que no se base en identificadores de


espera, sino que funcione completamente con tareas. Para ello, puede usar técnicas como las
descritas en Utilizar el modelo asincrónico basado en tareas para compilar estructuras de datos
sobre Task. De hecho, el tipo SemaphoreSlim expone un método WaitAsync que habilita esta
funcionalidad.

MCT: Luis Dueñas Pag 103 de 336


Manual de .NET Framework 4.5

De TAP a identificadores de espera


Como se mencionó anteriormente, la clase Task implementa IAsyncResult y expone una
propiedad IAsyncResult.AsyncWaitHandle que devuelve un identificador de espera establecido
cuando la Task ha sido completada. Puede obtener la instancia de WaitHandle para una Task de
esta manera:
WaitHandle wh = ((IAsyncResult)task).AsyncWaitHandle;

2.2. Publicar datos de símbolos


Hay varias maneras de exponer las características asincrónicas al código de cliente. El modelo
asincrónico basado en eventos prescribe una manera recomendada para que las clases presenten
comportamiento asincrónico.
2.2.1. Programación multiproceso con el modelo asincrónico basado en
eventos
Hay varias maneras de exponer las características asincrónicas al código de cliente. El Modelo
asincrónico basado en evento prescribe la manera recomendada para que las clases presenten
comportamiento asincrónico.
2.2.1.1. Información general sobre el modelo asincrónico basado en
eventos
Las aplicaciones que realizan simultáneamente muchas tareas y que, aun así, siguen siendo
receptivas a la interacción con el usuario, a menudo requieren un diseño que utiliza varios
subprocesos. El espacio de nombres System.Threading proporciona todas las herramientas
necesarias para crear aplicaciones multiproceso de alto rendimiento; pero el uso eficaz de dichas
herramientas exige una amplia experiencia en ingeniería de software multiproceso. Para las
aplicaciones multiproceso relativamente simples, el componente BackgroundWorker
proporciona una solución sencilla. Para aplicaciones asincrónicas más sofisticadas, pruebe a
implementar una clase que siga el modelo asincrónico basado en eventos.
El modelo asincrónico basado en eventos pone a disposición del usuario las ventajas de las
aplicaciones multiproceso, al tiempo que evita muchos de los problemas complejos inherentes al
diseño multiproceso. Utilizar una clase que admita este modelo puede permitirle:
 Realizar tareas que exigen mucho tiempo (como descargas y operaciones con bases de
datos) "en segundo plano", sin interrumpir la aplicación.
 Ejecutar simultáneamente varias operaciones y recibir la notificación correspondiente
cuando finalice cada una de ellas.
 Esperar a que estén disponibles los recursos sin que la aplicación se detenga (no
responda).
 Establecer comunicación con operaciones asincrónicas pendientes utilizando el modelo
habitual de eventos y delegados.
Toda clase que admita el modelo asincrónico basado en eventos tendrá uno o varios métodos
denominados nombreDeMétodoAsync. Estos métodos pueden reflejar versiones sincrónicas,
que realizan la misma operación en el subproceso actual. La clase también puede tener un
evento nombreDeMétodo Completed y un método nombreDeMétodoAsyncCancel (o
simplemente CancelAsync).
PictureBox es un componente típico que admite el modelo asincrónico basado en eventos.
Puede descargar una imagen de forma sincrónica llamando a su método Load, pero si se trata de
una imagen que ocupe mucho, o si tiene una conexión de red lenta, la aplicación se detendrá
(“no responderá”) hasta que finalice la operación de descarga y se devuelva la llamada a Load.
Si desea que la aplicación se siga ejecutando mientras se carga la imagen, puede llamar al
método LoadAsync y controlar el evento LoadCompleted, exactamente igual que controlaría

MCT: Luis Dueñas Pag 104 de 336


Manual de .NET Framework 4.5

cualquier otro evento. Cuando llame al método LoadAsync, la aplicación continuará


ejecutándose mientras se produce la descarga en un subproceso independiente (“en segundo
plano”). Una vez finalizada la carga de la imagen, se llamará al controlador de eventos, que
examinará el parámetro AsyncCompletedEventArgs para determinar si la descarga ha finalizado
correctamente.
El modelo asincrónico basado en eventos exige que se puede cancelar una operación
asincrónica, y el control PictureBox admite este requisito con su método CancelAsync. Al
llamar al método CancelAsync, se envía una solicitud para detener la descarga pendiente y,
cuando se cancela la tarea, se provoca el evento LoadCompleted.
Precaución
Es posible que la descarga finalice justo en el momento en que se realiza la solicitud
CancelAsync, por lo que Cancelled puede no reflejar la solicitud de cancelación. Esto se
denomina condición de carrera y es un problema común en la programación multiproceso.
Características del modelo asincrónico basado en eventos
El modelo asincrónico basado en eventos puede adoptar varias formas, dependiendo de la
complejidad de las operaciones admitidas por una clase determinada. Las clases más simples
pueden tener un único método NombreMétodoAsync con su evento NombreMétodoCompleted
correspondiente. Las clases más complejas pueden tener varios métodos NombreMétodoAsync,
cada uno de ellos con su evento NombreMétodoCompleted correspondiente, así como versiones
sincrónicas de estos métodos. Las clases pueden opcionalmente admitir la cancelación, la
emisión de informes de progreso y la generación de resultados incrementales para cada método
asincrónico.
Todo método asincrónico puede también admitir varias llamadas pendientes (varias
invocaciones simultáneas), lo que permite que el código lo llame cualquier número de veces
antes de finalizar otras operaciones pendientes. Para controlar correctamente esta situación,
puede que la aplicación deba realizar un seguimiento de la finalización de cada operación.
Ejemplos del modelo asincrónico basado en eventos
Los componentes SoundPlayer y PictureBox representan implementaciones sencillas del modelo
asincrónico basado en eventos. Los componentes WebClient y BackgroundWorker representan
implementaciones más complejas del modelo asincrónico basado en eventos.
A continuación se muestra un ejemplo de declaración de clase que se ajusta al modelo:
public class AsyncExample
{
// Synchronous methods.
public int Method1(string param);
public void Method2(double param);
// Asynchronous methods.
public void Method1Async(string param);
public void Method1Async(string param, object userState);
public event Method1CompletedEventHandler Method1Completed;
public void Method2Async(double param);
public void Method2Async(double param, object userState);
public event Method2CompletedEventHandler Method2Completed;
public void CancelAsync(object userState);
public bool IsBusy { get; }
// Class implementation not shown.
}

La clase ficticia AsyncExample tiene dos métodos y ambos admiten invocaciones sincrónicas y
asincrónicas. Las sobrecargas sincrónicas se comportan como cualquier llamada a un método y
ejecutan la operación en el subproceso que realiza la llamada. Si esta operación llevase mucho
tiempo, podría haber un retraso significativo en la devolución de la llamada. Las sobrecargas
asincrónicas inician la operación en otro subproceso y después regresan inmediatamente, lo que

MCT: Luis Dueñas Pag 105 de 336


Manual de .NET Framework 4.5

permite que el subproceso que realiza la llamada continúe mientras la operación se ejecuta "en
segundo plano".
Sobrecargas de método asincrónicas
Hay dos sobrecargas posibles para las operaciones asincrónicas: de invocación única y de
invocación múltiple. Las dos formas se distinguen por sus signaturas de método: la sobrecarga
de invocación múltiple tiene un parámetro adicional denominado userState. Esto permite que el
código llame varias veces al método Method1Async(string param, object userState), sin esperar
a que finalice ninguna operación asincrónica pendiente. Si, por otro lado, intenta llamar al
método Method1Async(string param) antes de que haya finalizado una invocación anterior, el
método producirá una excepción InvalidOperationException.
El parámetro userState para las sobrecargas de invocación múltiple le permite distinguir entre
operaciones asincrónicas. Debe proporcionar un valor único, como un identificador único global
(GUID) o código hash, para cada llamada al método Method1Async(string param, object
userState) y, una vez finalizada cada operación, el controlador de eventos podrá determinar qué
instancia de la operación provocó el evento de finalización.
Realizar el seguimiento de las operaciones pendientes
Si utiliza las sobrecargas de invocación múltiple, el código necesitará realizar un seguimiento de
los objetos userState (identificadores de tarea) para las tareas pendientes. Para cada llamada al
método Method1Async(string param, object userState), normalmente generará un nuevo y único
objeto userState y lo agregará a una colección. Cuando la tarea correspondiente a este objeto
userState provoque el evento de finalización, la implementación del método de finalización
examinará AsyncCompletedEventArgs. UserState, que después se quitará de la colección.
Utilizado de esta manera, el parámetro userState asume el rol de un identificador de tarea.
Nota
Debe tener cuidado de proporcionar un valor único para userState en las llamadas a sobrecargas
de invocación múltiple. Los identificadores de tarea con un valor no único harán que la clase
asincrónica produzca una excepción ArgumentException.
Cancelar las operaciones pendientes
Es importante poder cancelar las operaciones asincrónicas en cualquier momento antes de su
finalización. Las clases que implementen el modelo asincrónico basado en eventos tendrán un
método CancelAsync (si solo hay un método asincrónico) o un método
nombreDeMétodoAsyncCancel (si hay varios métodos asincrónicos).
Los métodos que permiten varias invocaciones toman un parámetro userState, que se puede usar
para realizar un seguimiento de la duración de cada tarea. CancelAsync toma un parámetro
userState, que le permite cancelar determinadas tareas pendientes.
Los métodos que admiten sólo una operación pendiente cada vez, como Method1Async(string
param), no son cancelables.
Recibir actualizaciones del progreso y resultados incrementales
Toda clase que se ajuste al modelo asincrónico basado en eventos puede proporcionar
opcionalmente un evento para realizar el seguimiento del progreso y los resultados
incrementales. Dicho evento se suele denominar ProgressChanged o
nombreDeMétodoProgressChanged y su controlador de eventos correspondiente tomará un
parámetro ProgressChangedEventArgs.
El controlador de eventos para el evento ProgressChanged puede examinar la propiedad
ProgressChangedEventArgs.ProgressPercentage para determinar qué porcentaje de una tarea
asincrónica ha finalizado. Esta propiedad oscilará entre 0 y 100 y se puede utilizar para
actualizar la propiedad Value de un objeto ProgressBar. Si hay varias operaciones asincrónicas

MCT: Luis Dueñas Pag 106 de 336


Manual de .NET Framework 4.5

pendientes, puede utilizar la propiedad ProgressChangedEventArgs.UserState para discernir qué


operación está dando información de progreso.
Algunas clases pueden emitir un informe con resultados incrementales durante el progreso de
las operaciones asincrónicas. Estos resultados se almacenan en una clase que deriva de
ProgressChangedEventArgs y aparecen como propiedades en la clase derivada. Se puede
obtener acceso a dichos resultados en el controlador de eventos para el evento ProgressChanged,
de la misma forma que se obtiene acceso a la propiedad ProgressPercentage. Si hay varias
operaciones asincrónicas pendientes, puede utilizar la propiedad UserState para discernir qué
operación está dando información sobre resultados incrementales.
2.2.1.2. Implementar el modelo asincrónico basado en eventos
Si está escribiendo una clase con algunas operaciones que puedan incurrir en retrasos notables,
puede probar a darle funcionalidad asincrónica implementando Información general sobre el
modelo asincrónico basado en eventos.
El modelo asincrónico basado en eventos constituye una forma estandarizada de empaquetar
clases con características asincrónicas. Si se implementa con clases de ayuda como
AsyncOperationManager, la clase funcionará correctamente bajo cualquier modelo de
aplicación, incluidas ASP.NET, aplicaciones de consola y aplicaciones de Windows Forms.
Para las operaciones asincrónicas simples, el componente BackgroundWorker puede resultar
apropiado.
En la lista siguiente se describen las características del modelo asincrónico basado en eventos
que se tratan en este tema.
 Posibilidad de implementar el modelo asincrónico basado en eventos
 Asignación de nombres a los métodos asincrónicos
 Admisión opcional de la cancelación
 Admisión opcional de la propiedad IsBusy
 Compatibilidad opcional con la información de progreso
 Compatibilidad opcional con la devolución de resultados incrementales
 Uso de los parámetros Out y Ref en métodos
Posibilidad de implementar el modelo asincrónico basado en eventos
Considere la posibilidad de implementar el modelo asincrónico basado en eventos en los
siguientes casos:
 Cuando los clientes de una clase no necesiten objetos WaitHandle y IAsyncResult para
operaciones asincrónicas, lo que significa que el sondeo y WaitAll o WaitAny tendrán
que ser compilados por el cliente.
 Cuando desee que las operaciones asincrónicas sean administradas por el cliente con el
modelo conocido de evento/delegado.
Cualquier operación puede ser objeto de una implementación asincrónica, si bien deberían
considerarse de manera especial aquéllas cuya latencia pudiera ser más larga. Son especialmente
adecuadas las operaciones en las que los clientes llaman a un método y se les envía una
notificación cuando finaliza, sin que sea necesaria ninguna otra intervención. También son
adecuadas las operaciones que se ejecutan de forma continua, y que avisan periódicamente a los
clientes sobre el progreso, los resultados incrementales o los cambios de estado.
Asignación de nombres a los métodos asincrónicos
Para cada método nombreDeMétodo sincrónico para el que quiera proporcionar un homólogo
asincrónico:
Defina un método nombreDeMétodoAsync que:
 Devuelva void.

MCT: Luis Dueñas Pag 107 de 336


Manual de .NET Framework 4.5

 Tome los mismos parámetros que el método nombreDeMétodo.


 Acepte varias invocaciones.
Si lo desea, también puede definir una sobrecarga nombreDeMétodoAsync, idéntica a
nombreDeMétodoAsync, pero con un parámetro de valor de objeto adicional denominado
userState. Haga esto si está preparado para administrar varias invocaciones simultáneas de su
método, en cuyo caso se devolverá el valor userState a todos los controladores de eventos para
diferenciar las invocaciones del método. También puede hacerlo simplemente para crear un
lugar donde almacenar el estado del usuario para su posterior recuperación.
Para cada signatura del método nombreDeMétodoAsync independiente:
1. Defina el evento siguiente en la misma clase que el método:
public event MethodNameCompletedEventHandler MethodNameCompleted;

2. Defina el delegado siguiente y AsyncCompletedEventArgs. Éstos probablemente se


definirán fuera de la propia clase, pero en el mismo espacio de nombres.
public delegate void MethodNameCompletedEventHandler(object sender,
MethodNameCompletedEventArgs e);

public class MethodNameCompletedEventArgs :


System.ComponentModel.AsyncCompletedEventArgs
{
public MyReturnType Result { get; }
}

o Asegúrese de que la clase nombreDeMétodoCompletedEventArgs expone sus


miembros como propiedades de solo lectura, y no como campos, ya que los
campos impiden el enlace de datos.
o No defina ninguna clase derivada de AsyncCompletedEventArgs para métodos
que no generan resultados. Simplemente utilice una instancia del propio
AsyncCompleted EventArgs.

Nota
Es perfectamente aceptable, siempre que sea factible y adecuado, reutilizar
tipos AsyncCompletedEventArgs y de delegado. En este caso, los nombres
asignados no serán necesariamente coherentes con el nombre del método, ya
que ni el delegado ni AsyncCompletedEventArgs estarán asociados a un único
método.
Admisión opcional de la cancelación
Si una clase va a admitir la cancelación de operaciones asincrónicas, dicha cancelación se
debería exponer al cliente tal y como se describe a continuación. Observe que deben resolverse
dos cuestiones antes de definir si se admite la cancelación:
 ¿Tiene la clase, incluida cualquier futura adición que se pueda anticipar, una única
operación asincrónica que admita la cancelación?
 ¿Son compatibles las operaciones asincrónicas que admiten la cancelación con la
existencia de varias operaciones pendientes? Es decir, ¿puede tomar el método
nombreDeMétodoAsync un parámetro userState y permitir varias invocaciones sin
necesidad de esperar a que finalice cualquiera de ellas?
Busque las respuestas a estas dos preguntas en la tabla siguiente para determinar cuál debería ser
la signatura para el método de cancelación.
Admisión de varias operaciones
Sólo una operación cada vez
simultáneas
Una operación void void

MCT: Luis Dueñas Pag 108 de 336


Manual de .NET Framework 4.5

asincrónica en MethodNameAsyncCancel(object MethodNameAsyncCancel();


toda la clase userState);
Varias
operaciones void CancelAsync(object
void CancelAsync();
asincrónicas en la userState);
clase
Si define el métodoCancelAsync(object userState) , los clientes deberán tener la precaución, a la
hora de elegir sus valores de estado, de hacer que éstos sean capaces de diferenciar entre todos
los métodos asincrónicos invocados en el objeto, y no sólo entre todas las invocaciones de un
único método asincrónico.
La decisión de denominar a la versión de la operación asincrónica única como
nombreDeMétodo AsyncCancel se basa en la capacidad de detectar el método con mayor
facilidad en un entorno de diseño como IntelliSense, de Visual Studio. Esto agrupa los
miembros relacionados y los distingue de otros miembros que no tienen nada que ver con la
funcionalidad asincrónica. Si espera que se puedan agregar operaciones asincrónicas adicionales
en versiones subsiguientes, es mejor definir CancelAsync.
No defina varios métodos de la tabla anterior en la misma clase. Eso no tendrá sentido, o
recargará la interfaz de clase con una proliferación de métodos.
Normalmente, estos métodos devolverán un resultado inmediatamente y la operación podría, o
no, cancelarse realmente. En el controlador de eventos del evento nombreDeMétodoCompleted,
el objeto nombreDeMétodoCompletedEventArgs contiene un campo Cancelled, que los clientes
pueden utilizar para determinar si se ha producido la cancelación.
Aténgase a la semántica de cancelación descrita en Procedimientos recomendados para
implementar el modelo asincrónico basado en eventos.
Admisión opcional de la propiedad IsBusy
Si una clase no admite varias invocaciones simultáneas, considere la posibilidad de exponer una
propiedad IsBusy. Esto permite a los desarrolladores determinar si se está ejecutando un método
nombreDeMétodoAsync sin detectar una excepción del método nombreDeMétodoAsync.
Aténgase a la semántica de IsBusy descrita en Procedimientos recomendados para implementar
el modelo asincrónico basado en eventos.
Compatibilidad opcional con la información de progreso
Con frecuencia se desea que una operación asincrónica informe sobre el progreso de su
actividad. El modelo asincrónico basado en eventos proporciona una pauta para ello.
 Defina opcionalmente el evento que quiere que desencadene la operación asincrónica y
que se invoque en el subproceso adecuado. El objeto ProgressChangedEventArgs lleva
un indicador de progreso con un valor entero que se espera que esté entre 0 y 100.
 Denomine este evento como se indica a continuación:
o ProgressChanged si la clase tiene varias operaciones asincrónicas (o si se espera
que crezca y llegue a incluir varias operaciones asincrónicas en versiones
futuras);
o MethodName ProgressChanged si la clase tiene una sola operación asincrónica.
Esta opción de denominación es paralela a la realizada para el método de cancelación,
tal y como se describe en la sección Admisión opcional de la cancelación.
Este evento debería utilizar la signatura del delegado ProgressChangedEventHandler y la clase
ProgressChangedEventArgs. Como alternativa, si se puede proporcionar otro indicador de
progreso mas específico de dominio (por ejemplo, bytes leídos y bytes totales para una
operación de descarga), debería definir una clase derivada de ProgressChangedEventArgs.

MCT: Luis Dueñas Pag 109 de 336


Manual de .NET Framework 4.5

Observe que sólo hay un evento ProgressChanged o nombreDeMétodoProgressChanged para la


clase, independientemente del número de métodos asincrónicos que admita. Se espera que los
clientes utilicen el objeto userState que se pasa a los métodos nombreDeMétodoAsync para
distinguir entre las actualizaciones de progreso que se producen en varias operaciones
simultáneas.
Puede haber situaciones en las que varias operaciones admiten el progreso y cada una devuelve
un indicador diferente del mismo. En este caso, un evento ProgressChanged único no es
adecuado, por lo que puede resultar conveniente admitir varios eventos ProgressChanged. Si
éste es el caso, utilice un modelo de denominación de nombreDeMétodoProgressChanged para
cada método nombreDeMétodoAsync.
Aténgase a la semántica de información de progreso descrita en Procedimientos recomendados
para implementar el modelo asincrónico basado en eventos.
Compatibilidad opcional con la devolución de resultados incrementales
A veces, una operación asincrónica puede devolver resultados incrementales antes de finalizar.
Hay varias opciones que se pueden utilizar para que admitan esta posibilidad. A continuación,
se dan algunos ejemplos.
Clase de operación única
Si una clase sólo admite una operación asincrónica única y esa operación puede devolver
resultados incrementales, entonces:
 Amplíe el tipo ProgressChangedEventArgs para que lleve los datos del resultado
incremental y defina un evento nombreDeMétodoProgressChanged con estos datos
extendidos.
 Provoque este evento nombreDeMétodoProgressChanged cuando haya un resultado
incremental sobre el que informar.
Esta solución es específica de las clases de operaciones asincrónicas únicas, porque no hay
ningún problema en que un mismo evento devuelva resultados incrementales en "todas las
operaciones", como sucede con el evento nombreDeMétodoProgressChanged.
Clase de varias operaciones con resultados incrementales homogéneos
En este caso, la clase admite varios métodos asincrónicos, cada uno de ellos capaz de devolver
resultados incrementales, y estos resultados incrementales tienen todos los mismos tipos de
datos.
Siga el modelo descrito más arriba para las clases de operación única, ya que la estructura
EventArgs es la misma para todos los resultados incrementales. Defina un evento
ProgressChanged en lugar de un evento nombreDeMétodoProgressChanged, ya que éste es
válido para varios métodos asincrónicos.
Clase de varias operaciones con resultados incrementales heterogéneos
Si la clase admite varios métodos asincrónicos y cada uno de ellos devuelve un tipo diferente de
datos:
 Separe el informe sobre el resultado incremental del informe sobre el progreso.
 Defina un evento nombreDeMétodoProgressChanged independiente con el tipo
EventArgs adecuado para que cada método asincrónico pueda controlar los datos del
resultado incremental de ese método.
Invoque el controlador de dicho evento en el subproceso adecuado, tal y como se describe en
Procedimientos recomendados para implementar el modelo asincrónico basado en eventos.
Uso de los parámetros Out y Ref en métodos

MCT: Luis Dueñas Pag 110 de 336


Manual de .NET Framework 4.5

Aunque en general no se recomienda el uso de out y ref en .NET Framework, he aquí las reglas
que se deben seguir cuando están presentes:
Dado un método sincrónico nombreDeMétodo:
 Los parámetros out del método nombreDeMétodo no deberían formar parte de
nombreDeMétodoAsync. En su lugar, deberían formar parte de
nombreDeMétodoCompleted EventArgs con el mismo nombre que su parámetro
equivalente en nombreDeMétodo (a menos que haya un nombre más adecuado).
 Los parámetros ref del método nombreDeMétodo deberían aparecer como parte de
nombreDeMétodoAsync, y también como parte de
nombreDeMétodoCompletedEventArgs con el mismo nombre que su parámetro
equivalente en nombreDeMétodo (a menos que haya un nombre más adecuado).
Por ejemplo, dado el código:
public int MethodName(string arg1, ref string arg2, out string arg3);

El método asincrónico y la clase AsyncCompletedEventArgs tendrían el siguiente aspecto:


public void MethodNameAsync(string arg1, string arg2);

public class MethodNameCompletedEventArgs :


System.ComponentModel.AsyncCompletedEventArgs
{
public int Result { get; };
public string Arg2 { get; };
public string Arg3 { get; };
}

2.2.1.3. Procedimientos recomendados para implementar el modelo


asincrónico basado en eventos
El modelo asincrónico basado en eventos constituye una manera eficaz de exponer el
comportamiento asincrónico en clases, con semántica conocida de delegado y evento. Para
implementar el modelo asincrónico basado en eventos, es necesario seguir algunos requisitos de
comportamiento concretos. En las secciones siguientes se describen los requisitos e
instrucciones que se deben tener en cuenta al implementar una clase que sigue el modelo
asincrónico basado en eventos.
Garantías de comportamiento obligatorias
Cuando se implementa el modelo asincrónico basado en eventos, debe proporcionarse una serie
de garantías que aseguren que la clase se va a comportar adecuadamente y que los clientes de
dicha clase pueden confiar en dicho comportamiento.
Finalización
Invoque el controlador de eventos MethodNameCompleted siempre que la finalización sea
correcta, o cuando se produzca un error o una cancelación. Nunca debería ocurrir que una
aplicación permaneciese inactiva sin llegar nunca a la finalización. Como excepción a esta regla,
está el caso en que la propia operación asincrónica está diseñada para no finalizar nunca.
EventArgs y evento finalizado
Para cada método MethodNameAsync independiente, aplique los requisitos de diseño
siguientes:
 Defina un evento MethodNameCompleted en la misma clase que el método.
 Defina una clase EventArgs y el delegado que la acompaña para el evento
MethodNameCompleted que deriva de la clase AsyncCompletedEventArgs. El nombre
de clase predeterminado debería adoptar la forma MethodNameCompletedEventArgs.

MCT: Luis Dueñas Pag 111 de 336


Manual de .NET Framework 4.5

 Asegúrese de que la clase EventArgs es específica de los valores devueltos por el


método MethodName. Cuando utilice la clase EventArgs, nunca debería exigirles a los
desarrolladores que conviertan el resultado.
En el ejemplo de código siguiente se muestra, respectivamente, una implementación
buena y otra mala de este requisito de diseño.
// Good design
private void Form1_MethodNameCompleted(object sender, xxxCompletedEventArgs e)
{
DemoType result = e.Result;
}

// Bad design
private void Form1_MethodNameCompleted(object sender,
MethodNameCompletedEventArgs e)
{
DemoType result = (DemoType)(e.Result);
}
 No defina una clase EventArgs para devolver los métodos que devuelven void. En su
lugar, use una instancia de la clase AsyncCompletedEventArgs.
 Asegúrese de que se genere siempre el evento NombreMétodoCompleted. Este evento
se debe generar cuando se complete correctamente, cuando se produzca un error o
cuando se cancele. Nunca debería ocurrir que una aplicación permaneciese inactiva sin
llegar nunca a la finalización.
 Asegúrese de que se detecte cualquier excepción que se produzca en la operación
asincrónica y se asigne la excepción detectada a la propiedad Error.
 Si se produjo un error al completar la tarea, los resultados no deben ser accesibles.
Cuando la propiedad Error no es null, asegúrese de que el acceso a cualquier propiedad
de la estructura EventArgs genere una excepción. Utilice el método
RaiseExceptionIfNecessary para realizar la comprobación anterior.
 Modele un tiempo de espera como un error. Cuando se produzca un tiempo de espera,
genere el evento NombreMétodoCompleted y asigne TimeoutException a la propiedad
Error.
 Si la clase admite varias invocaciones simultáneas, asegúrese de que el evento
NombreMétodoCompleted contenga el objeto userSuppliedState apropiado.
 Asegúrese de que el evento NombreMétodoCompleted se genere en el subproceso
adecuado y en el momento oportuno del ciclo de vida de la aplicación.
Operaciones que se ejecutan simultáneamente
 Si la clase admite varias invocaciones simultáneas, deje que el programador haga un
seguimiento independiente de cada invocación definiendo la sobrecarga
MethodNameAsync que toma un parámetro de estado con valor de objeto, o un
identificador de tarea, denominado userSuppliedState. Dicho parámetro siempre debería
ser el último parámetro en la firma del método MethodNameAsync.
 Si la clase define la sobrecarga MethodNameAsync que toma un parámetro de estado
con valor de objeto, o un identificador de tarea, asegúrese de realizar el seguimiento de
la duración de la operación con ese identificador de tarea y de devolverlo al controlador
de finalización. Hay clases de ayuda disponibles para asistir este proceso.
 Si la clase define el método MethodNameAsync sin el parámetro de estado y no admite
varias invocaciones simultáneas, asegúrese de que cualquier intento de invocar
MethodNameAsync, antes de que haya finalizado la invocación previa de
MethodNameAsync, produzca una excepción InvalidOperationException.
 En términos generales, no produzca una excepción si se invoca varias veces el método
MethodNameAsync sin el parámetro userSuppliedState, de manera que haya varias
operaciones pendientes. Puede provocar una excepción cuando la clase no puede
controlar explícitamente esa situación, pero se supone que los desarrolladores pueden
controlar todas estas devoluciones de llamada imposibles de distinguir.

MCT: Luis Dueñas Pag 112 de 336


Manual de .NET Framework 4.5

Acceso a los resultados


 Si se produjo un error durante la ejecución de la operación asincrónica, los resultados no
deberían ser accesibles. Asegúrese de que el acceso a cualquier propiedad en
AsyncCompletedEventArgs cuando Error no es null provoca la excepción a la que hace
referencia Error. La clase AsyncCompletedEventArgs proporciona el método
RaiseExceptionIfNecessary para este propósito.
 Asegúrese de que cualquier intento de obtener acceso al resultado produce una
excepción InvalidOperationException, que informa de que se ha cancelado la operación.
Utilice el método AsyncCompletedEventArgs.RaiseExceptionIfNecessary para realizar
la comprobación anterior.
Información de progreso
 Si es posible, permita que se genere información de progreso. Esto permitirá a los
desarrolladores ofrecer una mejor experiencia al usuario de la aplicación cuando utilicen
la clase en cuestión.
 Si implementa un evento ProgressChanged/MethodNameProgressChanged, asegúrese
de que no se ha producido ningún evento de este tipo para ninguna operación
asincrónica específica una vez producido el evento MethodNameCompleted para dicha
operación.
 Si se rellena el objeto ProgressChangedEventArgs estándar, asegúrese de que
ProgressPercentage siempre se puede interpretar como un porcentaje. El porcentaje no
tiene que ser preciso, basta con que represente un porcentaje. Si la medida utilizada en
el informe sobre el progreso no puede ser un porcentaje, derive una clase de la clase
ProgressChangedEventArgs y marque ProgressPercentage como 0. Evite utilizar una
medida para el informe que no sea un porcentaje.
 Asegúrese de que el evento ProgressChanged se provoca en el subproceso adecuado y
en el momento oportuno del ciclo de vida de la aplicación.
Implementación de IsBusy
 No exponga una propiedad IsBusy si la clase admite varias invocaciones simultáneas.
Por ejemplo, los servidores proxy del servicio Web XML no exponen una propiedad
IsBusy porque admiten varias invocaciones simultáneas de métodos asincrónicos.
 La propiedad IsBusy debería devolver true después de haber llamado al método
MethodNameAsync y antes de que se haya provocado el evento
MethodNameCompleted. Si no fuese así, debería devolver false. Los componentes
BackgroundWorker y WebClient son ejemplos de clases que exponen una propiedad
IsBusy.
Cancelación
 Si es posible, permita la posibilidad de cancelación. Esto permitirá a los desarrolladores
ofrecer una mejor experiencia al usuario de la aplicación cuando utilicen la clase en
cuestión.
 En caso de cancelación, establezca el marcador Cancelled en el objeto AsyncCompleted
EventArgs.
 Asegúrese de que cualquier intento de obtener acceso al resultado produce una
excepción InvalidOperationException, que informa de que se ha cancelado la operación.
Utilice el método AsyncCompletedEventArgs.RaiseExceptionIfNecessary para realizar
la comprobación anterior.
 Asegúrese de que las llamadas a un método de cancelación se realicen correctamente y
no produzcan nunca una excepción. Como norma general, no se notifica a los clientes si
una operación es realmente cancelable en cualquier momento, ni tampoco si una
cancelación emitida con anterioridad ha tenido éxito. Sin embargo, siempre se notificará
a la aplicación si la cancelación ha tenido éxito, ya que la aplicación participa en el
estado de realización.

MCT: Luis Dueñas Pag 113 de 336


Manual de .NET Framework 4.5

 Provoque el evento MethodNameCompleted cuando se cancele la operación.


Errores y excepciones
 Detecte cualquier excepción que se produzca en la operación asincrónica y establezca
esa excepción como valor de la propiedad AsyncCompletedEventArgs.Error.
Subprocesamiento y contextos
Para que la clase funcione correctamente, es fundamental que los controladores de eventos del
cliente se invoquen en el subproceso o contexto adecuado para dicho modelo de aplicación,
incluidas las aplicaciones de Windows Forms y ASP.NET. Se proporcionan dos clases de ayuda
importantes para garantizar que la clase asincrónica se comporta correctamente bajo cualquier
modelo de aplicación: AsyncOperation y AsyncOperationManager.
AsyncOperationManager proporciona un método, CreateOperation, que devuelve un objeto
AsyncOperation. El método MethodNameAsync llama a CreateOperation y la clase utiliza el
objeto AsyncOperation devuelto para realizar el seguimiento del período de duración de la tarea
asincrónica.
Para informar al cliente sobre el progreso, los resultados incrementales y la finalización, llame a
los métodos Post y OperationCompleted en AsyncOperation. AsyncOperation es responsable de
calcular las referencias de las llamadas a los controladores de eventos del cliente en el contexto
o subproceso adecuados.
Nota
Puede no seguir estas reglas si quiere contradecir explícitamente las directivas del modelo de
aplicación; pero, incluso así, podrá seguir beneficiándose de las otras ventajas de utilizar el
modelo asincrónico basado en eventos. Por ejemplo, tal vez quiera que una clase que funciona
en los formularios Windows Forms tenga subprocesamiento libre. Puede crear una clase con
subprocesamiento libre, con tal de que los desarrolladores entiendan las restricciones que esto
implica. Las aplicaciones de consola no sincronizan la ejecución de llamadas Post. Esto puede
hacer que los eventos ProgressChanged se produzcan en un orden incorrecto. Si desea tener una
ejecución serializada de llamadas Post, implemente e instale una clase
System.Threading.SynchronizationContext.
Instrucciones
 Lo ideal es que cada invocación de método debería ser independiente de las otras.
Debería evitar asociar las invocaciones con recursos compartidos. Si fuese necesario
compartir los recursos entre las invocaciones, tendrá que utilizar un mecanismo de
sincronización apropiado en la implementación.
 No se recomiendan los diseños que exigen al cliente que implemente sincronización.
Por ejemplo, si tiene un método asincrónico que recibe un objeto estático global como
parámetro, varias invocaciones simultáneas de dicho método podrían dar como
resultado interbloqueos o datos dañados.
 Si implementa un método con la sobrecarga de varias invocaciones (userState en la
firma), la clase tendrá que administrar una colección de estados de usuario, o
identificadores de tarea, y las operaciones pendientes correspondientes. Esta colección
se debería proteger con regiones lock, porque las distintas invocaciones agregan y
quitan objetos userState en la colección.
 Siempre que sea factible y conveniente, considere la posibilidad de reutilizar clases
CompletedEventArgs. En este caso, los nombres asignados no son coherentes con el
nombre de método, ya que un delegado dado y el tipo EventArgs no están asociados a
un único método. Sin embargo, no es aceptable obligar a los desarrolladores a convertir
el valor recuperado de una propiedad en EventArgs.

MCT: Luis Dueñas Pag 114 de 336


Manual de .NET Framework 4.5

 Si está creando una clase que deriva de Component, no implemente ni instale su propia
clase SynchronizationContext. Los modelos de la aplicación, no los componentes, son
los que controlan la clase SynchronizationContext que se utiliza.
 Al utilizar multithreading de cualquier tipo, el usuario se expone potencialmente a
errores muy serios y complejos.
2.2.1.4. Decidir cuándo implementar el modelo asincrónico basado en
eventos
El Modelo asincrónico basado en evento proporciona un modelo para exponer el
comportamiento asincrónico de una clase. Con la introducción de este modelo, .NET
Framework define dos modelos para exponer el comportamiento asincrónico: el Modelo
asincrónico basado en la interfaz System.IAsyncResult y el modelo basado en eventos. En este
tema se explica cuándo es adecuado implementar ambos modelos.
Principios generales
En general, siempre que sea posible debería exponer las características asincrónicas mediante el
Modelo asincrónico basado en evento. Sin embargo, hay algunos requisitos que no puede
cumplir el modelo basado en eventos. En esos casos, puede ser necesario implementar el
modelo IAsyncResult además del modelo basado en eventos.
Nota
Es raro que se implemente el modelo IAsyncResult sin que también se implemente el modelo
basado en eventos.
Instrucciones
La lista siguiente incluye instrucciones sobre cuándo se debería implementar el Modelo
asincrónico basado en evento:
 Utilice el modelo basado en eventos como la API predeterminada para exponer el
comportamiento asincrónico de su clase.
 No exponga el modelo IAsyncResult cuando su clase se utilice principalmente en una
aplicación cliente como, por ejemplo, Windows Forms.
 Exponga el modelo IAsyncResult sólo cuando sea necesario para cumplir sus requisitos.
Por ejemplo, la compatibilidad con una API existente puede requerir que se exponga el
modelo IAsyncResult.
 No exponga el modelo IAsyncResult sin exponer también el modelo basado en evento.
 Si debe exponer el modelo IAsyncResult, hágalo como una opción avanzada. Por
ejemplo, si genera un objeto de servidor proxy, genere de forma predeterminada el
modelo basado en eventos, con una opción para generar el modelo IAsyncResult.
 Compile su implementación del modelo basado en eventos en su implementación del
modelo IAsyncResult.
 Evite exponer el modelo basado en eventos y el modelo IAsyncResult en la misma
clase. Exponga el modelo basado en eventos en las clases de "alto nivel" y el modelo
IAsyncResult en clases de "nivel inferior". Por ejemplo, compare el modelo basado en
eventos en el componente WebClient con el modelo IAsyncResult en la clase
HttpRequest.
o Exponga el modelo basado en eventos y el modelo IAsyncResult en la misma
clase cuando lo requiera la compatibilidad. Por ejemplo, si ya ha publicado una
API que utiliza el modelo IAsyncResult, sería necesario conservar el modelo
IAsyncResult para la compatibilidad con versiones anteriores.
o Exponga el modelo basado en eventos y el modelo IAsyncResult en la misma
clase si la complejidad del modelo de objetos resultante no compensará la
ventaja de separar las implementaciones. Es mejor exponer ambos modelos en
una sola clase que evitar exponer el modelo basado en evento.

MCT: Luis Dueñas Pag 115 de 336


Manual de .NET Framework 4.5

o Si debe exponer tanto el modelo basado en eventos como el modelo


IAsyncResult en una única clase, utilice EditorBrowsableAttribute establecido
en Advanced para marcar la implementación del modelo IAsyncResult como
característica avanzada. Esto indica a los entornos de diseño, como el
IntelliSense de Visual Studio, que no deben mostrar las propiedades y métodos
de IAsyncResult. Estas propiedades y métodos todavía son totalmente
utilizables, pero el desarrollador que trabaja con IntelliSense tiene una visión
más clara de la API.
Criterios para exponer el modelo IAsyncResult además del modelo basado en eventos
Aunque el Modelo asincrónico basado en evento tiene muchas ventajas en los escenarios
mencionados anteriormente, tiene algunos inconvenientes que debería conocer si el factor más
importante para sus aplicaciones es el rendimiento.
Hay tres escenarios que no prevé el modelo basado en eventos así como el modelo
IAsyncResult:
 Bloquear la espera de un IAsyncResult
 Bloquear la espera de muchos objetos IAsyncResult
 Sondear la finalización en IAsyncResult
Puede afrontar estos escenarios utilizando el modelo basado en eventos, pero hacerlo resulta
más torpe que utilizar el modelo IAsyncResult.
A menudo, los desarrolladores utilizan el modelo IAsyncResult para los servicios que
normalmente tienen requisitos de muy alto rendimiento. Por ejemplo, el escenario de sondeo de
finalización es una técnica de servidor de alto rendimiento.
Además, el modelo basado en eventos es menos eficaz que el modelo IAsyncResult porque crea
más objetos, sobre todo EventArgs, y porque sincroniza los subprocesos.
La lista siguiente muestra algunas recomendaciones que seguir si decide utilizar el modelo
IAsyncResult:
 Exponga sólo el modelo IAsyncResult cuando requiere específicamente la
compatibilidad para los objetos WaitHandle o IAsyncResult.
 Exponga el modelo IAsyncResult sólo cuando tenga una API existente que utilice el
modelo IAsyncResult.
 Si tiene una API existente basada en el modelo IAsyncResult, plantéese exponer
también el modelo basado en eventos en su siguiente versión publicada.
 Exponga el modelo IAsyncResult sólo si requiere un rendimiento algo que haya
comprobado que no puede obtener con el modelo basado en eventos pero que sí se
puede conseguir con el modelo IAsyncResult.
2.2.1.5. Tutorial: Implementar un componente que admita el modelo
asincrónico basado en eventos
Si está escribiendo una clase con algunas operaciones que puedan incurrir en retrasos notables,
puede probar a darle funcionalidad asincrónica implementando Información general sobre el
modelo asincrónico basado en eventos.
Este tutorial muestra cómo crear un componente que implemente el modelo asincrónico basado
en eventos. Se implementa utilizando clases de ayuda del espacio de nombres
System.ComponentModel, lo que garantiza que el componente funciona correctamente bajo
cualquier modelo de aplicación, incluso ASP.NET, aplicaciones de consola y aplicaciones de
Windows Forms. Este componente también se puede diseñar con un control PropertyGrid y con
diseñadores personalizados propios.
Cuando haya terminado, tendrá una aplicación que calcula de forma asincrónica los números
primos. La aplicación tendrá un subproceso de interfaz de usuario principal y un subproceso

MCT: Luis Dueñas Pag 116 de 336


Manual de .NET Framework 4.5

para cada cálculo de número primo. Aunque comprobar si un número elevado es primo puede
llevar una cantidad de tiempo considerable, el subproceso de interfaz de usuario principal no se
verá interrumpido por este retraso, y el formulario permanecerá receptivo durante el cálculo.
Podrá ejecutar tantos cálculos como desee simultáneamente, así como cancelar de forma
selectiva los cálculos pendientes.
Las tareas ilustradas en este tutorial incluyen:
 Crear el componente
 Definir delegados y eventos asincrónicos públicos
 Definir delegados privados
 Implementar eventos públicos
 Implementar el método de finalización
 Implementar los métodos de trabajo
 Implementar métodos de inicio y cancelación
Crear el componente
El primer paso es crear el componente que implementará el modelo asincrónico basado en
eventos.
Para crear el componente
 Cree una clase denominada PrimeNumberCalculator que herede de Component.
Definir delegados y eventos asincrónicos públicos
Un componente se comunica con los clientes mediante eventos. El evento
MethodNameCompleted avisa a los clientes de la finalización de una tarea asincrónica y el
evento MethodNameProgressChanged informa a los clientes sobre el progreso de dicha tarea.
Para definir eventos asincrónicos para los clientes de un componente:
1. Importe los espacios de nombres System.Threading y System.Collections.Specialized
que están en la parte superior del archivo.
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Globalization;
using System.Threading;
using System.Windows.Forms;

2. Antes de la definición de la clase PrimeNumberCalculator , declare delegados para los


eventos de progreso y finalización.
public delegate void ProgressChangedEventHandler(
ProgressChangedEventArgs e);
public delegate void CalculatePrimeCompletedEventHandler(
object sender, CalculatePrimeCompletedEventArgs e);

3. En la definición de la clase PrimeNumberCalculator , declare los eventos para informar


a los clientes sobre el progreso y la finalización de la tarea.
public event ProgressChangedEventHandler ProgressChanged;
public event CalculatePrimeCompletedEventHandler
CalculatePrimeCompleted;

4. Después de la definición de la clase PrimeNumberCalculator , derive la clase


CalculatePrime CompletedEventArgs para informar del resultado de cada cálculo al
controlador de eventos del cliente correspondiente al evento CalculatePrimeCompleted.
Además de las propiedades AsyncCompletedEventArgs, esta clase permite al cliente

MCT: Luis Dueñas Pag 117 de 336


Manual de .NET Framework 4.5

determinar qué número se ha comprobado, si es primo y, en caso de que no lo sea, cuál


es el primer divisor.
public class CalculatePrimeCompletedEventArgs :
AsyncCompletedEventArgs
{
private int numberToTestValue = 0;
private int firstDivisorValue = 1;
private bool isPrimeValue;

public CalculatePrimeCompletedEventArgs(
int numberToTest,
int firstDivisor,
bool isPrime,
Exception e,
bool canceled,
object state) : base(e, canceled, state)
{
this.numberToTestValue = numberToTest;
this.firstDivisorValue = firstDivisor;
this.isPrimeValue = isPrime;
}

public int NumberToTest


{
get
{
// Raise an exception if the operation failed or was
canceled.
RaiseExceptionIfNecessary();
// If the operation was successful, return the property
value.
return numberToTestValue;
}
}

public int FirstDivisor


{
get
{
// Raise an exception if the operation failed or was
canceled.
RaiseExceptionIfNecessary();
// If the operation was successful, return the property
value.
return firstDivisorValue;
}
}

public bool IsPrime


{
get
{
// Raise an exception if the operation failed or was
canceled.
RaiseExceptionIfNecessary();
// If the operation was successful, return the property
value.
return isPrimeValue;
}
}
}

Punto de control
En este punto, ya puede compilar el componente.

MCT: Luis Dueñas Pag 118 de 336


Manual de .NET Framework 4.5

Para probar el componente


 Compile el componente.
Recibirá dos advertencias del compilador:
warning CS0067: The event
'AsynchronousPatternExample.PrimeNumberCalculator.ProgressChanged' is
never used
warning CS0067: The event
'AsynchronousPatternExample.PrimeNumberCalculator.CalculatePrimeComplete
d' is never used

Estas advertencias se borrarán en la sección siguiente.


Definir delegados privados
Los aspectos asincrónicos del componente PrimeNumberCalculator se implementan
internamente con un delegado especial conocido como SendOrPostCallback.
SendOrPostCallback representa un método de devolución de llamada que se ejecuta en un
subproceso ThreadPool. El método de devolución de llamada debe tener una firma que tome un
único parámetro de tipo Object, lo que significa que tendrá que pasar información de estado
entre los delegados de una clase contenedora.
Para implementar el comportamiento asincrónico interno de un componente:
1. Declare y cree los delegados SendOrPostCallback en la clase PrimeNumberCalculator.
Cree los objetos SendOrPostCallback en un método de utilidad denominado
InitializeDelegates.
Necesitará dos delegados: uno para informar sobre el progreso al cliente y otro para
informar sobre la finalización al cliente.
private SendOrPostCallback onProgressReportDelegate;
private SendOrPostCallback onCompletedDelegate;
...
protected virtual void InitializeDelegates()
{
onProgressReportDelegate = new SendOrPostCallback(ReportProgress);
onCompletedDelegate = new SendOrPostCallback(CalculateCompleted);
}

2. Llame al método InitializeDelegates en el constructor del componente.


public PrimeNumberCalculator()
{
InitializeComponent();
InitializeDelegates();
}

3. Declare un delegado en la clase PrimeNumberCalculator que controla el trabajo real que


se va a hacer de forma asincrónica. Este delegado ajusta el método de trabajo que
comprueba si un número es primo. El delegado toma un parámetro AsyncOperation,
que se utilizará para realizar el seguimiento de la duración de la operación asincrónica.
private delegate void WorkerEventHandler(int
numberToCheck,AsyncOperation asyncOp);

4. Cree una colección para administrar la duración de las operaciones asincrónicas


pendientes. El cliente necesita de alguna manera realizar un seguimiento de la ejecución
y finalización de las operaciones. Dicho seguimiento se realiza pidiéndole al cliente que
pase un símbolo (token) único, o identificador de tarea, cuando realice la llamada al
método asincrónico. El componente PrimeNumberCalculator debe mantener un registro
de cada llamada asociando el identificador de tarea a su invocación correspondiente. Si
el cliente pasa un identificador de tarea que no es único, el componente
PrimeNumberCalculator debe provocar una excepción.

MCT: Luis Dueñas Pag 119 de 336


Manual de .NET Framework 4.5

El componentePrimeNumberCalculatormantiene un registro del identificador de tarea


utilizando una clase de colección especial denominada HybridDictionary. En la
definición de clase, cree un objeto HybridDictionary denominado
userTokenToLifetime.
private HybridDictionary userStateToLifetime = new HybridDictionary();

Implementar eventos públicos


Los componentes que implementan el modelo asincrónico basado en eventos se comunican con
los clientes mediante eventos. Estos eventos se invocan en el subproceso apropiado con la ayuda
de la clase AsyncOperation.
Para provocar eventos en los clientes de un componente:
 Implemente eventos públicos para informar a los clientes. Necesitará un evento para
informar sobre el progreso y otro para informar de la finalización.
// This method is invoked via the AsyncOperation object,
// so it is guaranteed to be executed on the correct thread.
private void CalculateCompleted(object operationState)
{
CalculatePrimeCompletedEventArgs e = operationState as
CalculatePrimeCompletedEventArgs;
OnCalculatePrimeCompleted(e);
}

// This method is invoked via the AsyncOperation object,


// so it is guaranteed to be executed on the correct thread.
private void ReportProgress(object state)
{
ProgressChangedEventArgs e = state as ProgressChangedEventArgs;
OnProgressChanged(e);
}

protected void OnCalculatePrimeCompleted(


CalculatePrimeCompletedEventArgs e)
{
if (CalculatePrimeCompleted != null)
{
CalculatePrimeCompleted(this, e);
}
}

protected void OnProgressChanged(ProgressChangedEventArgs e)


{
if (ProgressChanged != null)
{
ProgressChanged(e);
}
}

Implementar el método de finalización


El delegado de finalización es el método que invocará el comportamiento asincrónico de
subprocesamiento libre subyacente una vez que finalice la operación asincrónica, bien porque
ésta haya acabado correctamente, porque se haya producido un error o por cancelación. Esta
invocación se produce en un subproceso arbitrario.
En este método es donde se quita el identificador de tarea del cliente de la colección interna de
símbolos (token) de cliente únicos. Este método también pone término a la duración de una
operación asincrónica determinada llamando al método PostOperationCompleted en el objeto
AsyncOperation correspondiente. Esta llamada provoca en el subproceso el evento de
finalización adecuado para el modelo de aplicación. Una vez llamado el método
PostOperationCompleted, no es posible seguir utilizando esta instancia de AsyncOperation.
Cualquier intento posterior de utilizarla producirá una excepción.

MCT: Luis Dueñas Pag 120 de 336


Manual de .NET Framework 4.5

La firma CompletionMethod debe contener toda la información de estado necesaria para


describir el resultado de la operación asincrónica. Contiene información de estado del número
comprobado por esta operación asincrónica específica, si el número es primo, y el valor de su
primer divisor, si el número no es primo. También contiene información de estado que describe
cualquier excepción que se haya producido y el objeto AsyncOperation correspondiente a esta
tarea determinada.
Para poner término a una operación asincrónica:
 Implemente el método de finalización. Toma seis parámetros, que utiliza para rellenar
CalculatePrimeCompletedEventArgs que se devuelve al cliente a través de
CalculatePrime CompletedEventHandler del cliente. Quita el símbolo (token) del
identificador de tarea del cliente de la colección interna y finaliza la duración de la
operación asincrónica con una llamada al método PostOperationCompleted.
AsyncOperation calcula las referencias de la llamada al subproceso o contexto adecuado
para el modelo de aplicación.
// This is the method that the underlying, free-threaded
// asynchronous behavior will invoke. This will happen on
// an arbitrary thread.
private void CompletionMethod(int numberToTest,int firstDivisor,bool
isPrime,
Exception exception, bool canceled, AsyncOperation asyncOp)
{
// If the task was not previously canceled,
// remove the task from the lifetime collection.
if (!canceled)
{
lock (userStateToLifetime.SyncRoot)
{
userStateToLifetime.Remove(asyncOp.UserSuppliedState);
}
}
// Package the results of the operation in a
// CalculatePrimeCompletedEventArgs.
CalculatePrimeCompletedEventArgs e = new
CalculatePrimeCompletedEventArgs(
numberToTest,firstDivisor,isPrime,exception,canceled,
asyncOp.UserSuppliedState);
// End the task. The asyncOp object is responsible for marshaling
the call.
asyncOp.PostOperationCompleted(onCompletedDelegate, e);
// Note that after the call to OperationCompleted, asyncOp is no
longer
// usable, and any attempt to use it will cause an exception to be
thrown.
}

Punto de control
En este punto, ya puede compilar el componente.
Para probar el componente
 Compile el componente.
Recibirá una advertencia del compilador:
warning CS0169: The private field
'AsynchronousPatternExample.PrimeNumberCalculator.workerDelegate' is
never used

Esta advertencia se resolverá en la sección siguiente.


Implementar los métodos de trabajo

MCT: Luis Dueñas Pag 121 de 336


Manual de .NET Framework 4.5

Hasta el momento, ha implementado el código asincrónico admitido para el


componentePrimeNumber Calculator. Ahora puede implementar el código que hace el trabajo
real. Implementará tres métodos: CalculateWorker, BuildPrimeNumberList eIsPrime. Juntos,
BuildPrime NumberList e IsPrimeconstituyen un algoritmo muy conocido denominado criba de
Eratóstenes, que determina si un número es primo buscando todos los números primos hasta la
raíz cuadrada del número probado. Si llegados a ese punto no se ha encontrado ningún divisor,
el número en cuestión es primo.
Si este componente fuese escrito para obtener una eficacia máxima, recordaría todos los
números primos detectados por distintas invocaciones para los distintos números probados.
También buscaría divisores triviales como el 2, el 3 y el 5. Sea como sea, con este ejemplo se
pretende demostrar lo largas que pueden resultar las operaciones ejecutadas de forma
asincrónica, por lo que estas optimizaciones se dejan como ejercicio para quien las quiera
realizar.
El método CalculateWorker se ajusta en un delegado y se invoca de forma asincrónica con una
llamada a BeginInvoke.
Nota
La información del progreso se implementa en el método BuildPrimeNumberList. En equipos
rápidos, se pueden provocar eventos ProgressChanged en una sucesión rápida. El subproceso
del cliente, en el que se producen estos eventos, debe ser capaz de controlar esta situación. El
código de interfaz de usuario puede verse inundado de mensajes y es posible que no pueda
seguir el ritmo y no responda.
Para ejecutar de forma asincrónica el cálculo de números primos:
1. Implemente el método de utilidad TaskCanceled. Esto comprueba la colección de
duración de tarea para el identificador de tarea determinado y devuelve true si no
encuentra el identificador de tarea.
// Utility method for determining if a task has been canceled.
private bool TaskCanceled(object taskId)
{
return( userStateToLifetime[taskId] == null );
}

2. Implemente el método CalculateWorker. Toma dos parámetros: un número que se va a


comprobar y un objeto AsyncOperation.
// This method performs the actual prime number computation.
// It is executed on the worker thread.
private void CalculateWorker(int numberToTest,AsyncOperation asyncOp)
{
bool isPrime = false;
int firstDivisor = 1;
Exception e = null;
// Check that the task is still active.
// The operation may have been canceled before the thread was
scheduled.
if (!TaskCanceled(asyncOp.UserSuppliedState))
{
try
{
// Find all the prime numbers up to the square root of
numberToTest.
ArrayList primes =
BuildPrimeNumberList(numberToTest,asyncOp);
// Now we have a list of primes less than numberToTest.
isPrime = IsPrime(primes,numberToTest,out firstDivisor);
}
catch (Exception ex)
{
e = ex;

MCT: Luis Dueñas Pag 122 de 336


Manual de .NET Framework 4.5

}
}
//CalculatePrimeState calcState = new CalculatePrimeState(
// numberToTest,
// firstDivisor,
// isPrime,
// e,
// TaskCanceled(asyncOp.UserSuppliedState),
// asyncOp);
//this.CompletionMethod(calcState);
this.CompletionMethod(numberToTest,firstDivisor,isPrime,e,
TaskCanceled(asyncOp.UserSuppliedState),asyncOp);
//completionMethodDelegate(calcState);
}

3. ImplementeBuildPrimeNumberList. Toma dos parámetros: el número que se va a


comprobar y un objeto AsyncOperation. AsyncOperation se utiliza para informar sobre
el progreso y los resultados incrementales. Esto garantiza que se llame a los
controladores de eventos del cliente en el contexto o subproceso apropiados para el
modelo de aplicación. Cuando BuildPrime NumberList encuentra un número primo,
informa que éste es el resultado incremental al controlador de eventos del cliente
correspondiente al evento ProgressChanged. Esto requiere una clase derivada de
ProgressChangedEventArgs, llamada CalculatePrimeProgressChangedEvent Args, que
tiene una propiedad agregada llamada LatestPrimeNumber.
El método BuildPrimeNumberList también llama periódicamente al método
TaskCanceled y se cierra si el método devuelve true.
// This method computes the list of prime numbers used by the IsPrime
method.
private ArrayList BuildPrimeNumberList(int numberToTest,AsyncOperation
asyncOp)
{
ProgressChangedEventArgs e = null;
ArrayList primes = new ArrayList();
int firstDivisor;
int n = 5;
// Add the first prime numbers.
primes.Add(2);
primes.Add(3);
// Do the work.
while (n < numberToTest && !TaskCanceled( asyncOp.UserSuppliedState
) )
{
if (IsPrime(primes, n, out firstDivisor))
{
// Report to the client that a prime was found.
e = new CalculatePrimeProgressChangedEventArgs(n,
(int)((float)n / (float)numberToTest * 100),
asyncOp.UserSuppliedState);
asyncOp.Post(this.onProgressReportDelegate, e);
primes.Add(n);
// Yield the rest of this time slice.
Thread.Sleep(0);
}
// Skip even numbers.
n += 2;
}
return primes;
}

4. ImplementeIsPrime. Toma tres parámetros: una lista de números primos conocidos, el


número que se quiere comprobar y un parámetro de salida para el primer divisor
encontrado. Dada la lista de números primos, determina si el número que se está
comprobando es primo.
// This method tests n for primality against the list of

MCT: Luis Dueñas Pag 123 de 336


Manual de .NET Framework 4.5

// prime numbers contained in the primes parameter.


private bool IsPrime(ArrayList primes,int n,out int firstDivisor)
{
bool foundDivisor = false;
bool exceedsSquareRoot = false;
int i = 0;
int divisor = 0;
firstDivisor = 1;
// Stop the search if: there are no more primes in the list,
// there is a divisor of n in the list, or there is a prime that
// is larger than the square root of n.
while ((i < primes.Count) && !foundDivisor && !exceedsSquareRoot)
{
// The divisor variable will be the smallest prime number not
yet tried
divisor = (int)primes[i++];
// Determine whether the divisor is greater than the square root
of n.
if (divisor * divisor > n)
{
exceedsSquareRoot = true;
}
// Determine whether the divisor is a factor of n.
else if (n % divisor == 0)
{
firstDivisor = divisor;
foundDivisor = true;
}
}
return !foundDivisor;
}

5. DeriveCalculatePrimeProgressChangedEventArgsde ProgressChangedEventArgs. Esta


clase es necesaria para informar sobre los resultados incrementales al controlador de
eventos del cliente correspondiente al evento ProgressChanged. Tiene una propiedad
adicional denominada LatestPrimeNumber.
public class CalculatePrimeProgressChangedEventArgs :
ProgressChangedEventArgs
{
private int latestPrimeNumberValue = 1;

public CalculatePrimeProgressChangedEventArgs(int latestPrime,


int progressPercentage,
object userToken) : base( progressPercentage, userToken )
{
this.latestPrimeNumberValue = latestPrime;
}

public int LatestPrimeNumber


{
get
{
return latestPrimeNumberValue;
}
}
}

Punto de control
En este punto, ya puede compilar el componente.
Para probar el componente
 Compile el componente.
Lo único que falta por escribir son los métodos para iniciar y cancelar operaciones
asincrónicas, CalculatePrimeAsyncyCancelAsync.

MCT: Luis Dueñas Pag 124 de 336


Manual de .NET Framework 4.5

Implementar los métodos de inicio y cancelación


Inicie el método de trabajo en su propio subproceso llamando a BeginInvoke en el delegado que
lo incluye. Para administrar la duración de una operación asincrónica determinada, llame al
método CreateOperation en la clase de ayuda AsyncOperationManager. Esto devuelve un objeto
AsyncOperation, que calcula las referencias de las llamadas en los controladores de eventos del
cliente para el contexto o subproceso apropiado.
Puede cancelar una operación pendiente determinada llamando a PostOperationCompleted en su
AsyncOperation correspondiente. Esto finaliza esa operación y cualquier llamada subsiguiente a
su AsyncOperation producirá una excepción.
Para implementar la funcionalidad de inicio y cancelación:
1. Implemente el método CalculatePrimeAsync. Asegúrese de que el símbolo (token) o
identificador de tarea proporcionado por el cliente es único, frente a todos los símbolos
(token) que representan tareas pendientes en ese momento. Si el cliente pasa un símbolo
(token) que no es único, CalculatePrimeAsync produce una excepción. De lo contrario,
el símbolo (token) se agrega a la colección de identificadores de tarea.
// This method starts an asynchronous calculation.
// First, it checks the supplied task ID for uniqueness.
// If taskId is unique, it creates a new WorkerEventHandler
// and calls its BeginInvoke method to start the calculation.
public virtual void CalculatePrimeAsync(int numberToTest,object taskId)
{
// Create an AsyncOperation for taskId.
AsyncOperation asyncOp =
AsyncOperationManager.CreateOperation(taskId);
// Multiple threads will access the task dictionary,
// so it must be locked to serialize access.
lock (userStateToLifetime.SyncRoot)
{
if (userStateToLifetime.Contains(taskId))
{
throw new ArgumentException("Task ID parameter must be
unique",
"taskId");
}
userStateToLifetime[taskId] = asyncOp;
}
// Start the asynchronous operation.
WorkerEventHandler workerDelegate=new
WorkerEventHandler(CalculateWorker);
workerDelegate.BeginInvoke(numberToTest,asyncOp,null,null);
}

2. Implemente el método CancelAsync. Si existe el parámetro taskId en la colección de


símbolos (token), se quita. Esto evita la ejecución de las tareas canceladas que no han
comenzado. Si la tarea está ejecutándose, el método BuildPrimeNumberList se cierra
cuando detecta que se ha quitado el identificador de tarea de la colección de duración.
// This method cancels a pending asynchronous operation.
public void CancelAsync(object taskId)
{
AsyncOperation asyncOp = userStateToLifetime[taskId] as
AsyncOperation;
if (asyncOp != null)
{
lock (userStateToLifetime.SyncRoot)
{
userStateToLifetime.Remove(taskId);
}
}
}

MCT: Luis Dueñas Pag 125 de 336


Manual de .NET Framework 4.5

Punto de control
En este punto, ya puede compilar el componente.
Para probar el componente
 Compile el componente.
El componente PrimeNumberCalculator ya está completo y listo para su uso.
2.2.1.5.1. Cómo: Implementar un componente que admita el modelo
asincrónico basado en eventos
En el ejemplo de código siguiente se implementa un componente con un método asincrónico,
según Información general sobre el modelo asincrónico basado en eventos. El componente es
una calculadora de número primo que utiliza el algoritmo de Eratosthenes o de Sieve para
determinar si un número es primo o compuesto.
Visual Studio ofrece una amplia compatibilidad para esta tarea.
Para obtener un ejemplo de cliente que utilice el componente PrimeNumberCalculator, vea
Cómo: Implementar un cliente en un modelo asincrónico basado en eventos.
Ejemplo
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Globalization;
using System.Threading;
using System.Windows.Forms;
...
/////////////////////////////////////////////////////////////
#region PrimeNumberCalculator Implementation
public delegate void ProgressChangedEventHandler(
ProgressChangedEventArgs e);
public delegate void CalculatePrimeCompletedEventHandler(
object sender, CalculatePrimeCompletedEventArgs e);
// This class implements the Event-based Asynchronous Pattern.
// It asynchronously computes whether a number is prime or
// composite (not prime).
public class PrimeNumberCalculator : Component
{
private delegate void WorkerEventHandler(int numberToCheck,
AsyncOperation asyncOp);
private SendOrPostCallback onProgressReportDelegate;
private SendOrPostCallback onCompletedDelegate;
private HybridDictionary userStateToLifetime = new HybridDictionary();
private System.ComponentModel.Container components = null;
/////////////////////////////////////////////////////////////
#region Public events
public event ProgressChangedEventHandler ProgressChanged;
public event CalculatePrimeCompletedEventHandler
CalculatePrimeCompleted;
#endregion
/////////////////////////////////////////////////////////////
#region Construction and destruction

public PrimeNumberCalculator(IContainer container)


{
container.Add(this);
InitializeComponent();
InitializeDelegates();
}

MCT: Luis Dueñas Pag 126 de 336


Manual de .NET Framework 4.5

public PrimeNumberCalculator()
{
InitializeComponent();
InitializeDelegates();
}

protected virtual void InitializeDelegates()


{
onProgressReportDelegate = new SendOrPostCallback(ReportProgress);
onCompletedDelegate = new SendOrPostCallback(CalculateCompleted);
}

protected override void Dispose(bool disposing)


{
if (disposing)
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose(disposing);
}

#endregion // Construction and destruction

/////////////////////////////////////////////////////////////
#region Implementation

// This method starts an asynchronous calculation.


// First, it checks the supplied task ID for uniqueness.
// If taskId is unique, it creates a new WorkerEventHandler
// and calls its BeginInvoke method to start the calculation.
public virtual void CalculatePrimeAsync(int numberToTest,object
taskId)
{
// Create an AsyncOperation for taskId.
AsyncOperation asyncOp =
AsyncOperationManager.CreateOperation(taskId);
// Multiple threads will access the task dictionary,
// so it must be locked to serialize access.
lock (userStateToLifetime.SyncRoot)
{
if (userStateToLifetime.Contains(taskId))
{
throw new ArgumentException(
"Task ID parameter must be unique", "taskId");
}
userStateToLifetime[taskId] = asyncOp;
}
// Start the asynchronous operation.
WorkerEventHandler workerDelegate =
new WorkerEventHandler(CalculateWorker);
workerDelegate.BeginInvoke(numberToTest,asyncOp,null,null);
}

// Utility method for determining if a task has been canceled.


private bool TaskCanceled(object taskId)
{
return( userStateToLifetime[taskId] == null );
}

// This method cancels a pending asynchronous operation.


public void CancelAsync(object taskId)
{
AsyncOperation asyncOp = userStateToLifetime[taskId] as
AsyncOperation;

MCT: Luis Dueñas Pag 127 de 336


Manual de .NET Framework 4.5

if (asyncOp != null)
{
lock (userStateToLifetime.SyncRoot)
{
userStateToLifetime.Remove(taskId);
}
}
}

// This method performs the actual prime number computation.


// It is executed on the worker thread.
private void CalculateWorker(int numberToTest,AsyncOperation asyncOp)
{
bool isPrime = false;
int firstDivisor = 1;
Exception e = null;
// Check that the task is still active.
// The operation may have been canceled before
// the thread was scheduled.
if (!TaskCanceled(asyncOp.UserSuppliedState))
{
try
{
// Find all the prime numbers up to the square root of
numberToTest.
ArrayList primes =
BuildPrimeNumberList(numberToTest,asyncOp);
// Now we have a list of primes less than numberToTest.
isPrime = IsPrime(primes,numberToTest,out firstDivisor);
}
catch (Exception ex)
{
e = ex;
}
}

//CalculatePrimeState calcState = new CalculatePrimeState(


// numberToTest,
// firstDivisor,
// isPrime,
// e,
// TaskCanceled(asyncOp.UserSuppliedState),
// asyncOp);
//this.CompletionMethod(calcState);
this.CompletionMethod(numberToTest,firstDivisor,isPrime,e,
TaskCanceled(asyncOp.UserSuppliedState),asyncOp);
//completionMethodDelegate(calcState);
}

// This method computes the list of prime numbers used by the


// IsPrime method.
private ArrayList BuildPrimeNumberList(int numberToTest,
AsyncOperation asyncOp)
{
ProgressChangedEventArgs e = null;
ArrayList primes = new ArrayList();
int firstDivisor;
int n = 5;
// Add the first prime numbers.
primes.Add(2);
primes.Add(3);
// Do the work.
while (n < numberToTest && !TaskCanceled(
asyncOp.UserSuppliedState ) )
{
if (IsPrime(primes, n, out firstDivisor))
{
// Report to the client that a prime was found.

MCT: Luis Dueñas Pag 128 de 336


Manual de .NET Framework 4.5

e = new CalculatePrimeProgressChangedEventArgs(n,
(int)((float)n / (float)numberToTest * 100),
asyncOp.UserSuppliedState);
asyncOp.Post(this.onProgressReportDelegate, e);
primes.Add(n);
// Yield the rest of this time slice.
Thread.Sleep(0);
}
// Skip even numbers.
n += 2;
}
return primes;
}

// This method tests n for primality against the list of


// prime numbers contained in the primes parameter.
private bool IsPrime(ArrayList primes,int n,out int firstDivisor)
{
bool foundDivisor = false;
bool exceedsSquareRoot = false;
int i = 0;
int divisor = 0;
firstDivisor = 1;
// Stop the search if: there are no more primes in the list,
// there is a divisor of n in the list, or there is a prime that
is
// larger than the square root of n.
while ((i < primes.Count)&&!foundDivisor &&!exceedsSquareRoot)
{
// The divisor variable will be the smallest
// prime number not yet tried.
divisor = (int)primes[i++];
// Determine whether the divisor is greater than the square
root of n.
if (divisor * divisor > n)
{
exceedsSquareRoot = true;
}
// Determine whether the divisor is a factor of n.
else if (n % divisor == 0)
{
firstDivisor = divisor;
foundDivisor = true;
}
}
return !foundDivisor;
}

// This method is invoked via the AsyncOperation object,


// so it is guaranteed to be executed on the correct thread.
private void CalculateCompleted(object operationState)
{
CalculatePrimeCompletedEventArgs e =
operationState as CalculatePrimeCompletedEventArgs;
OnCalculatePrimeCompleted(e);
}

// This method is invoked via the AsyncOperation object,


// so it is guaranteed to be executed on the correct thread.
private void ReportProgress(object state)
{
ProgressChangedEventArgs e = state as ProgressChangedEventArgs;
OnProgressChanged(e);
}

protected void
OnCalculatePrimeCompleted(CalculatePrimeCompletedEventArgs e)
{

MCT: Luis Dueñas Pag 129 de 336


Manual de .NET Framework 4.5

if (CalculatePrimeCompleted != null)
{
CalculatePrimeCompleted(this, e);
}
}

protected void OnProgressChanged(ProgressChangedEventArgs e)


{
if (ProgressChanged != null)
{
ProgressChanged(e);
}
}

// This is the method that the underlying, free-threaded asynchronous


// behavior will invoke. This will happen on an arbitrary thread.
private void CompletionMethod(int numberToTest,int firstDivisor,
bool isPrime,Exception exception,bool canceled,AsyncOperation
asyncOp)
{
// If the task was not previously canceled,
// remove the task from the lifetime collection.
if (!canceled)
{
lock (userStateToLifetime.SyncRoot)
{
userStateToLifetime.Remove(asyncOp.UserSuppliedState);
}
}
// Package the results of the operation in a
// CalculatePrimeCompletedEventArgs.
CalculatePrimeCompletedEventArgs e =
new
CalculatePrimeCompletedEventArgs(numberToTest,firstDivisor,
isPrime,exception,canceled,asyncOp.UserSuppliedState);
// End the task. The asyncOp object is responsible
// for marshaling the call.
asyncOp.PostOperationCompleted(onCompletedDelegate, e);
// Note that after the call to OperationCompleted,
// asyncOp is no longer usable, and any attempt to use it
// will cause an exception to be thrown.
}
#endregion

/////////////////////////////////////////////////////////////
#region Component Designer generated code

private void InitializeComponent()


{
components = new System.ComponentModel.Container();
}

#endregion
}

public class
CalculatePrimeProgressChangedEventArgs:ProgressChangedEventArgs
{
private int latestPrimeNumberValue = 1;

public CalculatePrimeProgressChangedEventArgs(
int latestPrime,
int progressPercentage,
object userToken) : base( progressPercentage, userToken )
{
this.latestPrimeNumberValue = latestPrime;
}

MCT: Luis Dueñas Pag 130 de 336


Manual de .NET Framework 4.5

public int LatestPrimeNumber


{
get
{
return latestPrimeNumberValue;
}
}
}

public class CalculatePrimeCompletedEventArgs :


AsyncCompletedEventArgs
{
private int numberToTestValue = 0;
private int firstDivisorValue = 1;
private bool isPrimeValue;

public CalculatePrimeCompletedEventArgs(int numberToTest,int


firstDivisor,
bool isPrime,Exception e,bool canceled,object state) : base(e,
canceled, state)
{
this.numberToTestValue = numberToTest;
this.firstDivisorValue = firstDivisor;
this.isPrimeValue = isPrime;
}

public int NumberToTest


{
get
{
// Raise an exception if the operation failed or was canceled.
RaiseExceptionIfNecessary();
// If the operation was successful, return the property value.
return numberToTestValue;
}
}

public int FirstDivisor


{
get
{
// Raise an exception if the operation failed or was canceled.
RaiseExceptionIfNecessary();
// If the operation was successful, return the property value.
return firstDivisorValue;
}
}

public bool IsPrime


{
get
{
// Raise an exception if the operation failed or was canceled.
RaiseExceptionIfNecessary();
// If the operation was successful, return the property value.
return isPrimeValue;
}
}
}
#endregion

2.2.1.5.2. Cómo: Implementar un cliente en un modelo asincrónico


basado en eventos
El siguiente ejemplo de código muestra cómo utilizar un componente conforme a Información
general sobre el modelo asincrónico basado en eventos. El formulario de este ejemplo utiliza el

MCT: Luis Dueñas Pag 131 de 336


Manual de .NET Framework 4.5

componente PrimeNumberCalculator, que se describe en Cómo: Implementar un componente


que admita el modelo asincrónico basado en eventos.
Cuando ejecute un proyecto que utilice este ejemplo, verá un formulario de cálculo de números
primos ("Prime Number Calculator") con una cuadrícula y dos botones: Iniciar tarea nueva y
Cancelar. Puede hacer clic en el botón Iniciar tarea nueva varias veces seguidas; por cada clic,
una operación asincrónica iniciará un cálculo para determinar si un número de prueba generado
aleatoriamente es primo. El formulario mostrará periódicamente el progreso y los resultados
incrementales. A cada operación se le asigna un identificador de tarea único. El resultado del
cálculo se muestra en la columna Resultado; si el número de prueba no es primo, se le asigna la
etiqueta Compuesto y se muestra su primer divisor.
Cualquier operación pendiente puede cancelarse con el botón Cancelar. Se pueden realizar
selecciones múltiples.
Nota
La mayoría de los números no serán primos. Si tras realizar varias operaciones no encuentra
ningún número primo, simplemente inicie más tareas; acabará encontrando algún número primo.
Ejemplo
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Globalization;
using System.Threading;
using System.Windows.Forms;

namespace AsyncOperationManagerExample
{
// This form tests the PrimeNumberCalculator component.
public class PrimeNumberCalculatorMain : System.Windows.Forms.Form
{
#region Private fields

private PrimeNumberCalculator primeNumberCalculator1;


private System.Windows.Forms.GroupBox taskGroupBox;
private System.Windows.Forms.ListView listView1;
private System.Windows.Forms.ColumnHeader taskIdColHeader;
private System.Windows.Forms.ColumnHeader progressColHeader;
private System.Windows.Forms.ColumnHeader currentColHeader;
private System.Windows.Forms.Panel buttonPanel;
private System.Windows.Forms.Panel panel2;
private System.Windows.Forms.Button startAsyncButton;
private System.Windows.Forms.Button cancelButton;
private System.Windows.Forms.ColumnHeader testNumberColHeader;
private System.Windows.Forms.ColumnHeader resultColHeader;
private System.Windows.Forms.ColumnHeader firstDivisorColHeader;
private System.ComponentModel.IContainer components;
private int progressCounter;
private int progressInterval = 100;

#endregion // Private fields

#region Private fields


public PrimeNumberCalculatorMain ()
{
InitializeComponent();
// Hook up event handlers.
this.primeNumberCalculator1.CalculatePrimeCompleted +=
new CalculatePrimeCompletedEventHandler(
primeNumberCalculator1_CalculatePrimeCompleted);

MCT: Luis Dueñas Pag 132 de 336


Manual de .NET Framework 4.5

this.primeNumberCalculator1.ProgressChanged +=
new ProgressChangedEventHandler(
primeNumberCalculator1_ProgressChanged);
this.listView1.SelectedIndexChanged +=
new EventHandler(listView1_SelectedIndexChanged);
}
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#endregion // Construction and destruction

#region Implementation

// This event handler selects a number randomly to test for primality.


// It then starts the asynchronous calculation by calling the
// PrimeNumberCalculator component's CalculatePrimeAsync method.
private void startAsyncButton_Click(System.Object sender,
System.EventArgs e)
{
// Randomly choose test numbers up to 200,000 for primality.
Random rand = new Random();
int testNumber = rand.Next(200000);
// Task IDs are Guids.
Guid taskId = Guid.NewGuid();
this.AddListViewItem(taskId, testNumber);
// Start the asynchronous task.

this.primeNumberCalculator1.CalculatePrimeAsync(testNumber,taskId);
}

private void listView1_SelectedIndexChanged(object sender,EventArgs e)


{
this.cancelButton.Enabled = CanCancel();
}

// This event handler cancels all pending tasks that are


// selected in the ListView control.
private void cancelButton_Click(System.Object sender,System.EventArgs
e)
{
Guid taskId = Guid.Empty;
// Cancel all selected tasks.
foreach(ListViewItem lvi in this.listView1.SelectedItems)
{
// Tasks that have been completed or canceled have
// their corresponding ListViewItem.Tag property set to null.
if (lvi.Tag != null)
{
taskId = (Guid)lvi.Tag;
this.primeNumberCalculator1.CancelAsync(taskId);
lvi.Selected = false;
}
}
cancelButton.Enabled = false;
}

// This event handler updates the ListView control when the


// PrimeNumberCalculator raises the ProgressChanged event.
// On fast computers, the PrimeNumberCalculator can raise many
// successive ProgressChanged events, so the user interface

MCT: Luis Dueñas Pag 133 de 336


Manual de .NET Framework 4.5

// may be flooded with messages. To prevent the user interface


// from hanging, progress is only reported at intervals.
private void primeNumberCalculator1_ProgressChanged(
ProgressChangedEventArgs e)
{
if (this.progressCounter++ % this.progressInterval == 0)
{
Guid taskId = (Guid)e.UserState;
if (e is CalculatePrimeProgressChangedEventArgs)
{
CalculatePrimeProgressChangedEventArgs cppcea =
e as CalculatePrimeProgressChangedEventArgs;
this.UpdateListViewItem(taskId, cppcea.ProgressPercentage,
cppcea.LatestPrimeNumber);
}
else
{
this.UpdateListViewItem(taskId, e.ProgressPercentage);
}
}
else if (this.progressCounter > this.progressInterval)
{
this.progressCounter = 0;
}
}

// This event handler updates the ListView control when the


// PrimeNumberCalculator raises the CalculatePrimeCompleted
// event. The ListView item is updated with the appropriate
// outcome of the calculation: Canceled, Error, or result.
private void primeNumberCalculator1_CalculatePrimeCompleted(
object sender, CalculatePrimeCompletedEventArgs e)
{
Guid taskId = (Guid)e.UserState;
if (e.Cancelled)
{
string result = "Canceled";
ListViewItem lvi = UpdateListViewItem(taskId, result);
if (lvi != null)
{
lvi.BackColor = Color.Pink;
lvi.Tag = null;
}
}
else if (e.Error != null)
{
string result = "Error";
ListViewItem lvi = UpdateListViewItem(taskId, result);
if (lvi != null)
{
lvi.BackColor = Color.Red;
lvi.ForeColor = Color.White;
lvi.Tag = null;
}
}
else
{
bool result = e.IsPrime;
ListViewItem lvi = UpdateListViewItem(taskId,result,
e.FirstDivisor);
if (lvi != null)
{
lvi.BackColor = Color.LightGray;
lvi.Tag = null;
}
}
}

MCT: Luis Dueñas Pag 134 de 336


Manual de .NET Framework 4.5

#endregion // Implementation

#region Private Methods

private ListViewItem AddListViewItem(Guid guid,int testNumber)


{
ListViewItem lvi = new ListViewItem();
lvi.Text =
testNumber.ToString(CultureInfo.CurrentCulture.NumberFormat);
lvi.SubItems.Add("Not Started");
lvi.SubItems.Add("1");
lvi.SubItems.Add(guid.ToString());
lvi.SubItems.Add("---");
lvi.SubItems.Add("---");
lvi.Tag = guid;
this.listView1.Items.Add( lvi );
return lvi;
}

private ListViewItem UpdateListViewItem(Guid guid, int


percentComplete,
int current )
{
ListViewItem lviRet = null;
foreach (ListViewItem lvi in this.listView1.Items)
{
if (lvi.Tag != null)
{
if ((Guid)lvi.Tag == guid)
{
lvi.SubItems[1].Text = percentComplete.ToString(
CultureInfo.CurrentCulture.NumberFormat);
lvi.SubItems[2].Text = current.ToString(
CultureInfo.CurrentCulture.NumberFormat);
lviRet = lvi;
break;
}
}
}
return lviRet;
}

private ListViewItem UpdateListViewItem(Guid guid, int


percentComplete,
int current, bool result, int firstDivisor )
{
ListViewItem lviRet = null;
foreach (ListViewItem lvi in this.listView1.Items)
{
if ((Guid)lvi.Tag == guid)
{
lvi.SubItems[1].Text = percentComplete.ToString(
CultureInfo.CurrentCulture.NumberFormat);
lvi.SubItems[2].Text = current.ToString(
CultureInfo.CurrentCulture.NumberFormat);
lvi.SubItems[4].Text = result ? "Prime" : "Composite";
lvi.SubItems[5].Text = firstDivisor.ToString(
CultureInfo.CurrentCulture.NumberFormat);
lviRet = lvi;
break;
}
}
return lviRet;
}

private ListViewItem UpdateListViewItem(Guid guid, int percentComplete


)
{

MCT: Luis Dueñas Pag 135 de 336


Manual de .NET Framework 4.5

ListViewItem lviRet = null;


foreach (ListViewItem lvi in this.listView1.Items)
{
if (lvi.Tag != null)
{
if ((Guid)lvi.Tag == guid)
{
lvi.SubItems[1].Text = percentComplete.ToString(
CultureInfo.CurrentCulture.NumberFormat);
lviRet = lvi;
break;
}
}
}
return lviRet;
}

private ListViewItem UpdateListViewItem(Guid guid, bool result,


int firstDivisor )
{
ListViewItem lviRet = null;
foreach (ListViewItem lvi in this.listView1.Items)
{
if (lvi.Tag != null)
{
if ((Guid)lvi.Tag == guid)
{
lvi.SubItems[4].Text = result ? "Prime" : "Composite";
lvi.SubItems[5].Text = firstDivisor.ToString(
CultureInfo.CurrentCulture.NumberFormat);
lviRet = lvi;
break;
}
}
}
return lviRet;
}

private ListViewItem UpdateListViewItem(Guid guid, string result)


{
ListViewItem lviRet = null;
foreach (ListViewItem lvi in this.listView1.Items)
{
if (lvi.Tag != null)
{
if ((Guid)lvi.Tag == guid)
{
lvi.SubItems[4].Text = result;
lviRet = lvi;
break;
}
}
}
return lviRet;
}

private bool CanCancel()


{
bool oneIsActive = false;
foreach(ListViewItem lvi in this.listView1.SelectedItems)
{
if (lvi.Tag != null)
{
oneIsActive = true;
break;
}
}
return( oneIsActive == true );

MCT: Luis Dueñas Pag 136 de 336


Manual de .NET Framework 4.5

#endregion

#region Windows Form Designer generated code

private void InitializeComponent()


{
this.components = new System.ComponentModel.Container();
this.taskGroupBox = new System.Windows.Forms.GroupBox();
this.buttonPanel = new System.Windows.Forms.Panel();
this.cancelButton = new System.Windows.Forms.Button();
this.startAsyncButton = new System.Windows.Forms.Button();
this.listView1 = new System.Windows.Forms.ListView();
this.testNumberColHeader = new
System.Windows.Forms.ColumnHeader();
this.progressColHeader = new System.Windows.Forms.ColumnHeader();
this.currentColHeader = new System.Windows.Forms.ColumnHeader();
this.taskIdColHeader = new System.Windows.Forms.ColumnHeader();
this.resultColHeader = new System.Windows.Forms.ColumnHeader();
this.firstDivisorColHeader = new
System.Windows.Forms.ColumnHeader();
this.panel2 = new System.Windows.Forms.Panel();
this.primeNumberCalculator1 =
new
AsyncOperationManagerExample.PrimeNumberCalculator(this.components);
this.taskGroupBox.SuspendLayout();
this.buttonPanel.SuspendLayout();
this.SuspendLayout();
// taskGroupBox
this.taskGroupBox.Controls.Add(this.buttonPanel);
this.taskGroupBox.Controls.Add(this.listView1);
this.taskGroupBox.Dock = System.Windows.Forms.DockStyle.Fill;
this.taskGroupBox.Location = new System.Drawing.Point(0, 0);
this.taskGroupBox.Name = "taskGroupBox";
this.taskGroupBox.Size = new System.Drawing.Size(608, 254);
this.taskGroupBox.TabIndex = 1;
this.taskGroupBox.TabStop = false;
this.taskGroupBox.Text = "Tasks";
// buttonPanel
this.buttonPanel.Controls.Add(this.cancelButton);
this.buttonPanel.Controls.Add(this.startAsyncButton);
this.buttonPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
this.buttonPanel.Location = new System.Drawing.Point(3, 176);
this.buttonPanel.Name = "buttonPanel";
this.buttonPanel.Size = new System.Drawing.Size(602, 75);
this.buttonPanel.TabIndex = 1;
// cancelButton
this.cancelButton.Enabled = false;
this.cancelButton.Location = new System.Drawing.Point(128, 24);
this.cancelButton.Name = "cancelButton";
this.cancelButton.Size = new System.Drawing.Size(88, 23);
this.cancelButton.TabIndex = 1;
this.cancelButton.Text = "Cancel";
this.cancelButton.Click +=
new System.EventHandler(this.cancelButton_Click);
// startAsyncButton
this.startAsyncButton.Location = new System.Drawing.Point(24, 24);
this.startAsyncButton.Name = "startAsyncButton";
this.startAsyncButton.Size = new System.Drawing.Size(88, 23);
this.startAsyncButton.TabIndex = 0;
this.startAsyncButton.Text = "Start New Task";
this.startAsyncButton.Click +=
new System.EventHandler(this.startAsyncButton_Click);
// listView1
this.listView1.Columns.AddRange(new
System.Windows.Forms.ColumnHeader[] {
this.testNumberColHeader,

MCT: Luis Dueñas Pag 137 de 336


Manual de .NET Framework 4.5

this.progressColHeader,
this.currentColHeader,
this.taskIdColHeader,
this.resultColHeader,
this.firstDivisorColHeader});
this.listView1.Dock = System.Windows.Forms.DockStyle.Fill;
this.listView1.FullRowSelect = true;
this.listView1.GridLines = true;
this.listView1.Location = new System.Drawing.Point(3, 16);
this.listView1.Name = "listView1";
this.listView1.Size = new System.Drawing.Size(602, 160);
this.listView1.TabIndex = 0;
this.listView1.View = System.Windows.Forms.View.Details;
// testNumberColHeader
this.testNumberColHeader.Text = "Test Number";
this.testNumberColHeader.Width = 80;
// progressColHeader
this.progressColHeader.Text = "Progress";
// currentColHeader
this.currentColHeader.Text = "Current";
// taskIdColHeader
this.taskIdColHeader.Text = "Task ID";
this.taskIdColHeader.Width = 200;
// resultColHeader
this.resultColHeader.Text = "Result";
this.resultColHeader.Width = 80;
// firstDivisorColHeader
this.firstDivisorColHeader.Text = "First Divisor";
this.firstDivisorColHeader.Width = 80;
// panel2
this.panel2.Location = new System.Drawing.Point(200, 128);
this.panel2.Name = "panel2";
this.panel2.TabIndex = 2;
// PrimeNumberCalculatorMain
this.ClientSize = new System.Drawing.Size(608, 254);
this.Controls.Add(this.taskGroupBox);
this.Name = "PrimeNumberCalculatorMain";
this.Text = "Prime Number Calculator";
this.taskGroupBox.ResumeLayout(false);
this.buttonPanel.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion

[STAThread]
static void Main()
{
Application.Run(new PrimeNumberCalculatorMain());
}
}

#region PrimeNumberCalculator Implementation

public delegate void ProgressChangedEventHandler(ProgressChangedEventArgs


e);

public delegate void CalculatePrimeCompletedEventHandler(object sender,


CalculatePrimeCompletedEventArgs e);

// This class implements the Event-based Asynchronous Pattern.


// It asynchronously computes whether a number is prime or composite (not
prime).
public class PrimeNumberCalculator : Component
{
private delegate void WorkerEventHandler(int numberToCheck,
AsyncOperation asyncOp);
private SendOrPostCallback onProgressReportDelegate;
private SendOrPostCallback onCompletedDelegate;

MCT: Luis Dueñas Pag 138 de 336


Manual de .NET Framework 4.5

private HybridDictionary userStateToLifetime = new HybridDictionary();


private System.ComponentModel.Container components = null;

#region Public events

public event ProgressChangedEventHandler ProgressChanged;


public event CalculatePrimeCompletedEventHandler
CalculatePrimeCompleted;

#endregion

#region Construction and destruction

public PrimeNumberCalculator(IContainer container)


{
container.Add(this);
InitializeComponent();
InitializeDelegates();
}

public PrimeNumberCalculator()
{
InitializeComponent();
InitializeDelegates();
}

protected virtual void InitializeDelegates()


{
onProgressReportDelegate = new SendOrPostCallback(ReportProgress);
onCompletedDelegate = new SendOrPostCallback(CalculateCompleted);
}

protected override void Dispose(bool disposing)


{
if (disposing)
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose(disposing);
}

#endregion // Construction and destruction

#region Implementation

// This method starts an asynchronous calculation.


// First, it checks the supplied task ID for uniqueness.
// If taskId is unique, it creates a new WorkerEventHandler
// and calls its BeginInvoke method to start the calculation.
public virtual void CalculatePrimeAsync(int numberToTest, object
taskId)
{
// Create an AsyncOperation for taskId.
AsyncOperation asyncOp =
AsyncOperationManager.CreateOperation(taskId);
// Multiple threads will access the task dictionary,
// so it must be locked to serialize access.
lock (userStateToLifetime.SyncRoot)
{
if (userStateToLifetime.Contains(taskId))
{
throw new ArgumentException(
"Task ID parameter must be unique", "taskId");
}
userStateToLifetime[taskId] = asyncOp;

MCT: Luis Dueñas Pag 139 de 336


Manual de .NET Framework 4.5

}
// Start the asynchronous operation.
WorkerEventHandler workerDelegat=new
WorkerEventHandler(CalculateWorker);
workerDelegate.BeginInvoke(numberToTest,asyncOp,null,null);
}

// Utility method for determining if a task has been canceled.


private bool TaskCanceled(object taskId)
{
return( userStateToLifetime[taskId] == null );
}

// This method cancels a pending asynchronous operation.


public void CancelAsync(object taskId)
{
AsyncOperation asyncOp = userStateToLifetime[taskId] as
AsyncOperation;
if (asyncOp != null)
{
lock (userStateToLifetime.SyncRoot)
{
userStateToLifetime.Remove(taskId);
}
}
}

// This method performs the actual prime number computation.


// It is executed on the worker thread.
private void CalculateWorker(int numberToTest, AsyncOperation asyncOp)
{
bool isPrime = false;
int firstDivisor = 1;
Exception e = null;
// Check that the task is still active.
// The operation may have been canceled before the thread was
scheduled.
if (!TaskCanceled(asyncOp.UserSuppliedState))
{
try
{
// Find all the prime numbers up to the square root of
numberToTest.
ArrayList primes =
BuildPrimeNumberList(numberToTest,asyncOp);
// Now we have a list of primes less than numberToTest.
isPrime = IsPrime(primes,numberToTest,out firstDivisor);
}
catch (Exception ex)
{
e = ex;
}
}
//CalculatePrimeState calcState = new CalculatePrimeState(
// numberToTest,
// firstDivisor,
// isPrime,
// e,
// TaskCanceled(asyncOp.UserSuppliedState),
// asyncOp);
//this.CompletionMethod(calcState);
this.CompletionMethod(numberToTest,firstDivisor,isPrime,e,
TaskCanceled(asyncOp.UserSuppliedState),asyncOp);
//completionMethodDelegate(calcState);
}

// This method computes the list of prime numbers used by the IsPrime
method.

MCT: Luis Dueñas Pag 140 de 336


Manual de .NET Framework 4.5

private ArrayList BuildPrimeNumberList(int numberToTest,


AsyncOperation asyncOp)
{
ProgressChangedEventArgs e = null;
ArrayList primes = new ArrayList();
int firstDivisor;
int n = 5;
// Add the first prime numbers.
primes.Add(2);
primes.Add(3);
// Do the work.
while (n < numberToTest && !TaskCanceled(
asyncOp.UserSuppliedState))
{
if (IsPrime(primes, n, out firstDivisor))
{
// Report to the client that a prime was found.
e = new CalculatePrimeProgressChangedEventArgs(n,
(int)((float)n / (float)numberToTest * 100),
asyncOp.UserSuppliedState);
asyncOp.Post(this.onProgressReportDelegate, e);
primes.Add(n);
// Yield the rest of this time slice.
Thread.Sleep(0);
}
// Skip even numbers.
n += 2;
}
return primes;
}

// This method tests n for primality against the list of


// prime numbers contained in the primes parameter.
private bool IsPrime(ArrayList primes, int n, out int firstDivisor)
{
bool foundDivisor = false;
bool exceedsSquareRoot = false;
int i = 0;
int divisor = 0;
firstDivisor = 1;
// Stop the search if: there are no more primes in the list,
// there is a divisor of n in the list, or
// there is a prime that is larger than the square root of n.
while ((i < primes.Count) && !foundDivisor && !exceedsSquareRoot)
{
// The divisor variable will be the smallest prime number not
yet tried.
divisor = (int)primes[i++];
// Determine whether the divisor is greater than the square
root of n.
if (divisor * divisor > n)
{
exceedsSquareRoot = true;
}
// Determine whether the divisor is a factor of n.
else if (n % divisor == 0)
{
firstDivisor = divisor;
foundDivisor = true;
}
}
return !foundDivisor;
}

// This method is invoked via the AsyncOperation object,


// so it is guaranteed to be executed on the correct thread.
private void CalculateCompleted(object operationState)
{

MCT: Luis Dueñas Pag 141 de 336


Manual de .NET Framework 4.5

CalculatePrimeCompletedEventArgs e =
operationState as CalculatePrimeCompletedEventArgs;
OnCalculatePrimeCompleted(e);
}

// This method is invoked via the AsyncOperation object,


// so it is guaranteed to be executed on the correct thread.
private void ReportProgress(object state)
{
ProgressChangedEventArgs e = state as ProgressChangedEventArgs;
OnProgressChanged(e);
}

protected void
OnCalculatePrimeCompleted(CalculatePrimeCompletedEventArgs e)
{
if (CalculatePrimeCompleted != null)
{
CalculatePrimeCompleted(this, e);
}
}

protected void OnProgressChanged(ProgressChangedEventArgs e)


{
if (ProgressChanged != null)
{
ProgressChanged(e);
}
}

// This is the method that the underlying, free-threaded


// asynchronous behavior will invoke. This will happen on an arbitrary
thread.
private void CompletionMethod(int numberToTest,int firstDivisor,bool
isPrime,
Exception exception,bool canceled,AsyncOperation asyncOp )
{
// If the task was not previously canceled,
// remove the task from the lifetime collection.
if (!canceled)
{
lock (userStateToLifetime.SyncRoot)
{
userStateToLifetime.Remove(asyncOp.UserSuppliedState);
}
}
// Package the results of the operation in a
// CalculatePrimeCompletedEventArgs.
CalculatePrimeCompletedEventArgs e = new
CalculatePrimeCompletedEventArgs(
numberToTest,firstDivisor,isPrime,exception,canceled,
asyncOp.UserSuppliedState);
// End the task. The asyncOp object is responsible for marshaling
the call
asyncOp.PostOperationCompleted(onCompletedDelegate, e);
// Note that after the call to OperationCompleted,
// asyncOp is no longer usable, and any attempt to use it
// will cause an exception to be thrown.
}

#endregion

#region Component Designer generated code

private void InitializeComponent()


{
components = new System.ComponentModel.Container();
}

MCT: Luis Dueñas Pag 142 de 336


Manual de .NET Framework 4.5

#endregion

public class
CalculatePrimeProgressChangedEventArgs:ProgressChangedEventArgs
{
private int latestPrimeNumberValue = 1;

public CalculatePrimeProgressChangedEventArgs(int latestPrime,


int progressPercentage, object userToken) :
base( progressPercentage, userToken )
{
this.latestPrimeNumberValue = latestPrime;
}

public int LatestPrimeNumber


{
get
{
return latestPrimeNumberValue;
}
}
}

public class CalculatePrimeCompletedEventArgs:AsyncCompletedEventArgs


{
private int numberToTestValue = 0;
private int firstDivisorValue = 1;
private bool isPrimeValue;

public CalculatePrimeCompletedEventArgs(
int numberToTest,
int firstDivisor,
bool isPrime,
Exception e,
bool canceled,
object state) : base(e, canceled, state)
{
this.numberToTestValue = numberToTest;
this.firstDivisorValue = firstDivisor;
this.isPrimeValue = isPrime;
}

public int NumberToTest


{
get
{
// Raise an exception if the operation failed or was canceled.
RaiseExceptionIfNecessary();
// If the operation was successful, return the property value.
return numberToTestValue;
}
}

public int FirstDivisor


{
get
{
// Raise an exception if the operation failed or was canceled.
RaiseExceptionIfNecessary();
// If the operation was successful, return the property value.
return firstDivisorValue;
}
}

public bool IsPrime

MCT: Luis Dueñas Pag 143 de 336


Manual de .NET Framework 4.5

{
get
{
// Raise an exception if the operation failed or was canceled.
RaiseExceptionIfNecessary();
// If the operation was successful, return the property value.
return isPrimeValue;
}
}
}

#endregion
}

2.2.1.6. Cómo: Utilizar componentes que admitan el modelo


asincrónico basado en eventos
Muchos componentes le ofrecen la opción de realizar tareas de forma asincrónica. Los
componentes SoundPlayer y PictureBox, por ejemplo, le permiten cargar sonidos e imágenes
"en segundo plano", mientras el subproceso principal continúa ejecutándose sin interrupciones.
El uso de métodos asincrónicos en una clase que admite Información general sobre el modelo
asincrónico basado en eventos puede reducirse a asociar un controlador de eventos al evento
nombreDeMétodoCompleted del componente, al igual que haría con cualquier otro evento. Al
llamar al método nombreDeMétodoAsync, la aplicación seguirá ejecutándose sin interrupciones,
hasta que se produzca el evento nombreDeMétodoCompleted. Puede examinar el parámetro
AsyncCompleted EventArgs del controlador de eventos para determinar si la operación
asincrónica ha finalizado correctamente o se ha cancelado.
El siguiente procedimiento muestra cómo utilizar la función de carga de imágenes asincrónica
de un control PictureBox.
Para permitir que un control PictureBox cargue una imagen de forma asincrónica
1. Cree una instancia del componente PictureBox en el formulario.
2. Asigne un controlador de eventos al evento LoadCompleted.
Compruebe aquí que no se haya producido ningún error durante la descarga asincrónica.
Aquí también puede comprobar posibles cancelaciones.
public Form1()
{
InitializeComponent();
this.pictureBox1.LoadCompleted += new
System.ComponentModel.AsyncCompletedEventHandler(this.pictureBox1_LoadCo
mpleted);
}

private void pictureBox1_LoadCompleted(object sender,


AsyncCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show(e.Error.Message, "Load Error");
}
else if (e.Cancelled)
{
MessageBox.Show("Load canceled", "Canceled");
}
else
{
MessageBox.Show("Load completed", "Completed");
}
}

MCT: Luis Dueñas Pag 144 de 336


Manual de .NET Framework 4.5

3. Agregue dos botones al formulario, los botones denominados loadButton y


cancelLoadButton. Agregue controladores de eventos Click para iniciar y cancelar la
descarga.
private void loadButton_Click(object sender, EventArgs e)
{
// Replace with a real url.
pictureBox1.LoadAsync("http://www.tailspintoys.com/image.jpg");
}

private void cancelLoadButton_Click(object sender, EventArgs e)


{
pictureBox1.CancelAsync();
}

4. Ejecute la aplicación.
A medida que vaya descargándose la imagen, podrá mover libremente el formulario,
minimizarlo y maximizarlo.
2.3. Modelo de programación asincrónica (APM)
Una operación asincrónica que utiliza el modelo de diseño IAsyncResult se implementa como
dos métodos denominados BeginnombreDeOperación y EndnombreDeOperación que empiezan
y terminan respectivamente la operación asincrónica nombreDeOperación. Por ejemplo, la clase
FileStream proporciona los métodos BeginRead y EndRead para leer de forma asincrónica bytes
de un archivo. Estos métodos implementan la versión asincrónica del método Read.
Después de llamar a BeginnombreDeOperación, una aplicación puede seguir ejecutando
instrucciones en el subproceso que realiza la llamada mientras la operación asincrónica se lleva
a cabo en un subproceso diferente. Para cada llamada a BeginnombreDeOperación, la aplicación
también debería llamar a EndnombreDeOperación para obtener los resultados de la operación.
Comenzar una operación asincrónica
El método BeginnombreDeOperación inicia la operación asincrónica nombreDeOperación y
devuelve un objeto que implementa la interfaz IAsyncResult. Los objetos IAsyncResult
almacenan información sobre una operación asincrónica. En la siguiente tabla se muestra
información sobre una operación asincrónica.
Miembro Descripción
Objeto específico opcional de aplicación que contiene información
AsyncState
sobre la operación asincrónica.
WaitHandle que se puede utilizar para bloquear la ejecución de la
AsyncWaitHandle
aplicación hasta que la operación asincrónica finaliza.
Valor que indica si la operación asincrónica se completó en el
CompletedSynchronously subproceso utilizado para llamar a BeginnombreDeOperación en
lugar de completarse en un subproceso ThreadPool independiente.
IsCompleted Valor que indica si la operación asincrónica ha finalizado.
Un método BeginnombreDeOperación toma todos los parámetros declarados en la firma de la
versión sincrónica del método que se pasen por valor o por referencia. Los parámetros out no
forman parte de la firma del método BeginnombreDeOperación. La firma del método
BeginnombreDeOperación también incluye dos parámetros adicionales. El primero define un
delegado AsyncCallback que hace referencia a un método al que se llama cuando finaliza la
operación asincrónica. El llamador puede especificar null (Nothing en Visual Basic) si no desea
que se invoque un método cuando la operación finaliza. El segundo parámetro adicional es un
objeto definido por el usuario. Este objeto se puede utilizar para pasar información de estado
específica de la aplicación al método invocado cuando la operación asincrónica finaliza. Si un
método BeginnombreDeOperación toma parámetros adicionales específicos de operación, como

MCT: Luis Dueñas Pag 145 de 336


Manual de .NET Framework 4.5

una matriz de bytes para almacenar bytes leídos de un archivo, el objeto de estado de aplicación
y AsyncCallback son los últimos parámetros de la firma del método BeginnombreDeOperación.
Begin nombreDeOperación devuelve inmediatamente el control al subproceso que realiza la
llamada. Si el método BeginnombreDeOperación produce excepciones, será antes de que se
inicie la operación asincrónica. Si el método BeginnombreDeOperación provoca excepciones,
no se invoca el método de devolución de llamada.
Finalizar una operación asincrónica
El método EndnombreDeOperación finaliza la operación asincrónica nombreDeOperación. El
tipo del valor devuelto del método EndnombreDeOperación coincide con el devuelto por su
homólogo sincrónico y es específico de la operación asincrónica. Por ejemplo, el método
EndRead devuelve el número de bytes leídos de FileStream y el método EndGetHostByName
devuelve un objeto IPHostEntry que contiene información acerca de un equipo host. El método
EndnombreDeOperación toma cualquier parámetro out o ref declarado en la firma de la versión
sincrónica del método. Además de los parámetros del método sincrónico, el método
EndnombreDeOperación también incluye un parámetro IAsyncResult. Los llamadores deben
pasar la instancia devuelta por la llamada correspondiente a BeginnombreDeOperación.
Si la operación asincrónica representada por el objeto IAsyncResult no se ha completado cuando
se llama a EndnombreDeOperación, EndnombreDeOperación bloquea el subproceso que realiza
la llamada hasta que se completa la operación asincrónica. Las excepciones generadas por la
operación asincrónica se producen desde el método EndnombreDeOperación. No se ha definido
el efecto de llamar varias veces al método EndnombreDeOperación con el mismo objeto
IAsyncResult. Tampoco se ha definido la llamada al método EndnombreDeOperación con un
objeto IAsyncResult que no fue devuelto por el método Begin relacionado.
Nota
Para cualquiera de los escenarios indefinidos, los implementadores deberían considerar la
posibilidad de producir InvalidOperationException.
Nota
Los implementadores de este modelo de diseño deben avisar al llamador de que la operación
sincrónica ha finalizado estableciendo IsCompleted en True, llamando al método de devolución
de llamada asincrónico (si se ha especificado) y señalizando el objeto AsyncWaitHandle.
Los desarrolladores de aplicaciones disponen de varias opciones de diseño para obtener acceso a
los resultados de la operación asincrónica. La opción correcta depende de si la aplicación tiene
instrucciones que se pueden ejecutar mientras la operación finaliza. Si una aplicación no puede
realizar ningún otro trabajo hasta que reciba los resultados de la operación asincrónica, debe
bloquearse hasta que los resultados estén disponibles. Para establecer el bloqueo hasta que
finalice una operación asincrónica, puede recurrir a uno de los métodos siguientes:
 Llamar a EndnombreDeOperación desde el subproceso principal de la aplicación, lo que
supone bloquear la ejecución de la aplicación hasta que la operación se complete.
 Utilice el objeto AsyncWaitHandle para bloquear la ejecución de la aplicación hasta que
una o más operaciones hayan finalizado.
En el caso de las aplicaciones que no necesariamente deben bloquearse mientras la operación
asincrónica finaliza, puede recurrir a uno de los métodos siguientes:
 Sondear el estado de ejecución de la operación mediante la comprobación periódica de
la propiedad IsCompleted y la realización de una llamada a EndnombreDeOperación
cuando se complete la operación.
 Utilice un delegado AsyncCallback para especificar que se invoque un método cuando
finalice la operación.
2.3.1. Llamar a métodos asincrónicos mediante IAsyncResult

MCT: Luis Dueñas Pag 146 de 336


Manual de .NET Framework 4.5

Los tipos de las bibliotecas de clases de otros fabricantes y de .NET Framework pueden
proporcionar métodos que permitan que una aplicación determinada siga ejecutándose mientras
se llevan a cabo operaciones asincrónicas en subprocesos diferentes con respecto al subproceso
de la aplicación principal. En las siguientes secciones se describen y se proporcionan ejemplos
de código que muestran las diferentes maneras en las que es posible llamar a métodos
asincrónicos que utilicen el modelo de diseño IAsyncResult.
 Bloquear la ejecución de una aplicación al finalizar una operación asincrónica.
 Bloquear la ejecución de una aplicación mediante AsyncWaitHandle.
 Sondear el estado de una operación asincrónica.
 Utilizar un delegado AsyncCallback para finalizar una operación asincrónica.
2.3.1.1. Bloquear la ejecución de una aplicación mediante
AsyncWaitHandle
Las aplicaciones que no pueden seguir realizando otra tarea mientras esperan los resultados de
una operación asincrónica deben bloquearse hasta que la operación finalice. Utilice una de las
opciones siguientes para bloquear el subproceso principal de la aplicación en cuestión a la
espera de que una operación asincrónica finalice:
 Utilice la propiedad AsyncWaitHandle de la interfaz IAsyncResult devuelta por el
método BeginnombreDeOperación de la operación asincrónica. Este tema muestra la
ejecución de dicho procedimiento.
 Llame al método EndnombreDeOperación de la operación asincrónica.
Las aplicaciones que utilizan uno o varios objetos WaitHandle para establecer bloqueos hasta
que se completa una operación asincrónica suelen llamar al método BeginnombreDeOperación,
realizan todas las tareas que se puedan realizar sin tener los resultados de dicha operación y, a
continuación, se bloquean hasta que se completa la operación asincrónica. Una aplicación se
puede bloquear en una única operación llamando a uno de los métodos WaitOne mediante
AsyncWaitHandle. Para bloquear la ejecución de una aplicación mientras espera que finalice un
conjunto de operaciones asincrónicas, almacene los objetos AsyncWaitHandle asociados en una
matriz y llame a uno de los métodos WaitAll. Para bloquear la ejecución de una aplicación
mientras espera que finalice alguno de los conjuntos de operaciones asincrónicas, almacene los
objetos AsyncWaitHandle asociados en una matriz y llame a uno de los métodos WaitAny.
Ejemplo
El ejemplo de código siguiente muestra cómo utilizar los métodos asincrónicos en la clase DNS
para recuperar información sobre el Sistema de nombres de dominio para un equipo
especificado por el usuario. El ejemplo muestra cómo realizar el bloqueo utilizando el objeto
WaitHandle asociado a la operación asincrónica. Observe que se pasa el valor null (Nothing en
Visual Basic) para los parámetros BeginGetHostByName, requestCallback y stateObject, puesto
que no son necesarios cuando se utiliza este procedimiento.
/* The following example demonstrates using asynchronous methods to
get Domain Name System information for the specified host computer.*/
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class WaitUntilOperationCompletes
{
public static void Main(string[] args)
{
// Make sure the caller supplied a host name.
if (args.Length == 0 || args[0].Length == 0)
{
// Print a message and exit.

MCT: Luis Dueñas Pag 147 de 336


Manual de .NET Framework 4.5

Console.WriteLine("You must specify the name of a host


computer.");
return;
}
// Start the asynchronous request for DNS information.
IAsyncResult result = Dns.BeginGetHostEntry(args[0], null, null);
Console.WriteLine("Processing request for information...");
// Wait until the operation completes.
result.AsyncWaitHandle.WaitOne();
// The operation completed. Process the results.
try
{
// Get the results.
IPHostEntry host = Dns.EndGetHostEntry(result);
string[] aliases = host.Aliases;
IPAddress[] addresses = host.AddressList;
if (aliases.Length > 0)
{
Console.WriteLine("Aliases");
for (int i = 0; i < aliases.Length; i++)
{
Console.WriteLine("{0}", aliases[i]);
}
}
if (addresses.Length > 0)
{
Console.WriteLine("Addresses");
for (int i = 0; i < addresses.Length; i++)
{
Console.WriteLine("{0}",addresses[i].ToString());
}
}
}
catch (SocketException e)
{
Console.WriteLine("Exception occurred while processing the
request:
{0}",e.Message);
}
}
}
}

2.3.1.2. Bloquear la ejecución de una aplicación al finalizar una


operación asincrónica
Las aplicaciones que no pueden seguir realizando otra tarea mientras esperan los resultados de
una operación asincrónica deben bloquearse hasta que la operación finalice. Utilice una de las
opciones siguientes para bloquear el subproceso principal de la aplicación en cuestión a la
espera de que una operación asincrónica finalice:
 Llame al método EndnombreDeOperación de la operación asincrónica. Este tema
muestra la ejecución de dicho procedimiento.
 Utilice la propiedad AsyncWaitHandle de la interfaz IAsyncResult devuelta por el
método BeginnombreDeOperación de la operación asincrónica.
Las aplicaciones que utilizan el método EndnombreDeOperación para establecer bloqueos hasta
que se completa una operación asincrónica normalmente llamarán al método
BeginnombreDeOperación, realizarán cualquier tarea que se pueda sin los resultados de la
operación y, a continuación, llamarán a EndnombreDeOperación.
Ejemplo
El siguiente ejemplo de código muestra cómo utilizar los métodos asincrónicos en la clase Dns
con el fin de recuperar información sobre el Sistema de nombres de dominio para un equipo

MCT: Luis Dueñas Pag 148 de 336


Manual de .NET Framework 4.5

especificado por el usuario. Hay que observar que se pasa null (Nothing en Visual Basic) para
los parámetros BeginGetHostByNamerequestCallback y stateObject ya que estos argumentos no
son necesarios a la hora de utilizar este enfoque.
/* The following example demonstrates using asynchronous methods to
get Domain Name System information for the specified host computer.*/
using System;
using System.Net;
using System.Net.Sockets;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class BlockUntilOperationCompletes
{
public static void Main(string[] args)
{
// Make sure the caller supplied a host name.
if (args.Length == 0 || args[0].Length == 0)
{
// Print a message and exit.
Console.WriteLine("You must specify the name of a host
computer.");
return;
}
// Start the asynchronous request for DNS information.
// This example does not use a delegate or user-supplied object
// so the last two arguments are null.
IAsyncResult result = Dns.BeginGetHostEntry(args[0], null, null);
Console.WriteLine("Processing your request for information...");
// Do any additional work that can be done here.
try
{
// EndGetHostByName blocks until the process completes.
IPHostEntry host = Dns.EndGetHostEntry(result);
string[] aliases = host.Aliases;
IPAddress[] addresses = host.AddressList;
if (aliases.Length > 0)
{
Console.WriteLine("Aliases");
for (int i = 0; i < aliases.Length; i++)
{
Console.WriteLine("{0}", aliases[i]);
}
}
if (addresses.Length > 0)
{
Console.WriteLine("Addresses");
for (int i = 0; i < addresses.Length; i++)
{
Console.WriteLine("{0}",addresses[i].ToString());
}
}
}
catch (SocketException e)
{
Console.WriteLine("An exception occurred while processing the
request:
{0}", e.Message);
}
}
}
}

2.3.1.3. Sondear el estado de una operación asincrónica


Aquellas aplicaciones que pueden realizar otra tarea mientras esperan los resultados de una
operación asincrónica, no se deberían bloquear mientras esperan a que la operación finalice.

MCT: Luis Dueñas Pag 149 de 336


Manual de .NET Framework 4.5

Utilice una de las siguientes opciones para seguir ejecutando instrucciones mientras se está a la
espera de que una operación asincrónica finalice:
 Utilice la propiedad IsCompleted de la interfaz IAsyncResult devuelta por el método
BeginnombreDeOperación de la operación asincrónica para determinar si la operación
se ha completado. Este enfoque se conoce como sondeo y se muestra en este tema.
 Utilice un delegado AsyncCallback para procesar los resultados de la operación
asincrónica en un subproceso independiente.
Ejemplo
El siguiente ejemplo de código muestra cómo utilizar los métodos asincrónicos en la clase Dns
con el fin de recuperar información sobre el Sistema de nombres de dominio para un equipo
especificado por el usuario. Este ejemplo inicia la operación asincrónica y, a continuación,
imprime puntos (".") en la consola hasta que la operación se haya completado. Hay que observar
que se pasa null (Nothing en Visual Basic) para los parámetros
BeginGetHostByNameAsyncCallback y Object ya que estos argumentos no son necesarios a la
hora de utilizar este enfoque.
/* The following example demonstrates using asynchronous methods to
get Domain Name System information for the specified host computer.
This example polls to detect the end of the asynchronous operation.*/
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class PollUntilOperationCompletes
{
static void UpdateUserInterface()
{
// Print a period to indicate that the application
// is still working on the request.
Console.Write(".");
}

public static void Main(string[] args)


{
// Make sure the caller supplied a host name.
if (args.Length == 0 || args[0].Length == 0)
{
// Print a message and exit.
Console.WriteLine("You must specify the name of a host
computer.");
return;
}
// Start the asychronous request for DNS information.
IAsyncResult result = Dns.BeginGetHostEntry(args[0], null, null);
Console.WriteLine("Processing request for information...");
// Poll for completion information.
// Print periods (".") until the operation completes.
while (result.IsCompleted != true)
{
UpdateUserInterface();
}
// The operation is complete. Process the results.
// Print a new line.
Console.WriteLine();
try
{
IPHostEntry host = Dns.EndGetHostEntry(result);
string[] aliases = host.Aliases;
IPAddress[] addresses = host.AddressList;
if (aliases.Length > 0)

MCT: Luis Dueñas Pag 150 de 336


Manual de .NET Framework 4.5

{
Console.WriteLine("Aliases");
for (int i = 0; i < aliases.Length; i++)
{
Console.WriteLine("{0}", aliases[i]);
}
}
if (addresses.Length > 0)
{
Console.WriteLine("Addresses");
for (int i = 0; i < addresses.Length; i++)
{
Console.WriteLine("{0}",addresses[i].ToString());
}
}
}
catch (SocketException e)
{
Console.WriteLine("An exception occurred while processing the
request:
{0}", e.Message);
}
}
}
}

2.3.1.4. Utilizar un delegado AsyncCallback para finalizar una


operación asincrónica
Aquellas aplicaciones que pueden realizar otra tarea mientras esperan los resultados de una
operación asincrónica, no se deberían bloquear mientras esperan a que la operación finalice.
Utilice una de las siguientes opciones para seguir ejecutando instrucciones mientras se está a la
espera de que una operación asincrónica finalice:
 Utilice un delegado AsyncCallback para procesar los resultados de la operación
asincrónica en un subproceso independiente. Este tema muestra la ejecución de dicho
procedimiento.
 Utilice la propiedad IsCompleted de la interfaz IAsyncResult devuelta por el método
BeginnombreDeOperación de la operación asincrónica para determinar si la operación
se ha completado.
Ejemplo
El siguiente ejemplo de código muestra cómo utilizar los métodos asincrónicos en la clase Dns
con el fin de recuperar información sobre el Sistema de nombres de dominio (DNS) para
equipos especificados por el usuario. En este ejemplo, se crea un delegado AsyncCallback que
hace referencia el método ProcessDnsInformation. Se llama a este método una vez por cada
solicitud asincrónica con el fin de obtener información DNS.
Obsérvese que el host especificado por el usuario se pasa al parámetro BeginGetHostByName.
/* The following example demonstrates using asynchronous methods to
get Domain Name System information for the specified host computers.
This example uses a delegate to obtain the results of each asynchronous
operation.*/
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections.Specialized;
using System.Collections;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class UseDelegateForAsyncCallback
{

MCT: Luis Dueñas Pag 151 de 336


Manual de .NET Framework 4.5

static int requestCounter;


static ArrayList hostData = new ArrayList();
static StringCollection hostNames = new StringCollection();
static void UpdateUserInterface()
{
// Print a message to indicate that the application
// is still working on the remaining requests.
Console.WriteLine("{0} requests remaining.", requestCounter);
}
public static void Main()
{
// Create the delegate that will process the results of the
// asynchronous request.
AsyncCallback callBack = new AsyncCallback(ProcessDnsInformation);
string host;
do
{
Console.Write(" Enter the name of a host computer or <enter>
to
finish: ");
host = Console.ReadLine();
if (host.Length > 0)
{
// Increment the request counter in a thread safe manner.
Interlocked.Increment(ref requestCounter);
// Start the asynchronous request for DNS information.
Dns.BeginGetHostEntry(host, callBack, host);
}
} while (host.Length > 0);
// The user has entered all of the host names for lookup.
// Now wait until the threads complete.
while (requestCounter > 0)
{
UpdateUserInterface();
}
// Display the results.
for (int i = 0; i< hostNames.Count; i++)
{
object data = hostData [i];
string message = data as string;
// A SocketException was thrown.
if (message != null)
{
Console.WriteLine("Request for {0} returned message: {1}",
hostNames[i], message);
continue;
}
// Get the results.
IPHostEntry h = (IPHostEntry) data;
string[] aliases = h.Aliases;
IPAddress[] addresses = h.AddressList;
if (aliases.Length > 0)
{
Console.WriteLine("Aliases for {0}", hostNames[i]);
for (int j = 0; j < aliases.Length; j++)
{
Console.WriteLine("{0}", aliases[j]);
}
}
if (addresses.Length > 0)
{
Console.WriteLine("Addresses for {0}", hostNames[i]);
for (int k = 0; k < addresses.Length; k++)
{
Console.WriteLine("{0}",addresses[k].ToString());
}
}
}

MCT: Luis Dueñas Pag 152 de 336


Manual de .NET Framework 4.5

// The following method is called when each asynchronous operation


completes.
static void ProcessDnsInformation(IAsyncResult result)
{
string hostName = (string) result.AsyncState;
hostNames.Add(hostName);
try
{
// Get the results.
IPHostEntry host = Dns.EndGetHostEntry(result);
hostData.Add(host);
}
// Store the exception message.
catch (SocketException e)
{
hostData.Add(e.Message);
}
finally
{
// Decrement the request counter in a thread-safe manner.
Interlocked.Decrement(ref requestCounter);
}
}
}
}

2.3.1.4.1. Utilizar un delegado AsyncCallback y un objeto State


Cuando se utiliza un delegado de AsyncCallback para procesar los resultados de la operación
asincrónica en un subproceso independiente, se puede utilizar un objeto de estado para pasar
información entre las devoluciones de llamada y recuperar un resultado final. Este tema ilustra
esta práctica con la expansión del ejemplo en Utilizar un delegado AsyncCallback para finalizar
una operación asincrónica.
Ejemplo
El siguiente ejemplo de código muestra cómo utilizar los métodos asincrónicos en la clase Dns
con el fin de recuperar información sobre el Sistema de nombres de dominio (DNS) para
equipos especificados por el usuario. Este ejemplo define y utiliza la clase HostRequest para
almacenar información de estado. Un objeto HostRequest se crea para cada nombre de equipo
escrito por el usuario. Este objeto se pasa al método BeginGetHostByName. Se llama al método
ProcessDnsInformation cada vez que una solicitud finaliza. El objeto HostRequest se recupera
utilizando la propiedad AsyncState. El método ProcessDnsInformation utiliza el objeto
HostRequest para almacenar un IPHostEntry devuelto por la solicitud o un SocketException
producido por la solicitud. Una vez que todas las solicitudes han finalizado, la aplicación recorre
en iteración los objetos HostRequest y muestra la información DNS o mensaje de error
SocketException.
/*The following example demonstrates using asynchronous methods to
get Domain Name System information for the specified host computer.*/
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
// Create a state object that holds each requested host name,
// an associated IPHostEntry object or a SocketException.
public class HostRequest
{
// Stores the requested host name.

MCT: Luis Dueñas Pag 153 de 336


Manual de .NET Framework 4.5

private string hostName;


// Stores any SocketException returned by the Dns EndGetHostByName
method.
private SocketException e;
// Stores an IPHostEntry returned by the Dns EndGetHostByName method.
private IPHostEntry entry;

public HostRequest(string name)


{
hostName = name;
}

public string HostName


{
get
{
return hostName;
}
}

public SocketException ExceptionObject


{
get
{
return e;
}
set
{
e = value;
}
}

public IPHostEntry HostEntry


{
get
{
return entry;
}
set
{
entry = value;
}
}
}

public class UseDelegateAndStateForAsyncCallback


{
// The number of pending requests.
static int requestCounter;
static ArrayList hostData = new ArrayList();
static void UpdateUserInterface()
{
// Print a message to indicate that the application
// is still working on the remaining requests.
Console.WriteLine("{0} requests remaining.", requestCounter);
}
public static void Main()
{
// Create the delegate that will process the results of the
// asynchronous request.
AsyncCallback callBack = new AsyncCallback(ProcessDnsInformation);
string host;
do
{
Console.Write(" Enter the name of a host computer or <enter>
to
finish: ");
host = Console.ReadLine();

MCT: Luis Dueñas Pag 154 de 336


Manual de .NET Framework 4.5

if (host.Length > 0)
{
// Increment the request counter in a thread safe manner.
Interlocked.Increment(ref requestCounter);
// Create and store the state object for this request.
HostRequest request = new HostRequest(host);
hostData.Add(request);
// Start the asynchronous request for DNS information.
Dns.BeginGetHostEntry(host, callBack, request);
}
} while (host.Length > 0);
// The user has entered all of the host names for lookup.
// Now wait until the threads complete.
while (requestCounter > 0)
{
UpdateUserInterface();
}
// Display the results.
foreach(HostRequest r in hostData)
{
if (r.ExceptionObject != null)
{
Console.WriteLine("Request for host {0} returned the
following
error: {1}.", r.HostName, r.ExceptionObject.Message);
}
else
{
// Get the results.
IPHostEntry h = r.HostEntry;
string[] aliases = h.Aliases;
IPAddress[] addresses = h.AddressList;
if (aliases.Length > 0)
{
Console.WriteLine("Aliases for {0}", r.HostName);
for (int j = 0; j < aliases.Length; j++)
{
Console.WriteLine("{0}", aliases[j]);
}
}
if (addresses.Length > 0)
{
Console.WriteLine("Addresses for {0}",
r.HostName);
for (int k = 0; k < addresses.Length; k++)
{

Console.WriteLine("{0}",addresses[k].ToString());
}
}
}
}
}

// The following method is invoked when each asynchronous operation


completes.
static void ProcessDnsInformation(IAsyncResult result)
{
// Get the state object associated with this request.
HostRequest request = (HostRequest) result.AsyncState;
try
{
// Get the results and store them in the state object.
IPHostEntry host = Dns.EndGetHostEntry(result);
request.HostEntry = host;
}
catch (SocketException e)
{

MCT: Luis Dueñas Pag 155 de 336


Manual de .NET Framework 4.5

// Store any SocketExceptions.


request.ExceptionObject = e;
}
finally
{
// Decrement the request counter in a thread-safe manner.
Interlocked.Decrement(ref requestCounter);
}
}
}
}

2.3.2. Programación asincrónica mediante delegados


Los delegados permiten llamar a métodos sincrónicos de forma asincrónica. Cuando se llama a
un delegado sincrónicamente, el método Invoke llama al método de destino directamente en el
subproceso actual. Si se llama al método BeginInvoke, Common Language Runtime (CLR)
coloca en cola la solicitud y devuelve inmediatamente el control al elemento que lo llamó. El
método de destino se llama asincrónicamente desde un subproceso del grupo de subprocesos. El
subproceso original, que envió la solicitud, puede continuar ejecutándose en paralelo con el
método de destino. Si se ha especificado un método de devolución de llamada en la llamada al
método BeginInvoke, el método de devolución de llamada se llama cuando finaliza el método
de destino. En el método de devolución de llamada, el método EndInvoke obtiene el valor
devuelto y cualquier parámetro de entrada y salida o de solo salida. Si no se especifica ningún
método de devolución de llamada al llamar a BeginInvoke, se puede llamar a EndInvoke desde
el subproceso que llamó a BeginInvoke.
Importante
Los compiladores deben emitir clases de delegados con los métodos Invoke, BeginInvoke y
EndInvoke mediante la firma de delegados que especifique el usuario. Los métodos EndInvoke
y BeginInvoke deben representarse como nativos. Debido a que estos métodos están marcados
como nativos, Common Language Runtime proporciona automáticamente la implementación en
el momento en que se carga la clase. El cargador se asegura de que no se reemplazarán.

2.3.2.1. Llamar a métodos sincrónicos de forma asincrónica


.NET Framework permite llamar a cualquier método de forma asincrónica. Para ello, es
necesario que defina un delegado con la misma firma que el método al que desea llamar.
Common Language Runtime definirá automáticamente los métodos BeginInvoke y EndInvoke
para este delegado, con las firmas adecuadas.
Nota
Las llamadas de delegado asincrónicas, específicamente los métodos EndInvoke y BeginInvoke,
no se admiten en .NET Compact Framework.
El método BeginInvoke inicia la llamada asincrónica. Tiene los mismos parámetros que el
método que desea ejecutar de forma asincrónica, más dos parámetros opcionales adicionales. El
primer parámetro es un delegado AsyncCallback que hace referencia a un método que se habrá
de llamar cuando finalice la llamada asincrónica. El segundo parámetro es un objeto definido
por el usuario que pasa información al método de devolución de llamada. BeginInvoke vuelve
inmediatamente y no espera que se complete la llamada asincrónica. BeginInvoke devuelve
IAsyncResult, que se puede usar para supervisar el progreso de la llamada asincrónica.
El método EndInvoke recupera los resultados de la llamada asincrónica. Se puede llamar en
cualquier momento después de ejecutar BeginInvoke. Si la llamada asincrónica no ha
completado, EndInvoke bloquea el subproceso que realiza la llamada hasta que se completa.
Entre los parámetros de EndInvoke se incluyen out y ref (<Out> ByRef y ByRef en Visual
Basic) del método que desea ejecutar de forma asincrónica, además de la interfaz IAsyncResult
devuelta por BeginInvoke.

MCT: Luis Dueñas Pag 156 de 336


Manual de .NET Framework 4.5

Nota
La característica IntelliSense en Visual Studio 2005 muestra los parámetros de BeginInvoke y
EndInvoke. Si no está utilizando Visual Studio u otra herramienta similar, o si está utilizando
C# con Visual Studio 2005, consulte Modelo de programación asincrónica (APM), donde
encontrará una descripción de los parámetros definidos para estos métodos.
En los ejemplos de código de este tema se muestran cuatro de las formas más comunes de
utilizar los métodos BeginInvoke y EndInvoke para realizar llamadas asincrónicas. Después de
llamar a BeginInvoke, puede hacer lo siguiente:
 Realizar algunas operaciones y, a continuación, llamar al método EndInvoke para que
mantenga un bloqueo hasta que se complete la llamada.
 Obtener un objeto WaitHandle mediante la propiedad IAsyncResult.AsyncWaitHandle,
utilizar su método WaitOne para bloquear la ejecución hasta que se señalice
WaitHandle y, a continuación, llamar al método EndInvoke.
 Sondear el resultado IAsyncResult devuelto por BeginInvoke para determinar cuándo se
completa la llamada asincrónica y, a continuación, llamar al método EndInvoke.
 Pasar un delegado de un método de devolución de llamada a BeginInvoke. El método se
ejecuta en un subproceso ThreadPool una vez finalizada la llamada asincrónica. El
método de devolución de llamada llama a EndInvoke.
Importante
Con independencia de la técnica que utilice, llame siempre a EndInvoke para completar la
llamada asincrónica.
Definir el método Test y el delegado asincrónico
En los ejemplos de código siguientes se muestran distintas maneras de llamar al mismo método
de ejecución prolongada, TestMethod, de forma asincrónica. El método TestMethod muestra un
mensaje en la consola para indicar que ha comenzado el procesamiento, espera unos segundos y,
a continuación, finaliza. TestMethod tiene un parámetro out para mostrar la manera en que esos
parámetros se agregan a las firmas de BeginInvoke y EndInvoke. Los parámetros ref se pueden
controlar de manera similar.
En el ejemplo de código siguiente se muestra la definición de TestMethod y el delegado
denominado AsyncMethodCaller que se puede utilizar para llamar a TestMethod de forma
asincrónica. Para compilar cualquiera de los ejemplos de código, debe incluir las definiciones
del método TestMethod y el delegado AsyncMethodCaller.
Ejemplo
using System;
using System.Threading;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class AsyncDemo
{
// The method to be executed asynchronously.
public string TestMethod(int callDuration, out int threadId)
{
Console.WriteLine("Test method begins.");
Thread.Sleep(callDuration);
threadId = Thread.CurrentThread.ManagedThreadId;
return String.Format("My call time was {0}.",
callDuration.ToString());
}
}
// The delegate must have the same signature as the method
// it will call asynchronously.
public delegate string AsyncMethodCaller(int callDuration, out int
threadId);

MCT: Luis Dueñas Pag 157 de 336


Manual de .NET Framework 4.5

Esperar una llamada asincrónica con EndInvoke


La manera más sencilla de ejecutar un método de forma asincrónica es empezar a ejecutar el
método llamando al método BeginInvoke del delegado, hacer algún trabajo en el subproceso
principal y, a continuación, llamar al método EndInvoke del delegado. EndInvoke podrían
bloquear el subproceso que realiza la llamada porque no vuelve hasta que no se completa la
llamada asincrónica. Ésta es una buena técnica para utilizarla con operaciones de archivos o red.
Importante
Dado que EndInvoke podría mantener un bloqueo, nunca debe llamar a este método desde los
subprocesos que dan servicio a la interfaz de usuario.
Ejemplo
using System;
using System.Threading;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class AsyncMain
{
public static void Main()
{
// The asynchronous method puts the thread id here.
int threadId;
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult result = caller.BeginInvoke(3000, out threadId, null,
null);
Thread.Sleep(0);
Console.WriteLine("Main thread {0} does some work.",
Thread.CurrentThread.ManagedThreadId);
// Call EndInvoke to wait for the asynchronous call to complete,
// and to retrieve the results.
string returnValue = caller.EndInvoke(out threadId, result);
Console.WriteLine("The call executed on thread {0}, with return
value
\"{1}\".", threadId, returnValue);
}
}
}
/* This example produces output similar to the following
Main thread 1 does some work.
Test method begins.
The call executed on thread 3, with return value "My call time was 3000.". */

Esperar una llamada asincrónica con WaitHandle


Puede obtener un objeto WaitHandle utilizando la propiedad AsyncWaitHandle de
IAsyncResult devuelta por BeginInvoke. WaitHandle se señaliza cuando finaliza la llamada
asincrónica y puede esperar a que termine llamando al método WaitOne.
Si utiliza un objeto WaitHandle, puede realizar otros procesamientos adicionales antes o
después de que se complete la llamada asincrónica, pero antes de llamar al método EndInvoke
para recuperar los resultados.
Nota
El identificador de espera no se cierra automáticamente cuando llama a EndInvoke. Si libera
todas las referencias al identificador de espera, se liberarán los recursos del sistema cuando la

MCT: Luis Dueñas Pag 158 de 336


Manual de .NET Framework 4.5

recolección de elementos no utilizados reclame el identificador de espera. Para liberar los


recursos del sistema tan pronto como se deje de utilizar el identificador de espera, elimínelo
llamando al método WaitHandle.Close. La recolección de elementos no utilizados funciona más
eficazmente cuando los objetos descartables se eliminan de forma explícita.
Ejemplo
using System;
using System.Threading;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class AsyncMain
{
static void Main()
{
// The asynchronous method puts the thread id here.
int threadId;
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult result = caller.BeginInvoke(3000, out threadId, null,
null);
Thread.Sleep(0);
Console.WriteLine("Main thread {0} does some work.",
Thread.CurrentThread.ManagedThreadId);
// Wait for the WaitHandle to become signaled.
result.AsyncWaitHandle.WaitOne();
// Perform additional processing here.
// Call EndInvoke to retrieve the results.
string returnValue = caller.EndInvoke(out threadId, result);
// Close the wait handle.
result.AsyncWaitHandle.Close();
Console.WriteLine("The call executed on thread {0}, with return
value
\"{1}\".", threadId, returnValue);
}
}
}
/* This example produces output similar to the following:
Main thread 1 does some work.
Test method begins.
The call executed on thread 3, with return value "My call time was 3000.". */

Sondear la finalización de una llamada asincrónica


Puede utilizar la propiedad IsCompleted del objeto IAsyncResult devuelto por BeginInvoke para
detectar el momento en que se completa la llamada asincrónica. Puede hacer esto último cuando
realice la llamada asincrónica desde un subproceso que dé servicio a la interfaz de usuario.
Sondear la finalización de una llamada asincrónica permite al subproceso de llamada seguirse
ejecutando mientras la llamada asincrónica se ejecuta en un subproceso ThreadPool.
using System;
using System.Threading;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class AsyncMain
{
static void Main() {
// The asynchronous method puts the thread id here.
int threadId;
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.

MCT: Luis Dueñas Pag 159 de 336


Manual de .NET Framework 4.5

AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod);


// Initiate the asychronous call.
IAsyncResult result = caller.BeginInvoke(3000, out threadId, null,
null);
// Poll while simulating work.
while(result.IsCompleted == false) {
Thread.Sleep(250);
Console.Write(".");
}
// Call EndInvoke to retrieve the results.
string returnValue = caller.EndInvoke(out threadId, result);
Console.WriteLine("\nThe call executed on thread {0}, with return
value
\"{1}\".",threadId, returnValue);
}
}
}
/* This example produces output similar to the following:
Test method begins.
.............
The call executed on thread 3, with return value "My call time was 3000.".*/

Ejecutar un método de devolución de llamada cuando finaliza una llamada asincrónica


Si no es necesario que el subproceso que inicia la llamada asincrónica sea el mismo que procesa
los resultados, puede ejecutar un método de devolución de llamada cuando se complete la
llamada. El método de devolución de llamada se ejecuta en un subproceso ThreadPool.
Para utilizar un método de devolución de llamada, debe pasar al método BeginInvoke un
delegado AsyncCallback que represente al método de devolución de llamada. También puede
pasar un objeto que contenga la información que va a utilizar el método de devolución de
llamada. En el método de devolución de llamada, puede convertir IAsyncResult, que es el único
parámetro del método de devolución de llamada, en un objeto AsyncResult. A continuación,
puede utilizar la propiedad AsyncResult.AsyncDelegate para obtener el delegado que se utilizó
para iniciar la llamada y, de ese modo, pueda llamar a EndInvoke.
Notas sobre el ejemplo:
 El parámetro threadId de TestMethod es un parámetro out (<Out> ByRef en Visual
Basic), por lo que TestMethod nunca utiliza el valor de entrada. Una variable ficticia se
pasa a la llamada a BeginInvoke. Si el parámetro threadId fuera un parámetro ref
(ByRef en Visual Basic), la variable tendría que ser un campo de nivel de clase para que
pudiera pasarse a los métodos BeginInvoke y EndInvoke.
 La información de estado que se pasa a BeginInvoke es una cadena de formato, que el
método de devolución de llamada utiliza para dar formato a un mensaje de salida. Dado
que se pasa como un tipo Object, la información de estado tiene que convertirse a su
tipo apropiado antes de poderse utilizar.
 La devolución de llamada se realiza en un subproceso ThreadPool. Los subprocesos
ThreadPool son subprocesos en segundo plano, que no mantienen la aplicación en
ejecución si el subproceso principal finaliza, por lo que el subproceso principal del
ejemplo debe permanecer en suspensión el tiempo suficiente para que la devolución de
llamada finalice.
using System;
using System.Threading;
using System.Runtime.Remoting.Messaging;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
public class AsyncMain
{
static void Main()
{
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();

MCT: Luis Dueñas Pag 160 de 336


Manual de .NET Framework 4.5

// Create the delegate.


AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod);
// The threadId parameter of TestMethod is an out parameter, so
// its input value is never used by TestMethod. Therefore, a dummy
// variable can be passed to the BeginInvoke call. If the threadId
// parameter were a ref parameter, it would have to be a class-
// level field so that it could be passed to both BeginInvoke and
// EndInvoke.
int dummy = 0;
// Initiate the asynchronous call, passing three seconds (3000 ms)
// for the callDuration parameter of TestMethod; a dummy variable
// for the out parameter (threadId); the callback delegate; and
// state information that can be retrieved by the callback method.
// In this case, the state information is a string that can be
used
// to format a console message.
IAsyncResult result = caller.BeginInvoke(3000,out dummy,
new AsyncCallback(CallbackMethod),
"The call executed on thread {0}, with return value
\"{1}\".");
Console.WriteLine("The main thread {0} continues to execute...",
Thread.CurrentThread.ManagedThreadId);
// The callback is made on a ThreadPool thread. ThreadPool threads
// are background threads, which do not keep the application
running
// if the main thread ends. Comment out the next line to
demonstrate
// this.
Thread.Sleep(4000);
Console.WriteLine("The main thread ends.");
}

// The callback method must have the same signature as the


// AsyncCallback delegate.
static void CallbackMethod(IAsyncResult ar)
{
// Retrieve the delegate.
AsyncResult result = (AsyncResult) ar;
AsyncMethodCaller caller = (AsyncMethodCaller)
result.AsyncDelegate;
// Retrieve the format string that was passed as state
// information.
string formatString = (string) ar.AsyncState;
// Define a variable to receive the value of the out parameter.
// If the parameter were ref rather than out then it would have to
// be a class-level field so it could also be passed to
BeginInvoke.
int threadId = 0;
// Call EndInvoke to retrieve the results.
string returnValue = caller.EndInvoke(out threadId, ar);
// Use the format string to format the output message.
Console.WriteLine(formatString, threadId, returnValue);
}
}
}
/* This example produces output similar to the following:
The main thread 1 continues to execute...
Test method begins.
The call executed on thread 3, with return value "My call time was 3000.".
The main thread ends.*/

2.3.2.2. Ejemplo de programación de delegados asincrónicos


El siguiente ejemplo de código muestra cómo utilizar la programación asincrónica de .NET
mediante una clase que factoriza algunos números. Este ejemplo define una clase con un método
Factorize único que calcula los factores principales de un número especificado. El ejemplo
también define un delegado denominado AsyncFactorCaller con una firma que coincide con la

MCT: Luis Dueñas Pag 161 de 336


Manual de .NET Framework 4.5

firma del método Factorize. Los métodos utilizan el delegado en la clase


DemonstrateAsyncPattern para llamar de forma asincrónica al método Factorize. El método
FactorizeNumberUsingCallback muestra cómo utilizar un delegado AsyncCallback para
finalizar la operación asincrónica y obtener los resultados. El método FactorizeNumberAndWait
muestra el sondeo periódico para determinar si la operación ha finalizado.
Ejemplo
using System;
using System.Threading;
using System.Runtime.Remoting.Messaging;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
// Create a class that factors a number.
public class PrimeFactorFinder
{
public static bool Factorize(int number,ref int primefactor1,ref int
primefactor2)
{
primefactor1 = 1;
primefactor2 = number;
// Factorize using a low-tech approach.
for (int i=2;i<number;i++)
{
if (0 == (number % i))
{
primefactor1 = i;
primefactor2 = number / i;
break;
}
}
if (1 == primefactor1 )
return false;
else
return true;
}
}

// Create an asynchronous delegate that matches the Factorize method.


public delegate bool AsyncFactorCaller (int number, ref int primefactor1,
ref int primefactor2);

public class DemonstrateAsyncPattern


{
// The waiter object used to keep the main application thread
// from terminating before the callback method completes.
ManualResetEvent waiter;

// Define the method that receives a callback when the results are
available.
public void FactorizedResults(IAsyncResult result)
{
int factor1=0;
int factor2=0;
// Extract the delegate from the
// System.Runtime.Remoting.Messaging.AsyncResult.
AsyncFactorCaller factorDelegate =
(AsyncFactorCaller)((AsyncResult)result).AsyncDelegate;
int number = (int) result.AsyncState;
// Obtain the result.
bool answer = factorDelegate.EndInvoke(ref factor1, ref factor2,
result);
// Output the results.
Console.WriteLine("On CallBack: Factors of {0} : {1} {2} - {3}",
number, factor1, factor2, answer);
waiter.Set();

MCT: Luis Dueñas Pag 162 de 336


Manual de .NET Framework 4.5

// The following method demonstrates the asynchronous pattern using a


callback
// method.
public void FactorizeNumberUsingCallback()
{
AsyncFactorCaller factorDelegate = new AsyncFactorCaller
(PrimeFactorFinder.Factorize);
int number = 1000589023;
int temp=0;
// Waiter will keep the main application thread from
// ending before the callback completes because
// the main thread blocks until the waiter is signaled
// in the callback.
waiter = new ManualResetEvent(false);
// Define the AsyncCallback delegate.
AsyncCallback callBack = new AsyncCallback(this.FactorizedResults);
// Asynchronously invoke the Factorize method.
IAsyncResult result = factorDelegate.BeginInvoke(number, ref temp,
ref temp, callBack, number);
// Do some other useful work while
// waiting for the asynchronous operation to complete.
// When no more work can be done, wait.
waiter.WaitOne();
}

// The following method demonstrates the asynchronous pattern


// using a BeginInvoke, followed by waiting with a time-out.
public void FactorizeNumberAndWait()
{
AsyncFactorCaller factorDelegate = new AsyncFactorCaller
(PrimeFactorFinder.Factorize);
int number = 1000589023;
int temp=0;
// Asynchronously invoke the Factorize method.
IAsyncResult result = factorDelegate.BeginInvoke(number,
ref temp, ref temp, null, null);
while (!result.IsCompleted)
{
// Do any work you can do before waiting.
result.AsyncWaitHandle.WaitOne(10000, false);
}
result.AsyncWaitHandle.Close();
// The asynchronous operation has completed.
int factor1=0;
int factor2=0;
// Obtain the result.
bool answer = factorDelegate.EndInvoke(ref factor1, ref factor2,
result);
// Output the results.
Console.WriteLine("Sequential : Factors of {0} : {1} {2} - {3}",
number, factor1, factor2, answer);
}

public static void Main()


{
DemonstrateAsyncPattern demonstrator = new
DemonstrateAsyncPattern();
demonstrator.FactorizeNumberUsingCallback();
demonstrator.FactorizeNumberAndWait();
}
}
}

MCT: Luis Dueñas Pag 163 de 336


Manual de .NET Framework 4.5

3. Programación paralela en .NET Framework


Muchos equipos y estaciones de trabajo tienen dos o cuatro núcleos (es decir, CPU) que
permiten ejecutar varios subprocesos simultáneamente. Se espera que los equipos en un futuro
cercano tengan significativamente más núcleos. Para aprovecharse del hardware de hoy y del
mañana, puede paralelizar el código para distribuir el trabajo entre varios procesadores. En el
pasado, la paralelización requería manipulación de bajo nivel de los subprocesos y bloqueos.
Visual Studio 2010 y .NET Framework 4 mejoran la compatibilidad para la programación
paralela proporcionando un nuevo runtime, nuevos tipos de biblioteca de clases y nuevas
herramientas de diagnóstico. Estas características simplifican el desarrollo en paralelo, de modo
que pueda escribir código paralelo eficaz, específico y escalable de forma natural sin tener que
trabajar directamente con subprocesos ni el bloque de subprocesos. La siguiente ilustración
proporciona una información general de alto nivel de la arquitectura de programación paralela
en .NET Framework 4.

3.1. Biblioteca de procesamiento paralelo basado en tareas (TPL)


La biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas)
es un conjunto de API y tipos públicos de los espacios de nombres System.Threading.Tasks y
System.Threading de .NET Framework 4. El propósito de la biblioteca TPL es aumentar la
productividad de los desarrolladores al simplificar el proceso de agregar paralelismo y
simultaneidad a las aplicaciones. La biblioteca TPL escala el grado de simultaneidad de forma
dinámica para usar más eficazmente todos los procesadores que están disponibles. Además, la
TPL se encarga de la división del trabajo, la programación de los subprocesos en ThreadPool, la
compatibilidad con la cancelación, la administración de los estados y otros detalles de bajo
nivel. Al utilizar la TPL, el usuario puede optimizar el rendimiento del código mientras se centra
en el trabajo para el que el programa está diseñado.
A partir de .NET Framework 4, la TPL es el modo preferido de escribir código paralelo y
multiproceso. Sin embargo, no todo el código se presta para la paralelización; por ejemplo, si un
bucle realiza solo una cantidad reducida de trabajo en cada iteración o no se ejecuta para un gran
número de iteraciones, la sobrecarga de la paralelización puede dar lugar a una ejecución más
lenta del código. Además, al igual que cualquier código multiproceso, la paralelización hace que
la ejecución del programa sea más compleja. Aunque la TPL simplifica los escenarios de
multithreading, recomendamos tener conocimientos básicos sobre conceptos de
subprocesamiento, por ejemplo, bloqueos, interbloqueos y condiciones de carrera, para usar la
TPL eficazmente.
3.1.1. Paralelismo de datos (Task Parallel Library)

MCT: Luis Dueñas Pag 164 de 336


Manual de .NET Framework 4.5

El paralelismo de datos hace referencia a los escenarios en los que la misma operación se realiza
simultáneamente (es decir, en paralelo) en elementos de una colección o matriz de origen. En las
operaciones paralelas de datos, se crean particiones de la colección de origen para que varios
subprocesos puedan funcionar simultáneamente en segmentos diferentes.
La biblioteca (TPL) paralelas task admite el paralelismo de datos a través de la clase de
System.Threading.Tasks.Parallel. Esta clase proporciona implementaciones paralelas método-
basadas de para y los bucles de foreach (For y For Each en Visual Basic). Se escribe la lógica
del bucle para un bucle Parallel.For o Parallel.ForEach de forma muy similar a como se
escribiría un bucle secuencial. No tiene que crear los subprocesos ni poner en la cola los
elementos de trabajo. En bucles básicos, no es preciso tomar bloqueos. TPL administra todo el
trabajo de bajo nivel. En el siguiente ejemplo de código se muestra un bucle foreach simple y su
equivalente paralelo.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la TPL.
// Sequential version
foreach (var item in sourceCollection)
{
Process(item);
}
// Parallel equivalent
Parallel.ForEach(sourceCollection, item => Process(item));

Cuando un bucle paralelo se ejecuta, la TPL crea particiones del origen de datos para que el
bucle pueda funcionar simultáneamente en varias partes. En segundo plano, el programador de
tareas crea particiones de la tarea según los recursos del sistema y la carga de trabajo. Cuando es
posible, el programador redistribuye el trabajo entre varios subprocesos y procesadores si se
desequilibra la carga de trabajo.
Nota
También puede proporcionar un programador o creador de particiones personalizado.
Los métodos Parallel.ForEach y Parallel.For tienen varias sobrecargas que permiten detener o
ejecutar la ejecución de bucles, supervisar el estado del bucle en otros subprocesos, mantener el
estado de subprocesos locales, finalizar los objetos de subprocesos locales, controlar el grado de
simultaneidad, etc. Los tipos de aplicación auxiliar que habilitan esta incluyen
ParallelLoopState, ParallelOptions, Parallel LoopResult, CancellationToken, y
CancellationTokenSource de la funcionalidad.
PLINQ admite el paralelismo de datos con sintaxis declarativa o de consulta.
3.1.1.1. Cómo: Escribir un bucle Parallel.For simple
En este ejemplo se muestra cómo utilizar la sobrecarga más simple del método Parallel.For para
calcular el producto de dos matrices. También se muestra cómo utilizar la clase
System.Diagnostics.Stopwatch para comparar el rendimiento de un bucle paralelo con un bucle
no paralelo.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la TPL.
Ejemplo
namespace MultiplyMatrices
{
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Diagnostics;

MCT: Luis Dueñas Pag 165 de 336


Manual de .NET Framework 4.5

using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Program
{
#region Sequential_Loop
static void MultiplyMatricesSequential(double[,] matA, double[,] matB,
double[,] result)
{
int matACols = matA.GetLength(1);
int matBCols = matB.GetLength(1);
int matARows = matA.GetLength(0);
for (int i = 0; i < matARows; i++)
{
for (int j = 0; j < matBCols; j++)
{
for (int k = 0; k < matACols; k++)
{
result[i, j] += matA[i, k] * matB[k, j];
}
}
}
}
#endregion

#region Parallel_Loop

static void MultiplyMatricesParallel(double[,] matA, double[,] matB,


double[,]
result)
{
int matACols = matA.GetLength(1);
int matBCols = matB.GetLength(1);
int matARows = matA.GetLength(0);
// A basic matrix multiplication.
// Parallelize the outer loop to partition the source array by
rows.
Parallel.For(0, matARows, i =>
{
for (int j = 0; j < matBCols; j++)
{
// Use a temporary to improve parallel performance.
double temp = 0;
for (int k = 0; k < matACols; k++)
{
temp += matA[i, k] * matB[k, j];
}
result[i, j] = temp;
}
}); // Parallel.For
}

#endregion

#region Main
static void Main(string[] args)
{
// Set up matrices. Use small values to better view
// result matrix. Increase the counts to see greater
// speedup in the parallel loop vs. the sequential loop.
int colCount = 180;
int rowCount = 2000;
int colCount2 = 270;
double[,] m1 = InitializeMatrix(rowCount, colCount);
double[,] m2 = InitializeMatrix(colCount, colCount2);
double[,] result = new double[rowCount, colCount2];
// First do the sequential version.

MCT: Luis Dueñas Pag 166 de 336


Manual de .NET Framework 4.5

Console.WriteLine("Executing sequential loop...");


Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
MultiplyMatricesSequential(m1, m2, result);
stopwatch.Stop();
Console.WriteLine("Sequential loop time in milliseconds: {0}",
stopwatch.ElapsedMilliseconds);
// For the skeptics.
OfferToPrint(rowCount, colCount2, result);
// Reset timer and results matrix.
stopwatch.Reset();
result = new double[rowCount, colCount2];
// Do the parallel loop.
Console.WriteLine("Executing parallel loop...");
stopwatch.Start();
MultiplyMatricesParallel(m1, m2, result);
stopwatch.Stop();
Console.WriteLine("Parallel loop time in milliseconds: {0}",
stopwatch.ElapsedMilliseconds);
OfferToPrint(rowCount, colCount2, result);
// Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}

#endregion

#region Helper_Methods

static double[,] InitializeMatrix(int rows, int cols)


{
double[,] matrix = new double[rows, cols];
Random r = new Random();
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
matrix[i, j] = r.Next(100);
}
}
return matrix;
}

private static void OfferToPrint(int rowCount, int colCount, double[,]


matrix)
{
Console.WriteLine("Computation complete. Print results? y/n");
char c = Console.ReadKey().KeyChar;
if (c == 'y' || c == 'Y')
{
Console.WindowWidth = 180;
Console.WriteLine();
for (int x = 0; x < rowCount; x++)
{
Console.WriteLine("ROW {0}: ", x);
for (int y = 0; y < colCount; y++)
{
Console.Write("{0:#.##} ", matrix[x, y]);
}
Console.WriteLine();
}
}
}

#endregion
}
}

MCT: Luis Dueñas Pag 167 de 336


Manual de .NET Framework 4.5

Puede utilizar la sobrecarga más básica del método For si no necesita cancelar ni interrumpir las
iteraciones, ni mantener un estado local de subproceso.
Al paralelizar un código, incluidos los bucles, un objetivo importante consiste en hacer tanto uso
de los procesadores como sea posible, sin excederse hasta el punto de que la sobrecarga del
procesamiento en paralelo anule las ventajas en el rendimiento. En este ejemplo determinado,
solamente se paraleliza el bucle exterior, ya que en el bucle interior no se realiza demasiado
trabajo. La combinación de una cantidad pequeña de trabajo y los efectos no deseados en la
memoria caché puede producir la degradación del rendimiento en los bucles paralelos anidados.
Por consiguiente, paralelizar el bucle exterior solo es la mejor manera de maximizar las ventajas
de simultaneidad en la mayoría de los sistemas.
Delegado
El tercer parámetro de esta sobrecarga de For es un delegado de tipo Action<int> en C# o
Action(Of Integer) en Visual Basic. Un delegado Action siempre devuelve void, tanto si no
tiene parámetros como si tiene uno o dieciséis. En Visual Basic, el comportamiento de Action se
define con Sub. En el ejemplo se utiliza una expresión lambda para crear el delegado, pero
también se puede crear de otras formas.
Valor de iteración
El delegado toma un único parámetro de entrada cuyo valor es la iteración actual. El runtime
proporciona este valor de iteración y su valor inicial es el índice del primer elemento del
segmento (partición) del origen que se procesa en el subproceso actual.
Si requiere más control sobre el nivel de simultaneidad, utilice una de las sobrecargas que toma
un parámetro de entrada System.Threading.Tasks.ParallelOptions, como: Parallel.For(Int32,
Int32, ParallelOptions, Action<Int32, ParallelLoopState>).
Valor devuelto y control de excepciones
For devuelve un objeto System.Threading.Tasks.ParallelLoopResult cuando se han completado
todos los subprocesos. Este valor devuelto es útil si se detiene o se interrumpe la iteración del
bucle de forma manual, ya que ParallelLoopResult almacena información como la última
iteración que se ejecutó hasta finalizar. Si se producen una o más excepciones en uno de los
subprocesos, se inicia System. AggregateException.
En el código de este ejemplo, no se usa el valor devuelto de For.
Análisis y rendimiento
Puede utilizar el Asistente de rendimiento para ver el uso de la CPU en el equipo. Como
experimento, aumente el número de columnas y filas en las matrices. Cuanto mayores son las
matrices, mayor es la diferencia de rendimiento entre las versiones en paralelo y en serie del
cálculo. Si la matriz es pequeña, la versión en serie se ejecutará más rápidamente debido a la
sobrecarga de la configuración del bucle paralelo.
Las llamadas sincrónicas a los recursos compartidos, como la consola o el sistema de archivos,
degradarán de forma significativa el rendimiento de un bucle paralelo. Al medir el rendimiento,
intente evitar llamadas como Console.WriteLine dentro del bucle.
3.1.1.2. Cómo: Escribir un bucle Parallel.ForEach simple
En este ejemplo, se muestra cómo se usa un bucle Parallel.ForEach para habilitar el paralelismo
de datos en cualquier origen de datos System.Collections.IEnumerable o
System.Collections.Generic. IEnumerable<T>.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la PLINQ.

MCT: Luis Dueñas Pag 168 de 336


Manual de .NET Framework 4.5

Ejemplo
namespace ForEachDemo
{
using System;
using System.Drawing; // requires system.Drawing.dll
using System.IO;
using System.Threading;
using System.Threading.Tasks;

class SimpleForEach
{
static void Main()
{
// A simple source for demonstration purposes. Modify this path as
necessary.
string[] files = System.IO.Directory.GetFiles
(@"C:\Users\Public\Pictures\Sample Pictures", "*.jpg");
string newDir = @"C:\Users\Public\Pictures\Sample
Pictures\Modified";
System.IO.Directory.CreateDirectory(newDir);
// Method signature: Parallel.ForEach(IEnumerable<TSource> source,
// Action<TSource> body)
Parallel.ForEach(files, currentFile =>
{
// The more computational work you do here, the greater
// the speedup compared to a sequential foreach loop.
string filename = System.IO.Path.GetFileName(currentFile);
System.Drawing.Bitmap bitmap = new
System.Drawing.Bitmap(currentFile);

bitmap.RotateFlip(System.Drawing.RotateFlipType.Rotate180FlipNone);
bitmap.Save(System.IO.Path.Combine(newDir, filename));
// Peek behind the scenes to see how work is parallelized.
// But be aware: Thread contention for the Console slows down
parallel
// loops!!!
Console.WriteLine("Processing {0} on thread {1}", filename,
Thread.CurrentThread.ManagedThreadId);

} //close lambda expression


); //close method invocation
// Keep the console window open in debug mode.
Console.WriteLine("Processing complete. Press any key to exit.");
Console.ReadKey();
}
}
}

Un bucle ForEach funciona como un bucle For. Se crea una partición de la colección de origen
y el trabajo se programa en varios subprocesos en función del entorno del sistema. Cuantos más
procesadores tenga el sistema, más rápido se ejecutará el método paralelo. En algunas
colecciones de origen, puede resultar más rápido un bucle secuencial, en función del tamaño del
origen y del tipo de trabajo que se realice.
Para usar ForEach con una colección no genérica, puede emplear el método de extensión
Cast<TResult> para convertir la colección en una colección genérica, como se muestra en el
ejemplo siguiente:
Parallel.ForEach(nonGenericCollection.Cast<object>(),currentElement =>{});

Puede usar también Parallel LINQ (PLINQ) con el fin de paralelizar el procesamiento de los
orígenes de datos IEnumerable<T>. PLINQ permite usar una sintaxis de consulta declarativa
para expresar el comportamiento del bucle.
3.1.1.3. Cómo: Detener o interrumpir un bucle Parallel.For

MCT: Luis Dueñas Pag 169 de 336


Manual de .NET Framework 4.5

En el siguiente ejemplo se muestra cómo interrumpir un bucle For (o Salir de él en Visual


Basic) y también cómo detener un bucle. En este contexto, "interrumpir" significa completar
todas las iteraciones en todos los subprocesos que son anteriores a la iteración actual en el
subproceso actual y, a continuación, salir del bucle. " Detener" significa detener todas las
iteraciones en cuanto sea conveniente.
Ejemplo
En este ejemplo se muestra un bucle For; sin embargo, se puede detener o interrumpir desde un
bucle ForEach de la misma manera. En un bucle ForEach, se genera un índice de iteración
internamente para cada uno de los elementos de cada partición.
namespace StopOrBreak
{
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Test
{
static void Main()
{
StopLoop();
BreakAtThreshold();
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}

private static void StopLoop()


{
Console.WriteLine("Stop loop...");
double[] source = MakeDemoSource(1000, 1);
ConcurrentStack<double> results = new ConcurrentStack<double>();
// i is the iteration variable. loopState is a
// compiler-generated ParallelLoopState
Parallel.For(0, source.Length, (i, loopState) =>
{
// Take the first 100 values that are retrieved
// from anywhere in the source.
if (i < 100)
{
// Accessing shared object on each iteration
// is not efficient. See remarks.
double d = Compute(source[i]);
results.Push(d);
}
else
{
loopState.Stop();
return;
}
} // Close lambda expression.
); // Close Parallel.For
Console.WriteLine("Results contains {0} elements",
results.Count());
}

static void BreakAtThreshold()


{
double[] source = MakeDemoSource(10000, 1.0002);
ConcurrentStack<double> results = new ConcurrentStack<double>();
// Store all values below a specified threshold.
Parallel.For(0, source.Length, (i, loopState) =>
{
double d = Compute(source[i]);

MCT: Luis Dueñas Pag 170 de 336


Manual de .NET Framework 4.5

results.Push(d);
if (d > .2)
{
// Might be called more than once!
loopState.Break();
Console.WriteLine("Break called at iteration {0}. d = {1}
", i, d);
Thread.Sleep(1000);
}
});
Console.WriteLine("results contains {0} elements",
results.Count());
}

static double Compute(double d)


{
//Make the processor work just a little bit.
return Math.Sqrt(d);
}

// Create a contrived array of monotonically increasing


// values for demonstration purposes.
static double[] MakeDemoSource(int size, double valToFind)
{
double[] result = new double[size];
double initialval = .01;
for (int i = 0; i < size; i++)
{
initialval *= valToFind;
result[i] = initialval;
}
return result;
}
}

En un bucle Parallel.For u Parallel.ForEach, no se puede usar la misma instrucción break o Exit


que se utiliza en un bucle secuencial porque estas construcciones de lenguaje son válidas para
los bucles, y un "bucle" paralelo es realmente un método, no un bucle. En su lugar, utilice Stop
o el método de Break. Algunas de las sobrecargas de Parallel.For aceptan Action<int,
ParallelLoopState> (Action(Of Integer, ParallelLoopState) en Visual Basic) como parámetro de
entrada. El runtime crea en segundo plano el objeto ParallelLoopState, al que puede dar
cualquier nombre que desee en la expresión lambda.
En el ejemplo, el método de StopLoop() requiere sólo 100 valores de la secuencia de origen y,
no importa qué elementos se recuperan. En este caso, el método de Stop se usa, porque indica
todas las iteraciones del bucle, incluidas las que comenzaron antes de la iteración actual en otros
subprocesos, detener en cuanto sea conveniente.
En el método de BreakAtThreshold(), recuperamos todos los elementos hasta un índice
especificado en la secuencia de origen. En este caso, se denomina Break, porque cuando se llega
al índice en un subproceso, es posible que los elementos anteriores en el origen todavía no se
hayan procesado. Break hará que otros subprocesos abandonen el trabajo en segmentos
posteriores (si están dedicados a ninguno) y completa el procesamiento de todos los elementos
anteriores antes de salir del bucle.
Observe que no puede controlar si otros subprocesos en un bucle continúan ejecutándose
después de que se llame a Stop o Break. Puede usar la propiedad ParallelLoopState.IsStopped
para comprobar si el bucle se ha detenido en otro subproceso. En el ejemplo siguiente, si
IsStopped es true, no se escriben más datos en la colección.
3.1.1.4. Cómo: Escribir un bucle Parallel.For que tenga variables
locales de subproceso

MCT: Luis Dueñas Pag 171 de 336


Manual de .NET Framework 4.5

En este ejemplo se muestra cómo utilizar variables locales de subproceso para almacenar y
recuperar el estado de cada tarea independiente que se crea en un bucle For. Si se usan datos
locales de subproceso, se puede evitar la sobrecarga de sincronizar un número grande de accesos
al estado compartido. En lugar de escribir en un recurso compartido en cada iteración, calcula y
almacena el valor hasta que se completan todas las iteraciones de la tarea. A continuación,
puede escribir el resultado final una vez en el recurso compartido o pasarlo a otro método.
Ejemplo
namespace ThreadLocalFor
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Test
{
static void Main()
{
int[] nums = Enumerable.Range(0, 1000000).ToArray();
long total = 0;
// Use type parameter to make subtotal a long, not an int
Parallel.For<long>(0, nums.Length, () => 0, (j, loop, subtotal) =>
{
subtotal += nums[j];
return subtotal;
},
(x) => Interlocked.Add(ref total, x)
);
Console.WriteLine("The total is {0}", total);
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}
}

Los dos primeros parámetros de cada método For especifican los valores de iteración inicial y
final. En esta sobrecarga del método, el tercer parámetro es donde inicializa el estado local. "
Estado local" en este contexto significa una variable cuya duración se extiende desde
inmediatamente antes de la primera iteración del bucle en el subproceso actual hasta
inmediatamente después de la última iteración.
El tipo del tercer parámetro es Func<TResult>, donde TResult es el tipo de la variable que
almacenará el estado local del subproceso. Tenga en cuenta que, en este ejemplo, se usa una
versión genérica del método y el parámetro de tipo es long (Long en Visual Basic). El parámetro
de tipo indica al compilador el tipo de la variable temporal que se usará para almacenar el estado
local del subproceso. La expresión () => 0 (Function() 0 en Visual Basic) de este ejemplo
significa que la variable local de subproceso se inicializa en cero. Si el parámetro de tipo es un
tipo de referencia o un tipo de valor definido por el usuario, este ejemplo de Func se parecería al
siguiente:
() => new MyClass()

El cuarto parámetro de tipo es donde define la lógica del bucle. IntelliSense muestra que tiene
un tipo de Func<int, ParallelLoopState, long, long> o Func(Of Integer, ParallelLoopState, Long,
Long). La expresión lambda espera tres parámetros de entrada en este mismo orden que
corresponde a estos tipos. El último parámetro de tipo es el tipo devuelto. En este caso, el tipo es
long porque es lo que se especificó en el parámetro de tipo For. Llamamos a esa variable
subtotal en la expresión lambda y la devolvemos. El valor devuelto se utiliza para inicializar el
subtotal en cada iteración subsiguiente. También puede considerar este último parámetro

MCT: Luis Dueñas Pag 172 de 336


Manual de .NET Framework 4.5

simplemente como un valor que se pasa a cada iteración y después al delegado localFinally
cuando se completa la última iteración.
El quinto parámetro es donde se define el método al que se llamará una vez, cuando todas las
iteraciones de este subproceso se hayan completado. El tipo del parámetro de entrada
corresponde de nuevo al parámetro de tipo del método For y al tipo que devuelve la expresión
lambda del cuerpo. En este ejemplo, el valor se agrega a una variable en el ámbito de clase de
una manera segura para subprocesos. Al usar una variable local de subproceso, hemos evitado
escribir en esta variable de clase en cada iteración de cada subproceso.
3.1.1.5. Cómo: Escribir un bucle Parallel.ForEach que tenga variables
locales de subproceso
En el siguiente ejemplo se muestra cómo escribir un método ForEach que utiliza variables
locales de subproceso. Cuando un bucle ForEach se ejecuta, divide su colección de origen en
varias particiones. Cada partición obtendrá su propia copia de la variable "local de subproceso".
(El término "local de subproceso" es ligeramente inexacto, porque en algunos casos dos
particiones se pueden ejecutar en el mismo subproceso).
El código y los parámetros de este ejemplo se parecen mucho al método For correspondiente.
Ejemplo
Para utilizar una variable local de subproceso en un bucle ForEach, debe utilizar la versión del
método que toma dos parámetros type. El primer parámetro especifica el tipo del elemento de
origen y el segundo parámetro especifica el tipo de la variable local de subproceso.
El primer parámetro de entrada es el origen de datos y el segundo es la función que inicializará
la variable local de subproceso. El tercer parámetro de entrada es un Func<T1, T2, T3, TResult>
que invoca el bucle paralelo en cada iteración. Se proporciona el código para el delegado y el
bucle pasa los parámetros de entrada. Los parámetros de entrada son el elemento vigente, una
variable ParallelLoopState que permite examinar el estado del bucle, y la variable local de
subproceso. Devuelve la variable local de subproceso y, a continuación, el método pasa a la
iteración siguiente de esta partición. Esta variable es distinta en todas las particiones del bucle.
El último parámetro de entrada del método ForEach es el delegado Action<T> que el método
invocará cuando todos los bucles se hayan completado. El método proporciona el valor final de
la variable local de subproceso para este subproceso (o partición del bucle) y proporciona el
código que captura el valor final y realiza cualquier acción necesaria para combinar el resultado
de esta partición con los resultados de las otras particiones. Como el tipo de delegado es
Action<T>, no hay valor devuelto.
namespace ThreadLocalForEach
{
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Test
{
static void Main()
{
int[] nums = Enumerable.Range(0, 1000000).ToArray();
long total = 0;
// First type parameter is the type of the source elements
// Second type parameter is the type of the local data (subtotal)
Parallel.ForEach<int, long>(nums, // source collection
() => 0, // method to initialize the local variable
(j, loop, subtotal) => // method invoked by the loop on each
iteration
{

MCT: Luis Dueñas Pag 173 de 336


Manual de .NET Framework 4.5

subtotal += j; //modify local


variable
return subtotal; // value to be passed to next
iteration
},
// Method to be executed when all loops have completed.
// finalResult is the final value of subtotal. supplied by the ForEach
method.
(finalResult) => Interlocked.Add(ref total,
finalResult));
Console.WriteLine("The total from Parallel.ForEach is {0}",
total);
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}
}

3.1.1.6. Cómo: Cancelar un bucle Parallel.For o ForEach


Los métodos Parallel.ForEach y Parallel.For admiten la cancelación a través del uso de tokens
de cancelación. Para obtener más información sobre la cancelación en general, vea Cancelación.
En un bucle paralelo, se proporciona CancellationToken al método en el parámetro
ParallelOptions y después se agrega la llamada paralela en un bloque try-catch.
Ejemplo
En el ejemplo siguiente se muestra cómo cancelar una llamada a Parallel.ForEach. Puede aplicar
el mismo enfoque a una llamada Parallel.For.
namespace CancelParallelLoops
{
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static void Main()
{
int[] nums = Enumerable.Range(0, 10000000).ToArray();
CancellationTokenSource cts = new CancellationTokenSource();
// Use ParallelOptions instance to store the CancellationToken
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;
Console.WriteLine("Press any key to start. Press 'c' to cancel.");
Console.ReadKey();
// Run a task so that we can cancel from another thread.
Task.Factory.StartNew(() =>
{
if (Console.ReadKey().KeyChar == 'c')
cts.Cancel();
Console.WriteLine("press any key to exit");
});
try
{
Parallel.ForEach(nums, po, (num) =>
{
double d = Math.Sqrt(num);
Console.WriteLine("{0} on {1}", d,
Thread.CurrentThread.ManagedThreadId);
po.CancellationToken.ThrowIfCancellationRequested();
});
}
catch (OperationCanceledException e)
{

MCT: Luis Dueñas Pag 174 de 336


Manual de .NET Framework 4.5

Console.WriteLine(e.Message);
}
Console.ReadKey();
}
}
}

Si el token que señala la cancelación es el mismo que se especifica en la instancia de


ParallelOptions, el bucle paralelo producirá una OperationCanceledException única en la
cancelación. Si algún otro token produce la cancelación, el bucle producirá una
AggregateException con OperationCanceledException como InnerException.
3.1.1.7. Cómo: Controlar excepciones en bucles paralelos
Las sobrecargas de For y de ForEach no tienen ningún mecanismo especial para controlar las
excepciones que puedan iniciarse. A este respecto, se asemejan a bucles for y foreach normales
(For y For Each en Visual Basic).
Cuando agregue su propia lógica de control de excepciones a los bucles paralelos, tenga en
cuenta la posibilidad de que se inicien excepciones similares en varios subprocesos al mismo
tiempo, así como el caso de que una excepción iniciada en un subproceso puede hacer que se
inicie otra excepción en otro subproceso. Puede administrar ambos casos si encapsula todas las
excepciones del bucle en System.AggregateException. En el ejemplo siguiente se muestra un
posible enfoque.
Nota
Cuando está habilitada la opción "Solo mi código", en algunos casos, Visual Studio se
interrumpe en la línea que produce la excepción y muestra el mensaje de error "Excepción no
controlada por el código de usuario". Este error es benigno. Puede presionar F5 para continuar y
ver el comportamiento de control de excepciones que se muestra en el ejemplo siguiente. Para
evitar que Visual Studio se interrumpa con el primer error, desactive la casilla "Solo mi código"
bajo Herramientas, Opciones, Depuración, General.
Ejemplo
En este ejemplo, todas las excepciones se detectan y, a continuación, se encapsulan en la
excepción System.AggregateException que se produce. El llamador puede decidir qué
excepciones se deben administrar.
class ExceptionDemo2
{
static void Main(string[] args)
{
// Create some random data to process in parallel.
// There is a good probability this data will cause some exceptions to
be
// thrown.
byte[] data = new byte[5000];
Random r = new Random();
r.NextBytes(data);
try
{
ProcessDataInParallel(data);
}
catch (AggregateException ae)
{
// This is where you can choose which exceptions to handle.
foreach (var ex in ae.InnerExceptions)
{
if (ex is ArgumentException)
Console.WriteLine(ex.Message);
else
throw ex;
}

MCT: Luis Dueñas Pag 175 de 336


Manual de .NET Framework 4.5

}
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}

private static void ProcessDataInParallel(byte[] data)


{
// Use ConcurrentQueue to enable safe enqueueing from multiple
threads.
var exceptions = new ConcurrentQueue<Exception>();
// Execute the complete loop and capture all exceptions.
Parallel.ForEach(data, d =>
{
try
{
// Cause a few exceptions, but not too many.
if (d < 0x3)
throw new ArgumentException(String.Format("value is {0:x}.
Elements must be greater than 0x3.", d));
else
Console.Write(d + " ");
}
// Store the exception and continue with the loop.
catch (Exception e) { exceptions.Enqueue(e); }
});
// Throw the exceptions here after the loop completes.
if (exceptions.Count > 0) throw new AggregateException(exceptions);
}
}

3.1.1.8. Cómo: Acelerar cuerpos de bucle pequeños


Cuando un bucle For tiene un cuerpo pequeño, puede registrar un rendimiento más lento que el
del bucle secuencial equivalente. Este rendimiento más lento es consecuencia de la sobrecarga
en la participación de los datos y el costo de invocar un delegado en cada iteración del bucle.
Para hacer frente a estos escenarios, la clase Partitioner proporciona el método
Partitioner.Create, que permite proporcionar un bucle secuencial para el cuerpo de delegado de
modo que el delegado solo se invoque una vez por partición, en lugar de una vez por iteración.
Ejemplo
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static void Main()
{
// Source must be array or IList.
var source = Enumerable.Range(0, 100000).ToArray();
// Partition the entire source array.
var rangePartitioner = Partitioner.Create(0, source.Length);
double[] results = new double[source.Length];
// Loop over the partitions in parallel.
Parallel.ForEach(rangePartitioner, (range, loopState) =>
{
// Loop over each range element without a delegate invocation.
for (int i = range.Item1; i < range.Item2; i++)
{
results[i] = source[i] * Math.PI;
}
});
Console.WriteLine("Operation complete. Print results? y/n");
char input = Console.ReadKey().KeyChar;

MCT: Luis Dueñas Pag 176 de 336


Manual de .NET Framework 4.5

if (input == 'y' || input == 'Y')


{
foreach(double d in results)
{
Console.Write("{0} ", d);
}
}
}
}

El enfoque mostrado en este ejemplo es útil cuando el bucle realiza una cantidad de trabajo
mínima. Cuando el trabajo se vuelve más costoso en los cálculos, obtendrá probablemente un
rendimiento igual o mejor si usa un bucle For o ForEach con el particionador predeterminado.
3.1.1.9. Cómo: Recorrer en iteración directorios con la clase paralela
En muchos casos, la iteración de archivo es una operación que se puede paralelizar fácilmente.
El tema Cómo: Recorrer en iteración directorios con PLINQ muestra la manera más fácil de
realizar esta tarea en muchos escenarios. Sin embargo, pueden surgir complicaciones cuando el
código tiene que tratar con los muchos tipos de excepciones que pueden surgir al obtener acceso
al sistema de archivos. En el ejemplo siguiente se muestra un enfoque para el problema. Usa una
iteración basada en la pila para recorrer todos los archivos y carpetas en un directorio
especificado y habilita el código para detectar y controlar diversas excepciones. Por supuesto, la
forma de controlar las excepciones depende de usted.
Ejemplo
En el ejemplo siguiente la iteración en los directorios se realiza de forma secuencial, pero el
procesamiento de los archivos se realiza en paralelo. Este enfoque es probablemente el mejor
cuando hay una tasa alta de directorios y archivos. También es posible ejecutar la iteración de
directorio y obtener acceso a cada archivo secuencialmente. Probablemente no es eficaz
paralelizar ambos bucles a menos que esté dirigido específicamente a un equipo con un gran
número de procesadores. Sin embargo, como en todos los casos, se debe probar
exhaustivamente la aplicación para determinar el mejor enfoque.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Parallel_File
{
class Program
{
static void Main(string[] args)
{
TraverseTreeParallelForEach(@"C:\Program Files", (f) =>
{
// For this demo we don't do anything with the data
// except to read it.
byte[] data = File.ReadAllBytes(f);
// For user interest, although it slows down the operation.
Console.WriteLine(f);
});
// Keep the console window open.
Console.ReadKey();
}

public static void TraverseTreeParallelForEach(string root,


Action<string> action)
{

MCT: Luis Dueñas Pag 177 de 336


Manual de .NET Framework 4.5

//Count of files traversed and timer for diagnostic output


int fileCount = 0;
var sw = Stopwatch.StartNew();
// Use this value to determine whether to parallelize
// file processing on each folder.
int procCount = System.Environment.ProcessorCount;
// Data structure to hold names of subfolders to be examined for
files.
Stack<string> dirs = new Stack<string>();
if (!System.IO.Directory.Exists(root))
{
throw new ArgumentException();
}
dirs.Push(root);
while (dirs.Count > 0)
{
string currentDir = dirs.Pop();
string[] subDirs = null;
string[] files = null;
try
{
subDirs = System.IO.Directory.GetDirectories(currentDir);
}
// An UnauthorizedAccessException exception will be thrown if we do
not have
// discovery permission on a folder or file. It may or may not be
acceptable
// to ignore the exception and continue enumerating the remaining
files and
// folders. It is also possible (but unlikely) that a
DirectoryNotFound
// exception will be raised. This will happen if currentDir has been
deleted
// by another application or thread after our call to
Directory.Exists.
// The choice of which exceptions to catch depends entirely on the
specific
// task you are intending to perform and also on how much you know
with
// certainty about the systems on which this code will run.
catch (UnauthorizedAccessException e)
{
Console.WriteLine(e.Message);
continue;
}
catch (System.IO.DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
continue;
}
try
{
files = System.IO.Directory.GetFiles(currentDir);
}
catch (UnauthorizedAccessException e)
{
Console.WriteLine(e.Message);
continue;
}
catch (System.IO.DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
continue;
}
// Perform the required action on each file here in parallel
// if there are a sufficient number of files in the directory
// or else sequentially if not. Files are opened and processed

MCT: Luis Dueñas Pag 178 de 336


Manual de .NET Framework 4.5

// synchronously but this could be modified to perform async


I/O.
try
{
if (files.Length < procCount)
{
foreach (var file in files)
{
action(file);
fileCount++;
}
}
else
{
Parallel.ForEach(files, () => 0, (file, loopState,
localCount) =>
{
action(file);
return (int) ++localCount;

},
(c) =>
{
Interlocked.Exchange(ref fileCount, fileCount +
c);
});
}
}
catch (AggregateException ae)
{
ae.Handle((ex) =>
{
if (ex is UnauthorizedAccessException)
{
// Here we just output a message and go on.
Console.WriteLine(ex.Message);
return true;
}
// Handle other exceptions here if necessary...
return false;
});
}
// Push the subdirectories onto the stack for traversal.
// This could also be done before handing the files.
foreach (string str in subDirs) dirs.Push(str);
}
// For diagnostic purposes.
Console.WriteLine("Processed {0} files in {1} milleseconds",
fileCount,
sw.ElapsedMilliseconds);
}
}
}

En este ejemplo, la E/S de archivo se realiza de forma sincrónica. Al trabajar con archivos
grandes o conexiones de red lentas, puede ser preferible obtener acceso a los archivos de forma
asincrónica. Puede combinar las técnicas de E/S asincrónica con la iteración paralela.
Tenga en cuenta que si se produce una excepción en el subproceso principal, los subprocesos
que inicia el método ForEach podrían seguir ejecutándose. Para detener estos subprocesos,
puede establecer una variable booleana en los controladores de excepciones y comprobar su
valor en cada iteración del bucle paralelo. Si el valor indica que se ha iniciado una excepción,
use la variable ParallelLoopState para detener o interrumpir el bucle.
3.1.2. Paralelismo de tareas (Task Parallel Library)

MCT: Luis Dueñas Pag 179 de 336


Manual de .NET Framework 4.5

Como indica su nombre, la biblioteca TPL (Task Parallel Library, biblioteca de procesamiento
paralelo basado en tareas) se basa en el concepto de tarea ("task" en inglés). El término
paralelismo de tareas hace referencia a la ejecución simultánea de una o varias tareas
independientes. Una tarea representa una operación asincrónica y, en ciertos aspectos, se
asemeja a la creación de un nuevo subproceso o elemento de trabajo ThreadPool, pero con un
nivel de abstracción mayor. Las tareas proporcionan dos ventajas fundamentales:
 Un uso más eficaz y más escalable de los recursos del sistema.
En segundo plano, las tareas se ponen en la cola del elemento ThreadPool, que se ha
mejorado con algoritmos (como el algoritmo de ascenso de colina o "hill-climbing")
que determinan y ajustan el número de subprocesos con el que se maximiza el
rendimiento. Esto hace que las tareas resulten relativamente ligeras y que, por tanto,
pueda crearse un gran número de ellas para habilitar un paralelismo pormenorizado.
Como complemento y para proporcionar el equilibrio de carga, se usan los conocidos
algoritmos de robo de trabajo.
 Un mayor control mediante programación del que se puede conseguir con un
subproceso o un elemento de trabajo.
Las tareas y el marco que se crea en torno a ellas proporcionan un amplio conjunto de
API que admiten el uso de esperas, cancelaciones, continuaciones, control robusto de
excepciones, estado detallado, programación personalizada, y más.
Por estos dos motivos, en .NET Framework, las tareas son las API preferidas para código
multiproceso, asincrónico, y paralelo de escritura.
Crear y ejecutar tareas implícitamente
El método Parallel.Invoke proporciona una manera conveniente de ejecutar cualquier número de
instrucciones arbitrarias simultáneamente. Pase un delegado Action por cada elemento de
trabajo. La manera más fácil de crear estos delegados es con expresiones lambda. La expresión
lambda puede llamar a un método con nombre o proporcionar el código alineado. En el
siguiente ejemplo se muestra una llamada a Invoke básica que crea e inicia dos tareas que se
ejecutan a la vez.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la TPL. Si no
está familiarizado con las expresiones lambda en C# o Visual Basic, vea Expresiones lambda en
PLINQ y TPL.
Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());
Nota
El número de instancias de Task que Invoke crea en segundo plano no es necesariamente igual
al número de delegados que se proporcionan. La TPL puede emplear varias optimizaciones,
sobre todo con grandes números de delegados.
Para tener un mayor control de la ejecución de tareas o para devolver un valor de la tarea, debe
trabajar con objetos Task más explícitamente.
Crear y ejecutar tareas explícitamente
Una tarea se representa mediante la clase System.Threading.Tasks.Task. Una tarea que devuelve
un valor se representa mediante la clase System.Threading.Tasks.Task<TResult>, que se hereda
de Task. El objeto de tarea administra los detalles de la infraestructura y proporciona métodos y
propiedades a los que se puede obtener acceso desde el subproceso que realiza la llamada a lo
largo de la duración de la tarea. Por ejemplo, se puede tener acceso a la propiedad Status de una
tarea en cualquier momento para determinar si ha empezado a ejecutarse, si se ha ejecutado
hasta su finalización, si se ha cancelado o si se ha producido una excepción. El estado se
representa mediante la enumeración TaskStatus.

MCT: Luis Dueñas Pag 180 de 336


Manual de .NET Framework 4.5

Cuando se crea una tarea, se proporciona un delegado de usuario que encapsula el código que la
tarea va a ejecutar. El delegado se puede expresar como un delegado con nombre, un método
anónimo o una expresión lambda. Las expresiones lambda pueden contener una llamada a un
método con nombre, tal y como se muestra en el siguiente ejemplo.
// Create a task and supply a user delegate by using a lambda
expression.
var taskA = new Task(() => Console.WriteLine("Hello from
taskA."));
// Start the task.
taskA.Start();
// Output a message from the joining thread.
Console.WriteLine("Hello from the calling thread.");
// Message from taskA should follow.
/* Output:
* Hello from the calling thread.
* Hello from taskA.
*/

También puede utilizar los métodos de Run para crear e iniciar una tarea en una operación. Para
administrar la tarea, los métodos de Run utilizan el programador de tareas predeterminado,
independientemente del que se asocia el programador de tareas al subproceso actual. Los
métodos de Run son la manera preferida de crear e iniciar tareas cuando más control sobre la
creación y la programación de la tarea no es necesario.
También se puede usar el método StartNew para crear e iniciar una tarea en una sola operación.
Utilice este método cuando la creación y la programación no tienen que ser independientes y
necesita más opciones de creación de la tarea o el uso de un programador concreto, o si necesita
pasar el estado adicional en la tarea a través de la propiedad de AsyncState, como se muestra en
el ejemplo siguiente.
// Create and start the task in one operation.
var taskA = Task.Factory.StartNew(() => Console.WriteLine("Hello from
taskA."));
// Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.");

Task y Task<TResult> cada exponen una propiedad estática de Factory que devuelve una
instancia predeterminada de TaskFactory, para que pueda llamar al método como
Task.Factory.StartNew(). Asimismo, en este ejemplo, dado que las tareas son de tipo
System.Threading.Tasks.Task<TResult>, cada una tiene una propiedad Result pública que
contiene el resultado del cálculo. Las tareas se ejecutan de forma asincrónica y pueden
completarse en cualquier orden. Si Result se obtiene antes de que el cálculo finaliza, la
propiedad se bloqueará el subproceso hasta que el valor esté disponible.
Task<double>[] taskArray = new Task<double>[]
{
Task<double>.Factory.StartNew(() => DoComputation1()),
// May be written more conveniently like this:
Task.Factory.StartNew(() => DoComputation2()),
Task.Factory.StartNew(() => DoComputation3())
};
double[] results = new double[taskArray.Length];
for (int i = 0; i < taskArray.Length; i++)
results[i] = taskArray[i].Result;

Cuando se usa una expresión lambda para crear un delegado, tiene acceso a todas las variables
que están visibles en ese momento en el código fuente. Sin embargo, en algunos casos,
especialmente en los bucles, una expresión lambda no captura la variable como se espera.
Captura solo el valor final, no el valor tal y como se transforma después de cada iteración.
Puede obtener acceso al valor en cada iteración si proporciona un objeto de estado a una tarea a
través de su constructor, como se muestra en el ejemplo siguiente:
class MyCustomData
{

MCT: Luis Dueñas Pag 181 de 336


Manual de .NET Framework 4.5

public long CreationTime;


public int Name;
public int ThreadNum;
}

static void TaskDemo2()


{
// Create the task object by using an Action(Of Object) to pass in
custom data
// in the Task constructor. This is useful when you need to capture
outer
// variables from within a loop. As an experiement, try modifying this
code to
// capture i directly in the lambda, and compare results.
Task[] taskArray = new Task[10];

for(int i = 0; i < taskArray.Length; i++)


{
taskArray[i] = new Task((obj) =>
{
MyCustomData mydata = (MyCustomData) obj;
mydata.ThreadNum = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine("Hello from Task #{0} created at {1}
running on
thread #{2}.",mydata.Name, mydata.CreationTime,
mydata.ThreadNum);
},
new MyCustomData () {Name = i, CreationTime = DateTime.Now.Ticks}
);
taskArray[i].Start();
}
}

Este estado se pasa como argumento al delegado de la tarea, y se puede tener acceso al objeto de
tarea mediante la propiedad de AsyncState. Además, el paso de los datos a través del constructor
podría proporcionar una pequeña ventaja de rendimiento en algunos escenarios.
Identificador de tarea
Cada tarea recibe un identificador entero que la identifica de forma única en un dominio de
aplicación y se puede tener acceso mediante la propiedad de Id. El identificador resulta útil para
ver información sobre la tarea en las ventanas Pilas paralelas y Tareas paralelas del depurador
de Visual Studio. El identificador se crea de forma diferida, lo que significa que no se crea hasta
que se solicite; por consiguiente, una tarea puede tener un identificador diferente cada vez que el
programa se ejecute.
Opciones de creación de tareas
La mayoría de las API que crean tareas proporcionan sobrecargas que aceptan un parámetro
TaskCreationOptions. Especificar una de estas opciones, indica al programador de tareas cómo
programar la tarea en el grupo de subprocesos. En la tabla siguiente se muestran las diversas
opciones de creación de tareas.
Elemento Descripción
Es la opción predeterminada si no se especifica ninguna opción. El
None
programador usa su heurística predeterminada para programar la tarea.
Especifica que la tarea debe programarse de modo que las tareas creadas
PreferFairness anteriormente tengan más posibilidades de ejecutarse antes y que las tareas
posteriormente tengan más posibilidades de ejecutarse después.
LongRunning Especifica que la tarea representa una operación de ejecución prolongada.
Especifica que una tarea debe crearse como elemento secundario asociado de
AttachedToParent
la tarea actual, si existe.

MCT: Luis Dueñas Pag 182 de 336


Manual de .NET Framework 4.5

Especifica que si una tarea interna especifica la opción de AttachedToParent,


DenyChildAttach
esa tarea no se realizará una tarea secundaria asociada.
Especifica que las tareas creadas en esta tarea se realizan la propiedad de
HideScheduler TaskScheduler.Current para ser TaskScheduler.Default en lugar de
programador en el que esta tarea se está ejecutando.
Las opciones pueden combinarse con una operación OR bit a bit. En el ejemplo siguiente se
muestra una tarea que tiene las opciones LongRunning y PreferFairness.
var task3 = new Task(() => MyLongRunningMethod(),
TaskCreationOptions.LongRunning |
TaskCreationOptions.PreferFairness);
task3.Start();

Crear continuaciones de tareas


El método de Task.ContinueWith y el método de Task<TResult>.ContinueWith permiten
especificar que una tarea se inicie cuando la tarea anterior finaliza. Se pasa al delegado de la
tarea de continuación una referencia al antecedente, de modo que pueda examinar su estado.
Además, la tarea de continuación puede recibir de la tarea anterior un valor definido por el
usuario en la propiedad Result para que la salida de la tarea anterior pueda servir de entrada de
la tarea de continuación. En el ejemplo siguiente, getData es iniciado por el código de programa.
A continuación, analyzeData se inicia automáticamente cuando getData termina, y se inicia
reportData cuando analyzeData finaliza. getData genera como resultado una matriz de bytes,
que se pasa a analyzeData. analyzeData procesa esa matriz y devuelve un resultado cuyo tipo se
infiere del tipo devuelto del método Analyze. reportData toma la entrada de analyzeData y
genera un resultado cuyo tipo se infiere de forma similar y que se pasa a estar disponible en el
programa en la propiedad Result.
Task<byte[]> getData = new Task<byte[]>(() => GetFileData());
Task<double[]> analyzeData = getData.ContinueWith(x =>
Analyze(x.Result));
Task<string> reportData = analyzeData.ContinueWith(y =>
Summarize(y.Result));
getData.Start();
//or...
Task<string> reportData2 = Task.Factory.StartNew(() =>
GetFileData())
.ContinueWith((x) =>
Analyze(x.Result))
.ContinueWith((y) =>
Summarize(y.Result));
System.IO.File.WriteAllText(@"C:\reportFolder\report.txt",
reportData.Result);

Los métodos ContinueWhenAll y ContinueWhenAny permiten continuar a partir de varias


tareas.
Crear tareas anidadas desasociadas
Cuando el código de usuario que se está ejecutando en una tarea crea una nueva tarea y no
especifica la opción de AttachedToParent, la nueva tarea no se sincroniza con la tarea externa de
ninguna manera especial. Este tipo de tareas se denominan tareas anidadas desasociadas. En el
siguiente ejemplo se muestra una tarea que crea una tarea anidada desasociada.
var outer = Task.Factory.StartNew(() =>
{
Console.WriteLine("Outer task beginning.");
var child = Task.Factory.StartNew(() =>
{
Thread.SpinWait(5000000);
Console.WriteLine("Detached task completed.");
});
});

MCT: Luis Dueñas Pag 183 de 336


Manual de .NET Framework 4.5

outer.Wait();
Console.WriteLine("Outer task completed.");
/* Output:
Outer task beginning.
Outer task completed.
Detached task completed.
*/

Observe que la tarea externa no espera a que la tarea anidada finalice.


Crear tareas secundarias
Cuando el código de usuario que se está ejecutando en una tarea crea una tarea con la opción
AttachedToParent, la nueva tarea se concibe como una tarea secundaria de la tarea original, que
se denomina tarea primaria. Puede utilizar la opción de AttachedToParent de expresar el
paralelismo de tareas estructurado, ya que la tarea primaria espera implícitamente a que todas
las tareas secundarias finalicen. En el siguiente ejemplo se muestra una tarea que crea una tarea
secundaria:
var parent = Task.Factory.StartNew(() =>
{
Console.WriteLine("Parent task beginning.");
var child = Task.Factory.StartNew(() =>
{
Thread.SpinWait(5000000);
Console.WriteLine("Attached child completed.");
}, TaskCreationOptions.AttachedToParent);

});
parent.Wait();
Console.WriteLine("Parent task completed.");
/* Output:
Parent task beginning.
Attached task completed.
Parent task completed.
*/

Una tarea puede utilizar la opción de DenyChildAttach de evitar que otras tareas asociado a la
tarea primaria.
Para tareas que esperan de finalizar
El tipo de System.Threading.Tasks.Task y el tipo de System.Threading.Tasks.Task<TResult>
proporcionan varias sobrecargas de un método de Task.Wait y de Task<TResult>.Wait que
permiten esperar una tarea. Además, las sobrecargas del método estático de Task.WaitAll y
método de Task.WaitAny permiten esperar el alguna o toda una matriz de tareas finalicen.
Normalmente, una tarea se espera por una de estas razones:
 El subproceso principal depende del resultado final que se calcula mediante una tarea.
 Hay que controlar las excepciones que pueden producirse en la tarea.
En el siguiente ejemplo se muestra el modelo básico donde el control de excepciones no está
implicado.
Task[] tasks = new Task[3]
{
Task.Factory.StartNew(() => MethodA()),
Task.Factory.StartNew(() => MethodB()),
Task.Factory.StartNew(() => MethodC())
};
//Block until all tasks complete.
Task.WaitAll(tasks);
// Continue on this thread...

MCT: Luis Dueñas Pag 184 de 336


Manual de .NET Framework 4.5

Algunas sobrecargas permiten especificar un tiempo de espera, y otras utilizan


CancellationToken adicional como parámetro de entrada, de modo que la espera puede
cancelarse mediante programación o en respuesta a los datos proporcionados por el usuario.
Cuando se espera una tarea, se espera implícitamente a todos los elementos secundarios de esa
tarea que se crearon con la opción de TaskCreationOptionsAttachedToParent. Task.Wait
devuelve un valor inmediatamente si la tarea ya se ha completado. Un método Wait producirá
las tareas generadas por una tarea incluso si se llama a este método Wait una vez completada la
tarea.
Tareas que componen

Las clases de Task y de Task<TResult> proporcionan varios métodos que pueden ayudarle a
crear varias tareas para implementar modelos comunes y mejorar utilizan las características de
lenguaje asincrónicas proporcionadas por C#, Visual Basic, y F#. Esta sección describe
WhenAll, WhenAny, Delay, y los métodos de FromResult<TResult>.

Task.WhenAll
El método de Task.WhenAll asincrónica espera Task múltiple o los objetos de Task<TResult>
al final. Proporciona las versiones sobrecargadas que permiten esperar conjuntos no uniforme de
tareas. Por ejemplo, puede esperar Task multithreading y Task<TResult>, objetos en
completarse en una llamada al método.

Task.WhenAny
El método de Task.WhenAny asincrónica espera uno de Task múltiple o de los objetos de
Task<TResult> al final. Como en el método de Task.WhenAll, este método proporciona
versiones sobrecargadas que permiten esperar conjuntos no uniforme de tareas. El método de
WhenAny es especialmente útil en los escenarios siguientes.
 Operaciones redundantes. Considere un algoritmo o una operación que se pueden
realizar en gran medida. Puede utilizar el método de WhenAny para seleccionar la
operación que finaliza primero y después cancelar operaciones restantes.
 Operaciones intercaladas. Puede iniciar varias operaciones que deben finalizar y utilizar
el método de WhenAny para procesar resultados como cada operación finaliza. Después
de una operación finalice, puede iniciar una o más tareas adicionales.
 Restringir las operaciones. Puede utilizar el método de WhenAny para extender el
escenario anterior limitando el número de operaciones simultáneas.
 Operaciones expirado. Puede utilizar el método de WhenAny para seleccionar entre una
o más tareas y una tarea que termina después de un tiempo concreto, como una tarea
devuelta por el método de Delay. El método de Delay se describe en la sección
siguiente.

Task.Delay
El método de Task.Delay genera un objeto de Task que termina después de que el tiempo
especificado. Puede utilizar este método para crear bucles que sondean en ocasiones para los
datos, especifique tiempos, retrasan administrar de datos proporcionados por el usuario durante
un tiempo predeterminado, etc.

Tarea (T).FromResult
Utilizando el método de Task.FromResult<TResult>, puede crear un objeto de Task<TResult>
que celebre un resultado pre- calculado. Este método es útil al realizar una operación
asincrónica que devuelve un objeto de Task<TResult>, y el resultado de ese objeto de
Task<TResult> se calcula ya.
Control de excepciones en tareas

MCT: Luis Dueñas Pag 185 de 336


Manual de .NET Framework 4.5

Cuando una tarea produce una o varias excepciones, las excepciones se encapsulan en un objeto
AggregateException. Esa excepción se propaga de nuevo al subproceso de unión con la tarea,
que normalmente es el subproceso que está esperando la tarea finalice o tenga acceso a la
propiedad de Result. Este comportamiento sirve para aplicar la directiva de .NET Framework
por la que, de manera predeterminada, todas las excepciones no controladas deben anular el
proceso. El código de llamada puede controlar las excepciones a través de los métodos Wait,
WaitAll o WaitAny o de la propiedad Result de la tarea o grupo de tareas, mientras incluye el
método Wait en un bloque try-catch.
El subproceso de unión también puede controlar excepciones; para ello, obtiene acceso a la
propiedad Exception antes de que la tarea se recolecte como elemento no utilizado. Al obtener
acceso a esta propiedad, impide que la excepción no controlada desencadene el comportamiento
de propagación de la excepción que anula el proceso cuando el objeto ha finalizado.
Cancelar tareas
La clase de Task admite la cancelación cooperativa y está totalmente integrada con la clase de
System.Threading.CancellationTokenSource y la clase de
System.Threading.CancellationToken, que son nuevas en .NET Framework 4. Muchos de los
constructores de la clase System.Threading.Tasks.Task toman un objeto CancellationToken
como parámetro de entrada. Muchas de las sobrecargas de StartNew y de Run toman también
CancellationToken.
Puede crear el token y emitir la solicitud de cancelación posteriormente usando la clase
CancellationTokenSource. A continuación, debe pasar el token a Task como argumento y hacer
referencia al mismo token también en el delegado de usuario, que se encarga de responder a una
solicitud de cancelación.
La clase TaskFactory
La clase TaskFactory proporciona métodos estáticos que encapsulan algunos modelos comunes
de creación e inicio de tareas y tareas de continuación.
 El modelo más común es StartNew, que crea e inicia una tarea en una sola instrucción.
 Cuando cree tareas de continuación a partir de, utilice el método del método o de
ContinueWhenAny de ContinueWhenAll o sus equivalentes en la clase de
Task<TResult>.
 Para encapsular los métodos BeginX y EndX del modelo de programación asincrónica
en una instancia de Task o Task<TResult>, use los métodos FromAsync.
TaskFactory predeterminado se puede tener acceso como propiedad estática de la clase de Task
o la clase de Task<TResult>. También pueden crearse directamente instancias de TaskFactory y
especificar varias opciones entre las que se incluyan las opciones CancellationToken,
TaskCreationOptions, TaskContinuationOptions o TaskScheduler. Las opciones se especifican
al crear el generador de tareas se aplicará a todas las tareas que cree, a menos que Task se crea
mediante la enumeración de TaskCreationOptions en ese caso, las opciones de la tarea
reemplazan los del generador de tareas.
Tareas sin delegados
En algunos casos, es posible que desee usar un objeto Task para encapsular alguna operación
asincrónica ejecutada por un componente externo en lugar de su propio usuario delegado. Si la
operación se basa en el patrón Begin/End del modelo de programación asincrónica, puede usar
los métodos FromAsync. Si no es este el caso, puede usar el objeto
TaskCompletionSource<TResult> para encapsular la operación en una tarea y, de este modo,
aprovechar algunas de las ventajas de programación de Task, como por ejemplo, su
compatibilidad con la propagación de excepciones y el uso de continuaciones.
Programadores personalizados

MCT: Luis Dueñas Pag 186 de 336


Manual de .NET Framework 4.5

La mayoría de los desarrolladores de aplicaciones o bibliotecas no prestan atención al


procesador en el que se ejecuta la tarea, al modo en que la tarea sincroniza su trabajo con otras
tareas o al modo en que se programa la tarea en el objeto System.Threading.ThreadPool. Solo
necesitan que la ejecución en el equipo host sea lo más eficaz posible. Si necesita tener un
control más minucioso sobre los detalles de programación, la biblioteca TPL (Task Parallel
Library, biblioteca de procesamiento paralelo basado en tareas) permite configurar algunos
valores del programador de tareas predeterminado e incluso permite proporcionar un
programador personalizado.
Estructuras de datos relacionadas
TPL tiene varios tipos públicos nuevos que resultan útiles tanto en escenarios en paralelo como
en escenarios secuenciales. Entre ellos, se incluyen diversas clases de colecciones multiproceso
rápidas y escalables del espacio de nombres System.Collections.Concurrent y varios tipos
nuevos de sincronización, como System.Threading.Semaphore y
System.Threading.ManualResetEventSlim, que resultan más eficaces que sus predecesores en
tipos concretos de cargas de trabajo. Otros tipos nuevos de .NET Framework 4, por ejemplo,
System.Threading.Barrier y System.Threading.SpinLock, proporcionan una funcionalidad que
no estaba disponible en versiones anteriores.
Tipos de la tarea personalizada
Se recomienda no heredar de System.Threading.Tasks.Task ni de System.Threading.Tasks.
Task<TResult>. En su lugar, se recomienda usar la propiedad de AsyncState para asociar datos
adicionales o el estado a un objeto de Task o de Task<TResult>. También puede usar métodos
de extensión para extender la funcionalidad de las clases Task y Task<TResult>.
Si debe heredar de Task o Task<TResult>, no puede utilizar Run, Run, o
System.Threading.Tasks. TaskFactory, System.Threading.Tasks.TaskFactory<TResult>, o las
clases de System.Threading.Tasks. TaskCompletionSource<TResult> para crear instancias de la
tarea personalizada tipo porque estos mecanismos crean solo Task y los objetos de
Task<TResult>. Además, no puede usar los mecanismos de continuación de tarea
proporcionados por Task, Task<TResult>, TaskFactory, y TaskFactory<TResult> para crear
instancias de la tarea personalizada tipo porque estos mecanismos también crean solo Task y los
objetos de Task<TResult>.
3.1.2.1. Tareas de continuación
En la programación asincrónica, es muy común que una operación asincrónica, cuando se
completa, invoque una segunda operación y le pase datos. Tradicionalmente, esto se hacía
utilizando métodos de devolución de llamada. En la biblioteca TPL (Task Parallel Library,
biblioteca de procesamiento paralelo basado en tareas), las tareas de continuación proporcionan
la misma funcionalidad. Una tarea de continuación (también conocida como continuación) es
una tarea asincrónica invocada por otra tarea, que se denomina antecedente, cuando el
antecedente finaliza.
Las continuaciones son relativamente fáciles de usar, pero son sin embargo muy eficaces y
flexibles. Por ejemplo, puede:
 Pasar datos del antecedente a la continuación
 Especificar las condiciones precisas en las que se invocará o no la continuación
 cancelar una continuación antes de iniciarse o cede mientras se ejecuta
 Proporcionar sugerencias sobre cómo se debería programar la continuación
 Invocar varias continuaciones desde el mismo antecedente
 invocar una continuación cuando todos los o antecedentes
 Encadenar las continuaciones una tras otra hasta cualquier longitud arbitraria
 Usar una continuación para controlar las excepciones producidas por el antecedente

MCT: Luis Dueñas Pag 187 de 336


Manual de .NET Framework 4.5

Las continuaciones se crean con el método Task.ContinueWith. En el siguiente ejemplo se


muestra el modelo básico (para mayor claridad, se omite el control de excepciones).
// The antecedent task. Can also be created with
Task.Factory.StartNew.
Task<DayOfWeek> taskA = new Task<DayOfWeek>(() =>
DateTime.Today.DayOfWeek);
// The continuation. Its delegate takes the antecedent task
// as an argument and can return a different type.
Task<string> continuation = taskA.ContinueWith((antecedent) =>
{
return String.Format("Today is {0}.",antecedent.Result);
});
// Start the antecedent.
taskA.Start();
// Use the contuation's result.
Console.WriteLine(continuation.Result);

También puede crear una continuación de varias tareas que se ejecutará cuando una parte o la
totalidad de las tareas de una matriz de tareas se haya completado, como se muestra en el
siguiente ejemplo.
Task<int>[] tasks = new Task<int>[2];
tasks[0] = new Task<int>(() =>
{
// Do some work...
return 34;
});
tasks[1] = new Task<int>(() =>
{
// Do some work...
return 8;
});
var continuation =
Task.Factory.ContinueWhenAll(tasks,(antecedents) =>
{
int answer = tasks[0].Result +
tasks[1].Result;
Console.WriteLine("The answer is {0}",
answer);
});
tasks[0].Start();
tasks[1].Start();
continuation.Wait();

Una continuación se crea en el estado WaitingForActivation y, por lo tanto, únicamente puede


iniciarla su tarea antecedente. Al llamar a Task.Start en una continuación en el código de
usuario, se produce una excepción System.InvalidOperationException.
Una continuación es un objeto Task y no bloquea el subproceso en el que se inicia. Utilice el
método Wait para bloquearlo hasta que la tarea de continuación finaliza.
Opciones de una continuación
Al crear una continuación de una sola tarea, puede usar una sobrecarga ContinueWith que tome
la enumeración System.Threading.Tasks.TaskContinuationOptions para especificar las
condiciones en las que la tarea antecedente debe iniciar la continuación. Por ejemplo, puede
especificar que la continuación se ejecute solo si el antecedente se ejecuta hasta que se haya
completado, o solo si se completó con errores, etc. Si no se cumple la condición cuando el
antecedente está listo para invocar la continuación, la continuación pasa directamente al estado
Canceled y desde ese momento no se podrá iniciar. Si especifica la opción NotOn u OnlyOn con
una continuación de varias tareas, se producirá una excepción en tiempo de ejecución.
La enumeración System.Threading.Tasks.TaskContinuationOptions también incluye las mismas
opciones que la enumeración System.Threading.Tasks.TaskCreationOptions. AttachedToParent

MCT: Luis Dueñas Pag 188 de 336


Manual de .NET Framework 4.5

, LongRunning y PreferFairness tienen los mismos significados y valores en ambos tipos de


enumeración. Estas opciones se pueden usar con continuaciones de varias tareas.
En la siguiente tabla se muestran todos los valores de TaskContinuationOptions.
Elemento Descripción
Especifica el comportamiento predeterminado cuando no se
especifican TaskContinuationOptions. La continuación se
None programará cuando el antecedente finaliza, independientemente del
estado final de este. Si la tarea es una tarea secundaria, se crea como
una tarea anidada desasociada.
Especifica que la continuación se programará de modo que las tareas
programadas antes tengan más posibilidades de ejecutarse antes y las
PreferFairness
tareas programadas después tengan más posibilidades de ejecutarse
más tarde.
Especifica que la continuación será una operación general de larga
duración. Proporciona una sugerencia al
LongRunning
System.Threading.Tasks.TaskScheduler de que se puede garantizar
la sobresuscripción.
Especifica que la continuación, si es una tarea secundaria, se adjunta
a un elemento primario en la jerarquía de tareas. La continuación es
AttachedToParent
una tarea secundaria solo si su antecedente también es una tarea
secundaria.
Especifica que si una tarea interna especifica la opción de
DenyChildAttach AttachedToParent , esa tarea no se realizará una tarea secundaria
asociada.
Especifica que las tareas creadas en esta tarea se realizan
HideScheduler TaskScheduler. Current para ser TaskScheduler.Default en lugar del
programador en el que esta tarea se está ejecutando.
Especifica que no se debe programar la continuación si su
NotOnRanToCompletion
antecedente se ejecuta hasta que se haya completado.
Especifica que no se debe programar la continuación si su
NotOnFaulted
antecedente produjo una excepción no controlada.
Especifica que no se debe programar la continuación si se cancela su
NotOnCanceled
antecedente.
Especifica que la continuación solo se debe programar si el
OnlyOnRanToCompletion
antecedente se ejecuta hasta que se haya completado.
Especifica que la continuación solo se debe programar si su
antecedente produjo una excepción no controlada. Al usar la opción
OnlyOnFaulted, se garantiza que la propiedad Exception del
antecedente no es NULL. Puede usar esa propiedad para detectar la
OnlyOnFaulted
excepción y ver qué excepción provocó el error de la tarea. Si no
tiene acceso a la propiedad Exception, no se controlará la excepción.
Asimismo, si intenta tener acceso a la propiedad Result de una tarea
cancelada o con errores, se producirá una nueva excepción.
Especifica que la continuación solo se debe programar si su
OnlyOnCanceled
antecedente finaliza en el estado de Canceled .
Para las continuaciones de muy corta duración. Especifica que lo
ideal es que la continuación se ejecute en el mismo subproceso que
causa la transición del antecedente a su estado final. Si el
ExecuteSynchronously
antecedente ya se ha completado cuando se crea la continuación, el
sistema intentará ejecutar la continuación en el subproceso que la
crea. Si CancellationTokenSource del antecedente se elimina en un

MCT: Luis Dueñas Pag 189 de 336


Manual de .NET Framework 4.5

bloque de finally (Finally en Visual Basic), una continuación con


esta opción se ejecutará que bloque de finally .
Pasar datos a una continuación
Una referencia al antecedente se pasa como argumento al delegado de usuario de la
continuación. Si el antecedente es System.Threading.Tasks.Task<TResult>, y la tarea se ejecutó
hasta que se completa, la continuación puede tener acceso a la propiedad de
Task<TResult>.Result de la tarea. Con una continuación de varias tareas y el método
Task.WaitAll, el argumento es la matriz de antecedentes. Al usar Task.WaitAny, el argumento
es el primer antecedente que se completó.
Task<TResult>.Result se bloquea hasta que la tarea se ha completado. Sin embargo, si la tarea
se canceló o tiene errores, Result produce una excepción cuando el código intenta tener acceso
al mismo. Puede evitar este problema mediante la opción OnlyOnRanToCompletion, como se
muestra en el siguiente ejemplo.
var t = Task<int>.Factory.StartNew(() => 54);
var c = t.ContinueWith((antecedent) =>
{
Console.WriteLine("continuation {0}", antecedent.Result);
},
TaskContinuationOptions.OnlyOnRanToCompletion);

Si desea que la continuación se ejecute aunque el antecedente no se hasta que se haya


completado, debe protegerse contra la excepción. Un posible enfoque es probar el estado del
antecedente e intentar tener acceso a Result solamente si el estado no es Faulted o Canceled.
También puede examinar la propiedad Exception del antecedente.
Cancelar una continuación
Una continuación pasa al estado Canceled en estos escenarios:
 Cuando produce una excepción OperationCanceledException en respuesta a una
solicitud de cancelación. Al igual que sucede con cualquier tarea, si la excepción
contiene el mismo token que se pasó a la continuación, se trata como una confirmación
de cancelación cooperativa.
 Cuando se pasa System.Threading.CancellationToken como argumento a la
continuación y la propiedad IsCancellationRequested del token es true (True) antes de
ejecutar la continuación. En este caso, la continuación no se inicia y pasa al estado de
Canceled .
 Cuando la continuación nunca se ejecuta porque no se cumple la condición establecida
en TaskContinuationOptions. Por ejemplo, si una tarea entra en estado Faulted, su
continuación, creada con la opción NotOnFaulted, pasará al estado Canceled y no se
ejecutará.
Para que una continuación no se ejecute si su antecedente se cancela, especifique la opción
NotOnCanceled al crear la continuación.
Si una tarea y su continuación representan dos partes de la misma operación lógica, puede pasar
el mismo token de cancelación a ambas tareas, como se muestra en el siguiente ejemplo.
CancellationTokenSource cts = new CancellationTokenSource();
Task task = new Task(() =>
{
CancellationToken ct = cts.Token;
while (someCondition)
{
ct.ThrowIfCancellationRequested();
// Do the work.
//...
}
},

MCT: Luis Dueñas Pag 190 de 336


Manual de .NET Framework 4.5

cts.Token
);
Task task2 = task.ContinueWith((antecedent) =>
{
CancellationToken ct = cts.Token;
while (someCondition)
{
ct.ThrowIfCancellationRequested();
// Do the work.
//...
}
},
cts.Token);
task.Start();
// Antecedent and/or continuation will
// respond to this request, depending on when it is made.
cts.Cancel();

Si el antecedente no se cancela, todavía se puede usar el token para cancelar la continuación. Si


el antecedente se cancela, no se inicia la continuación.
Después de que una continuación entra en estado Canceled, puede afectar a las continuaciones
posteriores, dependiendo de las opciones TaskContinuationOptions especificadas para esas
continuaciones.
Las continuaciones eliminadas no se inician.
Continuaciones y tareas secundarias
Una continuación no se ejecuta hasta que se completan su antecedente y todas las tareas
secundarias asociadas. La continuación no espera a que las tareas secundarias desasociadas
finalice. El estado final de la tarea antecedente depende del estado final de cualquier tarea
secundaria asociada. El estado de las tareas secundarias desasociadas no afecta a la tarea
primaria.
Asociación de estado a Continuaciones
Puede asociar el estado arbitraria con una continuación de la tarea. El método de ContinueWith
proporciona versiones sobrecargadas una de las cuales toma un valor de Object que representa
el estado de la continuación. Puede tener acceso más adelante en este objeto de estados
mediante la propiedad de Task.AsyncState . Este objeto de estado es null (NothingVisual Basic)
si no proporciona un valor.
El estado de la continuación es útil cuando se convierte el código existente que utiliza el modelo
de programación asincrónica (APM) para utilizar la TPL. En APM, se proporcionan
normalmente al estado del objeto en el método de BeginMethod y el acceso posterior que dice
mediante la propiedad de IAsyncResult.AsyncState . Utilizando el método de ContinueWith ,
puede conservar a este estado cuando convierta el código que utiliza APM para utilizar la TPL.
El estado de continuación también puede resultar útil cuando se trabaja con los objetos de Task
en el depurador de Visual Studio . Por ejemplo, en la ventana de tareas paralelas , la columna de
Tarea muestra la representación de cadena del objeto de estado para cada tarea.
El ejemplo siguiente se muestra cómo utilizar el estado de continuación. Este ejemplo crea una
cadena de las tareas de continuación. Cada tarea proporciona la hora actual, un objeto de
DateTime , para el parámetro de state del método de ContinueWith . Cada objeto de DateTime
representa el tiempo en el que se crea la tarea de continuación. Cada tarea genera como
resultado un objeto de DateTime del segundo que representa el tiempo en el que la tarea
finaliza. Después de todas las tareas, en este ejemplo se imprime en la consola la hora de
creación y el momento en que cada tarea de continuación finaliza.
using System;
using System.Collections.Generic;
using System.Threading;

MCT: Luis Dueñas Pag 191 de 336


Manual de .NET Framework 4.5

using System.Threading.Tasks;
// Demonstrates how to associate state with task continuations.
class ContinuationState
{
// Simluates a lengthy operation and returns the time at which
// the operation completed.
public static DateTime DoWork()
{
// Simulate work by suspending the current thread
// for two seconds.
Thread.Sleep(2000);
// Return the current time.
return DateTime.Now;
}
static void Main(string[] args)
{
// Start a root task that performs work.
Task<DateTime> t = Task<DateTime>.Run(delegate { return DoWork(); });
// Create a chain of continuation tasks, where each task is
// followed by another task that performs work.
List<Task<DateTime>> continuations = new List<Task<DateTime>>();
for (int i = 0; i < 5; i++)
{
// Provide the current time as the state of the continuation.
t = t.ContinueWith(delegate { return DoWork(); }, DateTime.Now);
continuations.Add(t);
}
// Wait for the last task in the chain to complete.
t.Wait();
// Print the creation time of each continuation (the state object)
// and the completion time (the result of that task) to the console.
foreach (var continuation in continuations)
{
DateTime start = (DateTime)continuation.AsyncState;
DateTime end = continuation.Result;
Console.WriteLine("Task was created at {0} and finished at {1}.",
start.TimeOfDay, end.TimeOfDay);
}
}
}
/* Sample output:
Task was created at 10:56:21.1561762 and finished at 10:56:25.1672062.
Task was created at 10:56:21.1610677 and finished at 10:56:27.1707646.
Task was created at 10:56:21.1610677 and finished at 10:56:29.1743230.
Task was created at 10:56:21.1610677 and finished at 10:56:31.1779883.
Task was created at 10:56:21.1610677 and finished at 10:56:33.1837083.
*/

Controlar las excepciones que producen las continuaciones


Una relación entre un antecedente y una continuación no es una relación primario-secundario.
Las excepciones producidas por las continuaciones no se propagan al antecedente. Por
consiguiente, las excepciones que producen las continuaciones se deben controlar de igual modo
que en cualquier otra tarea, como se indica a continuación.
1. Use el método Wait, WaitAny o WaitAll, o su homólogo genérico, para esperar en la
continuación. Puede esperar un antecedente y sus continuaciones en la misma
instrucción de try (Try en Visual Basic), como se muestra en el ejemplo siguiente.
var t = Task<int>.Factory.StartNew(() => 54);
var c = t.ContinueWith((antecedent) =>
{
Console.WriteLine("continuation {0}", antecedent.Result);
throw new InvalidOperationException();
});
try
{
t.Wait();

MCT: Luis Dueñas Pag 192 de 336


Manual de .NET Framework 4.5

c.Wait();
}
catch (AggregateException ae)
{
foreach(var e in ae.InnerExceptions)
Console.WriteLine(e.Message);
}
Console.WriteLine("Exception handled. Let's move on.");

1. Use una segunda continuación para observar la propiedad Exception de la primera


continuación.
2. Si la continuación es una tarea secundaria creada mediante la opción AttachedToParent,
la tarea primaria propagará sus excepciones al subproceso que realiza la llamada, como
sucede con cualquier otro elemento secundario asociado.
3.1.2.2. Tareas anidadas y tareas secundarias
Una tarea anidada no es más que una instancia de System.Threading.Tasks.Task que se crea en
el delegado de usuario de otra tarea. Una tarea secundaria es una tarea anidada que se crea con
la opción AttachedToParent. Una tarea puede crear cualquier número de tareas secundarias y
anidadas, con la única limitación de los recursos del sistema. En el ejemplo siguiente se muestra
una tarea primaria que crea una tarea anidada simple.
static void SimpleNestedTask()
{
var parent = Task.Factory.StartNew(() =>
{
Console.WriteLine("Outer task executing.");
var child = Task.Factory.StartNew(() =>
{
Console.WriteLine("Nested task starting.");
Thread.SpinWait(500000);
Console.WriteLine("Nested task completing.");
});
});
parent.Wait();
Console.WriteLine("Outer has completed.");
}
/* Sample output:
Outer task executing.
Nested task starting.
Outer has completed.
Nested task completing.
*/

Tareas secundarias asociadas frente a tareas anidadas desasociadas


El punto más importante respecto a elemento secundario VS. tareas anidadas es que las tareas
anidadas son esencialmente independientes de la tarea primaria o externa, mientras que las
tareas secundarias asociadas están estrechamente sincronizadas con el elemento primario. Si se
modifica la instrucción de creación de la tarea para usar la opción AttachedToParent, como se
muestra en el siguiente ejemplo,
var child = Task.Factory.StartNew((t) =>
{
Console.WriteLine("Attached child starting.");
Thread.SpinWait(5000000);
Console.WriteLine("Attached child completing.");
}, TaskCreationOptions.AttachedToParent);

se generará el siguiente resultado.


Parent task executing.
Attached child starting.
Attached child completing.
Parent has completed.

MCT: Luis Dueñas Pag 193 de 336


Manual de .NET Framework 4.5

Puede usar tareas secundarias asociadas para crear gráficos de operaciones asincrónicas con una
estrecha sincronización. Sin embargo, en la mayoría de los escenarios, recomendamos usar
tareas anidadas porque las relaciones con otras tareas son menos complejas. Esta es la razón por
la que las tareas que se crean dentro de otras tareas están anidadas de forma predeterminada y es
necesario especificar explícitamente la opción AttachedToParent para crear una tarea
secundaria.
En la tabla siguiente se muestran las diferencias básicas entre los dos tipos de tareas
secundarias.
Tareas Tareas secundarias
Categoría
anidadas asociadas
La tarea externa (primaria) espera a que las tareas internas
No Sí
se completen.
La tarea primaria propaga las excepciones iniciadas por las
No Sí
tareas secundarias (tareas internas).
El estado de la tarea primaria (tarea externa) depende del
No Sí
estado de la tarea secundaria (tarea interna).
En escenarios desasociados en los que la tarea anidada es un objetoTask<TResult>, se puede
forzar que la tarea primaria espere a la secundaria mediante el acceso a la propiedad Result de la
tarea anidada. La propiedad Result se bloquea hasta que su tarea se completa.
static void WaitForSimpleNestedTask()
{
var outer = Task<int>.Factory.StartNew(() =>
{
Console.WriteLine("Outer task executing.");
var nested = Task<int>.Factory.StartNew(() =>
{
Console.WriteLine("Nested task starting.");
Thread.SpinWait(5000000);
Console.WriteLine("Nested task completing.");
return 42;
});
// Parent will wait for this detached child.
return nested.Result;
});
Console.WriteLine("Outer has returned {0}.", outer.Result);
}
/* Sample output:
Outer task executing.
Nested task starting.
Nested task completing.
Outer has returned 42.
*/

Excepciones en tareas anidadas y secundarias


Si una tarea anidada produce una excepción, debe observarse o controlarse directamente en la
tarea exterior como si se tratara de una tarea no anidada. Si una tarea secundaria adjunta inicia
una excepción, la excepción se propaga automáticamente a la tarea primaria y de nuevo al
subproceso que espera o intenta obtener acceso a la propiedad de Result de la tarea. Por tanto, si
se usan tareas secundarias asociadas, se pueden controlar todas las excepciones en un solo
punto: la llamada a Wait del subproceso que realiza la llamada.
Cancelación y tareas secundarias
No conviene olvidar que la cancelación de tareas es cooperativa. Por tanto, para ser
"cancelable", cada tarea secundaria asociada o desasociada debe supervisar el estado del token
de cancelación. Si desea cancelar un elemento primario y todos sus elementos secundarios

MCT: Luis Dueñas Pag 194 de 336


Manual de .NET Framework 4.5

utilizando una sola solicitud de cancelación, debe pasar el mismo token como argumento a todas
las tareas y proporcionar en cada tarea la lógica de respuesta a la solicitud.

Cuando la tarea primaria se cancela


Si una tarea primaria se cancela antes de que se inicie una tarea secundaria, como es lógico, la
tarea secundaria (anidada) nunca se cancelará. Si una tarea primaria se cancela después de que
se ha iniciado una tarea secundaria o anidada, la tarea anidada (secundaria) se ejecutará hasta
completarse a menos que tenga su propia lógica de cancelación.

Cuando una tarea anidada se cancela


Si una tarea secundaria desasociada se cancela usando el mismo token que se pasó a la tarea y la
tarea primaria no espera a la secundaria, no se propagará ninguna excepción, pues la excepción
se trata cono una cancelación de cooperación benigna. Este comportamiento es igual que el de
cualquier tarea de nivel superior.
Cuando una tarea secundaria se cancela
Cuando una tarea secundaria asociada se cancela usando el mismo token que se pasó a la tarea,
se propaga una excepción TaskCanceledException al subproceso de unión dentro de
AggregateException. Es muy importante esperar a la tarea primaria para poder controlar todas
las excepciones benignas además de todos las excepciones de error que se propagan de manera
ascendente a través de un gráfico de tareas secundarias asociadas.
Evitar que una tarea secundaria adjunta a su elemento primario
Una excepción no controlada que produce una tarea secundaria se propaga a la tarea primaria.
Puede utilizar este comportamiento para observar todas las excepciones secundarias de la tarea
en una tarea raíz en lugar de recorrer un árbol de tareas. Sin embargo, la propagación de
excepciones puede ser problemática cuando una tarea primaria no cuenta con datos adjuntos de
otro código. Por ejemplo, considere una aplicación que llama a un componente de terceros de la
biblioteca de un objeto de Task. Si el componente de terceros de biblioteca también crea un
objeto de Task y especifica AttachedToParent para adjuntar a la tarea primaria, las excepciones
no controladas que aparecen en la propagación secundaria de la tarea al elemento primario. Esto
podría causar un comportamiento inesperado en la aplicación principal.
Para evitar que una tarea secundaria adjunta a su tarea primaria, especifique la opción de
DenyChildAttach cuando se crea Task o el objeto primario de Task<TResult>. Cuando una
tarea intenta asociar el elemento primario, y éste especifica la opción de DenyChildAttach, se
produce una excepción de InvalidOperationException.
Otro ejemplo en el que puede desear evitar que una tarea secundaria adjunta al elemento
primario es cuando la tarea secundaria no finaliza en el tiempo. Dado que una tarea primaria no
finaliza hasta que todo el final secundario de tareas, una tarea secundaria de ejecución
prolongada puede provocar la aplicación total para ejecutarse mal.
3.1.2.3. Cancelación de tareas
System.Threading.Tasks.Task y admiten la cancelación con de las clases de
System.Threading.Tasks. Task<TResult> con el uso de los tokenes de cancelación en .NET
Framework. En las clases de tareas, la cancelación implica la cooperación entre el delegado de
usuario, que representa una operación cancelable y el código que solicitó la cancelación. Una
cancelación correcta implica que el código solicitante llame al método
CancellationTokenSource.Cancel y que el delegado de usuario termine la operación en el
tiempo esperado. Puede finalizar la operación a través de una de estas opciones:
 Devolver simplemente un valor del delegado. En muchos escenarios esto es suficiente;
sin embargo, una instancia de tarea "cancelada" de esta manera cambia al estado
RanToCompletion, no al estado Canceled.

MCT: Luis Dueñas Pag 195 de 336


Manual de .NET Framework 4.5

 Producir una excepción OperationCanceledException y pasarle el token en el que se


solicitó la cancelación. En este caso, se prefiere usar el método
ThrowIfCancellationRequested. Una tarea cancelada de esta manera cambia al estado
Canceled, que sirve al código que realiza la llamada para comprobar que la tarea
respondió a su solicitud de cancelación.
En el siguiente ejemplo se muestra el modelo básico para la opción de cancelación de tareas que
produce la excepción. Observe que el token se pasa al delegado de usuario y a la propia
instancia de la tarea.
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
var tokenSource2 = new CancellationTokenSource();
CancellationToken ct = tokenSource2.Token;
var task = Task.Factory.StartNew(() =>
{
// Were we already canceled?
ct.ThrowIfCancellationRequested();
bool moreToDo = true;
while (moreToDo)
{
// Poll on this property if you have to do other cleanup before
throwing.
if (ct.IsCancellationRequested)
{
// Clean up here, then...
ct.ThrowIfCancellationRequested();
}
}
}, tokenSource2.Token); // Pass same token to StartNew.
tokenSource2.Cancel();
// Just continue on this thread, or Wait/WaitAll with try-catch:
try
{
task.Wait();
}
catch (AggregateException e)
{
foreach (var v in e.InnerExceptions)
Console.WriteLine(e.Message + " " + v.Message);
}
Console.ReadKey();
}
}

Cuando una instancia de tarea observa una excepción OperationCanceledException iniciada


desde el código de usuario, compara el token de la excepción con su token asociado (el que se
pasó a la API que creó la tarea). Si son iguales y la propiedad IsCancellationRequested del token
devuelve true, la tarea lo interpreta como una confirmación de cancelación y pasa al estado
Canceled. Si no se usa un método WaitAll o Wait para esperar a la tarea, esta simplemente
establece su estado en Canceled.
Si espera en una tarea que cambia al estado Canceled, se crea y se inicia una excepción Task
(encapsulada en AggregateException). Observe que esta excepción indica la cancelación
correcta en lugar de una situación de error. Por consiguiente, la propiedad Exception de la tarea
devuelve Null.
Si la propiedad IsCancellationRequested del token devuelve False o si el token de la excepción
no coincide con el token de la tarea, OperationCanceledException se trata como una excepción
normal, por lo que la tarea cambia al estado Faulted. Observe también que la presencia de otras

MCT: Luis Dueñas Pag 196 de 336


Manual de .NET Framework 4.5

excepciones también hará que la tarea pase al estado Faulted. Puede obtener el estado de la tarea
completada en la propiedad Status.
Es posible que una tarea continúe procesando algunos elementos una vez solicitada la
cancelación.
3.1.2.4. Control de excepciones
Las excepciones no controladas que se inician mediante el código de usuario que se ejecuta
dentro de una tarea se propagan de nuevo al subproceso de unión, excepto en determinados
escenarios que se describen posteriormente en este tema. Las excepciones se propagan cuando
se usa uno de los métodos estáticos o de instancia Task.Wait o Task<TResult>.Wait, y estos
métodos se controlan si la llamada se enmarca en una instrucción try-catch. Si una tarea es la
tarea primaria de unas tareas secundarias asociadas o si se esperan varias tareas, pueden
producirse varias excepciones. Para propagar todas las excepciones de nuevo al subproceso que
realiza la llamada, la infraestructura de la tarea las encapsula en una instancia de
AggregateException. AggregateException tiene una propiedad de InnerExceptions que se puede
enumerar para examinar todas las excepciones originales que se generaron, y administrar (o no)
cada individualmente. Aunque solo se inicie una única excepción, se encapsulará en un objeto
Aggregate Exception.
var task1 = Task.Factory.StartNew(() =>
{
throw new MyCustomException("I'm bad, but not too bad!");
});
try
{
task1.Wait();
}
catch (AggregateException ae)
{
// Assume we know what's going on with this particular exception.
// Rethrow anything else. AggregateException.Handle provides
// another way to express this. See later example.
foreach (var e in ae.InnerExceptions)
{
if (e is MyCustomException)
{
Console.WriteLine(e.Message);
}
else
{
throw;
}
}

Para evitar una excepción no controlada, basta con detectar el objeto AggregateException y
omitir las excepciones internas. Sin embargo, esta operación no resulta recomendable porque es
igual que detectar el tipo Exception base en escenarios no paralelos. Si desea detectar una
excepción sin realizar acciones concretas que la resuelvan, puede dejar al programa en un estado
indeterminado.
Si no espera que ninguna tarea propague la excepción ni tiene acceso a su propiedad Exception,
la excepción se escalará conforme a la directiva de excepciones de .NET cuando la tarea se
recopile como elemento no utilizado.
Cuando las excepciones pueden propagarse de nuevo al subproceso de unión, es posible que una
tarea continúe procesando algunos elementos después de que se haya producido la excepción.
Nota
Cuando está habilitada la opción "Solo mi código", en algunos casos, Visual Studio se

MCT: Luis Dueñas Pag 197 de 336


Manual de .NET Framework 4.5

interrumpe en la línea que produce la excepción y muestra el mensaje de error "Excepción no


controlada por el código de usuario". Este error es benigno. Puede presionar F5 para continuar y
ver el comportamiento de control de excepciones que se muestra en estos ejemplos. Para evitar
que Visual Studio se interrumpa con el primer error, desactive la casilla "Solo mi código" bajo
Herramientas, Opciones, Depuración, General.
Tareas secundarias asociadas y objetos AggregateException anidados
Si una tarea tiene una tarea secundaria adjunta que inicia una excepción, esa excepción se
encapsula en un objeto AggregateException antes de que se propague a la tarea primaria, que
encapsula esa excepción en su propio objeto AggregateException antes de propagarla de nuevo
al subproceso que realiza la llamada. En estos casos, la propiedad de AggregateException que se
detecta en Task.Wait o Task<TResult>.Wait o WaitAny o el método de WaitAll contiene una o
varias instancias de AggregateException , no las excepciones originales de InnerExceptions que
produjo el error. Para evitar tener que iterar en los objetos anidados AggregateExceptions, puede
utilizar el método de Flatten para quitar todos los objetos anidados, de modo que la propiedad de
AggregateException.InnerExceptions contiene las excepciones originales. En el ejemplo
siguiente, las instancias anidadas de AggregateException se reducen y se controlan en un solo
bucle.
// task1 will throw an AE inside an AE inside an AE
var task1 = Task.Factory.StartNew(() =>
{
var child1 = Task.Factory.StartNew(() =>
{
var child2 = Task.Factory.StartNew(() =>
{
throw new MyCustomException("Attached child2 faulted.");
},
TaskCreationOptions.AttachedToParent);
// Uncomment this line to see the exception rethrown.
// throw new MyCustomException("Attached child1 faulted.");
},
TaskCreationOptions.AttachedToParent);
});
try
{
task1.Wait();
}
catch (AggregateException ae)
{
foreach (var e in ae.Flatten().InnerExceptions)
{
if (e is MyCustomException)
{
// Recover from the exception. Here we just
// print the message for demonstration purposes.
Console.WriteLine(e.Message);
}
else
{
throw;
}
}
// or ...
// ae.Flatten().Handle((ex) => ex is MyCustomException);
}

Excepciones de tareas secundarias desasociadas


De forma predeterminada, las tareas secundarias están desasociadas cuando se crean. Las
excepciones producidas por tareas desasociadas deben controlarse o reiniciarse en la tarea
primaria inmediata; no se propagan de nuevo al subproceso que realiza la llamada del mismo
modo que las tareas secundarias asociadas. La tarea primaria superior puede reiniciar

MCT: Luis Dueñas Pag 198 de 336


Manual de .NET Framework 4.5

manualmente una excepción de una tarea desasociada para encapsularla en un objeto


AggregateException y propagarla de nuevo al subproceso de unión.
var task1 = Task.Factory.StartNew(() =>
{
var nested1 = Task.Factory.StartNew(() =>
{
throw new MyCustomException("Nested task faulted.");
});
// Here the exception will be escalated back to joining thread.
// We could use try/catch here to prevent that.
nested1.Wait();
});
try
{
task1.Wait();
}
catch (AggregateException ae)
{
foreach (var e in ae.Flatten().InnerExceptions)
{
if (e is MyCustomException)
{
// Recover from the exception. Here we just
// print the message for demonstration purposes.
Console.WriteLine(e.Message);
}
}
}

Aunque se use una tarea de continuación para observar una excepción en una tarea secundaria,
la tarea primaria debe seguir observando la excepción.
Excepciones que indican la cancelación cooperativa
Cuando el código de usuario de una tarea responde a una solicitud de cancelación, el
procedimiento correcto es producir una excepción OperationCanceledException que se pasa en
el token de cancelación con el que se comunicó la solicitud. Antes de intentar propagar la
excepción, la instancia de la tarea compara el token de la excepción con el que recibió durante
su creación. Si son iguales, la tarea propaga una excepción TaskCanceledException encapsulada
en un elemento AggregateException y puede verse cuando se examinan las excepciones
internas. Sin embargo, si el subproceso de unión no está esperando la tarea, no se propagará esta
excepción concreta.
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task1 = Task.Factory.StartNew(() =>
{
CancellationToken ct = token;
while (someCondition)
{
// Do some work...
Thread.SpinWait(50000);
ct.ThrowIfCancellationRequested();
}
},
token);
// No waiting required.

Usar el método Handle para filtrar excepciones internas


El método AggregateException.Handle puede usarse para filtrar excepciones que pueden
tratarse como "controladas" sin necesidad de usar ninguna otra lógica. En el delegado de usuario
que se proporciona a AggregateException.Handle, se puede examinar el tipo de excepción, su
propiedad Message o cualquier otra información sobre esta excepción que permita determinar si
es benigna. Cualquier excepción que retornos false de delegado se reinician en una nueva

MCT: Luis Dueñas Pag 199 de 336


Manual de .NET Framework 4.5

instancia de AggregateException inmediatamente después de AggregateException.Handle


devuelve.
En el siguiente fragmento de código se usa un bucle foreach sobre las excepciones internas.
foreach (var e in ae.InnerExceptions)
{
if (e is MyCustomException)
{
Console.WriteLine(e.Message);
}
else
{
throw;
}
}

En el siguiente fragmento de código se muestra el uso del método Handle con la misma función.
ae.Handle((ex) =>
{
return ex is MyCustomException;
});

Observar excepciones mediante la propiedad Task.Exception


Si una tarea se completa con el estado Faulted, se puede examinar su propiedad Exception para
detectar qué excepción concreta produjo el error. Un mecanismo adecuado para observar la
propiedad Exception es usar una continuación que se ejecute solo si se produce un error en la
tarea anterior, tal y como se muestra en el siguiente ejemplo.
var task1 = Task.Factory.StartNew(() =>
{
throw new MyCustomException("Task1 faulted.");
})
.ContinueWith((t) =>
{
Console.WriteLine("I have observed a {0}",
t.Exception.InnerException.GetType().Name);
},
TaskContinuationOptions.OnlyOnFaulted);

En una aplicación real, el delegado de continuación podría registrar información detallada sobre
la excepción y posiblemente generar nuevas tareas para recuperarse de la excepción.
Evento UnobservedTaskException
En algunos escenarios (por ejemplo, cuando se hospedan complementos que no son de
confianza), es posible que se produzcan numerosas excepciones benignas y que resulte
demasiado difícil observarlas todas manualmente. En estos casos, se puede proceder a controlar
el evento TaskScheduler. UnobservedTaskException. La instancia de
System.Threading.Tasks.UnobservedTaskException EventArgs que se pasa al controlador se
puede utilizar para evitar que la excepción no observada se propague de nuevo al subproceso de
unión.
3.1.2.5. Cómo: Usar Parallel.Invoke para ejecutar operaciones
paralelas
Este ejemplo muestra cómo paralelizar las operaciones utilizando Invoke en la biblioteca TPL.
En un origen de datos compartido se realizan tres operaciones. Dado que ninguna de ellas
modifica el origen, se pueden ejecutar en paralelo de manera sencilla.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la TPL.

MCT: Luis Dueñas Pag 200 de 336


Manual de .NET Framework 4.5

Ejemplo
namespace ParallelTasks
{
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net;

class ParallelInvoke
{
static void Main()
{
// Retrieve Darwin's "Origin of the Species" from Gutenberg.org.
string[] words =
CreateWordArray(@"http://www.gutenberg.org/files/2009/2009.txt");

#region ParallelTasks
// Perform three tasks in parallel on the source array
Parallel.Invoke(() =>
{
Console.WriteLine("Begin first task...");
GetLongestWord(words);
}, // close first Action
() =>
{
Console.WriteLine("Begin second task...");
GetMostCommonWords(words);
}, //close second Action
() =>
{
Console.WriteLine("Begin third task...");
GetCountForWord(words, "species");
} //close third Action
); //close parallel.invoke
Console.WriteLine("Returned from Parallel.Invoke");
#endregion
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}

#region HelperMethods
private static void GetCountForWord(string[] words, string term)
{
var findWord = from word in words
where word.ToUpper().Contains(term.ToUpper())
select word;
Console.WriteLine(@"Task 3 -- The word ""{0}"" occurs {1} times.",
term, findWord.Count());
}

private static void GetMostCommonWords(string[] words)


{
var frequencyOrder = from word in words
where word.Length > 6
group word by word into g
orderby g.Count() descending
select g.Key;
var commonWords = frequencyOrder.Take(10);
StringBuilder sb = new StringBuilder();
sb.AppendLine("Task 2 -- The most common words are:");
foreach (var v in commonWords)
{
sb.AppendLine(" " + v);
}

MCT: Luis Dueñas Pag 201 de 336


Manual de .NET Framework 4.5

Console.WriteLine(sb.ToString());
}

private static string GetLongestWord(string[] words)


{
var longestWord = (from w in words
orderby w.Length descending
select w).First();
Console.WriteLine("Task 1 -- The longest word is {0}",
longestWord);
return longestWord;
}

// An http request performed synchronously for simplicity.


static string[] CreateWordArray(string uri)
{
Console.WriteLine("Retrieving from {0}", uri);
// Download a web page the easy way.
string s = new WebClient().DownloadString(uri);
// Separate string into an array of words, removing some common
punctuation.
return s.Split(
new char[] { ' ', '\u000A', ',', '.', ';', ':', '-', '_', '/'
},
StringSplitOptions.RemoveEmptyEntries);
}
#endregion
}
/* Output (May vary on each execution):
Retrieving from http://www.gutenberg.org/dirs/etext99/otoos610.txt
Response stream received.
Begin first task...
Begin second task...
Task 2 -- The most common words are:
species
selection
varieties
natural
animals
between
different
distinct
several
conditions
Begin third task...
Task 1 -- The longest word is characteristically
Task 3 -- The word "species" occurs 1927 times.
Returned from Parallel.Invoke
Press any key to exit
*/
}

Observe que con Invoke, simplemente expresa qué acciones desea que se ejecuten
simultáneamente; el runtime controla todos los detalles de programación de subprocesos,
incluido el escalado automático al número de núcleos del equipo host.
En este ejemplo se paralelizan las operaciones, no los datos. Como enfoque alternativo, puede
paralelizar los consultas LINQ mediante PLINQ y ejecutar las consultas de forma secuencial.
También puede paralelizar los datos con PLINQ. Otra opción consiste en paralelizar las
consultas y las tareas. Aunque la sobrecarga resultante podría degradar el rendimiento en
equipos host con relativamente pocos procesadores, se ajustaría mucho mejor en equipos con
muchos procesadores.
3.1.2.6. Cómo: Devolver un valor de una tarea
En este ejemplo se muestra cómo se usa el tipo System.Threading.Tasks.Task<TResult> para
devolver un valor de la propiedad Result.

MCT: Luis Dueñas Pag 202 de 336


Manual de .NET Framework 4.5

Ejemplo
using System;
using System.Linq;
using System.Threading.Tasks;

class Program
{
static void Main()
{
// Return a value type with a lambda expression
Task<int> task1 = Task<int>.Factory.StartNew(() => 1);
int i = task1.Result;
// Return a named reference type with a multi-line statement lambda.
Task<Test> task2 = Task<Test>.Factory.StartNew(() =>
{
string s = ".NET";
double d = 4.0;
return new Test { Name = s, Number = d };
});
Test test = task2.Result;
// Return an array produced by a PLINQ query
Task<string[]> task3 = Task<string[]>.Factory.StartNew(() =>
{
string path = @"C:\users\public\pictures\";
string[] files = System.IO.Directory.GetFiles(path);
var result = (from file in files.AsParallel()
let info = new System.IO.FileInfo(file)
where info.Extension == ".jpg"
select file).ToArray();
return result;
});
foreach (var name in task3.Result)
Console.WriteLine(name);
}
class Test
{
public string Name { get; set; }
public double Number { get; set; }
}
}

La propiedad Result bloquea el subproceso que realiza la llamada hasta que la tarea finaliza.
3.1.2.7. Cómo: Esperar a que una o varias tareas se completen
En este ejemplo se muestra cómo utilizar el método Wait o su equivalente en la clase
Task<TResult>, para esperar en una tarea única. También se muestra cómo utilizar los métodos
WaitAny y WaitAll estáticos para esperar en varias tareas.
Ejemplo
using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static Random rand = new Random();
static void Main(string[] args)
{
// Wait on a single task with no timeout specified.
Task taskA = Task.Factory.StartNew(() => DoSomeWork(10000000));
taskA.Wait();
Console.WriteLine("taskA has completed.");
// Wait on a single task with a timeout specified.
Task taskB = Task.Factory.StartNew(() => DoSomeWork(10000000));
taskB.Wait(100); //Wait for 100 ms.

MCT: Luis Dueñas Pag 203 de 336


Manual de .NET Framework 4.5

if (taskB.IsCompleted)
Console.WriteLine("taskB has completed.");
else
Console.WriteLine("Timed out before taskB completed.");
// Wait for all tasks to complete.
Task[] tasks = new Task[10];
for (int i = 0; i < 10; i++)
{
tasks[i] = Task.Factory.StartNew(() => DoSomeWork(10000000));
}
Task.WaitAll(tasks);
// Wait for first task to complete.
Task<double>[] tasks2 = new Task<double>[3];
// Try three different approaches to the problem. Take the first
one.
tasks2[0] = Task<double>.Factory.StartNew(() => TrySolution1());
tasks2[1] = Task<double>.Factory.StartNew(() => TrySolution2());
tasks2[2] = Task<double>.Factory.StartNew(() => TrySolution3());
int index = Task.WaitAny(tasks2);
double d = tasks2[index].Result;
Console.WriteLine("task[{0}] completed first with result of {1}.",
index, d);
Console.ReadKey();
}

static void DoSomeWork(int val)


{
// Pretend to do something.
Thread.SpinWait(val);
}

static double TrySolution1()


{
int i = rand.Next(1000000);
// Simulate work by spinning
Thread.SpinWait(i);
return DateTime.Now.Millisecond;
}
static double TrySolution2()
{
int i = rand.Next(1000000);
// Simulate work by spinning
Thread.SpinWait(i);
return DateTime.Now.Millisecond;
}
static double TrySolution3()
{
int i = rand.Next(1000000);
// Simulate work by spinning
Thread.SpinWait(i);
Thread.SpinWait(1000000);
return DateTime.Now.Millisecond;
}
}

Por razones de simplicidad, estos ejemplos no muestran el código de control de excepciones ni


el código de cancelación. En la mayoría de los casos, debe incluir un método Wait en un bloque
try-catch, porque la espera es el mecanismo por el que el código de programa controla las
excepciones que se inician en cualquiera de las tareas. Si la tarea se puede cancelar, debe
comprobar las propiedades IsCanceled o IsCancellationRequested antes de intentar utilizar la
tarea o su propiedad Result.
3.1.2.8. Cómo: Cancelar una tarea y sus elementos secundarios
En estos ejemplos se muestra cómo realizar las tareas siguientes:
1. Crear e iniciar una tarea cancelable.

MCT: Luis Dueñas Pag 204 de 336


Manual de .NET Framework 4.5

2. Pasar un token de cancelación a un delegado de usuario y, opcionalmente, a la instancia


de la tarea.
3. Observar y responder a la solicitud de cancelación en el delegado de usuario.
4. Opcionalmente, observar en el subproceso que realiza la llamada que la tarea se
canceló.
El subproceso que realiza la llamada no finaliza la tarea forzosamente, sino que solo señala que
se solicita la cancelación. Si la tarea ya se está ejecutando, es el delegado de usuario el que debe
observar la solicitud y responder según corresponda. Si la cancelación se solicita antes de
ejecutarse la tarea, el delegado de usuario nunca se ejecuta y el objeto de tarea pasa al estado
Cancelado.
Ejemplo
En este ejemplo se muestra cómo finalizar un objeto Task y sus elementos secundarios en
respuesta a una solicitud de cancelación. También se muestra que, cuando un delegado de
usuario finaliza con una excepción OperationCanceledException, el subproceso que realiza la
llamada puede usar opcionalmente el método Wait o el método WaitAll para esperar a que las
tareas finalicen. En este caso, el delegado debe usar un bloque try-catch para controlar las
excepciones en el subproceso que realiza la llamada.
namespace CancellationWithOCE
{
using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static void Main(string[] args)
{
Console.WriteLine("Press any key to start. Press 'c' to cancel.");
Console.ReadKey();
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
// Store references to the tasks so that we can wait on them and
// observe their status after cancellation.
Task[] tasks = new Task[10];
// Request cancellation of a single task when the token source is
canceled.
// Pass the token to the user delegate, and also to the task so it
can
// handle the exception correctly.
tasks[0] = Task.Factory.StartNew(() => DoSomeWork(1, token),
token);
// Request cancellation of a task and its children. Note the token
is passed
// to (1) the user delegate and (2) as the second argument to
StartNew, so
// that the task instance can correctly handle the
OperationCanceledException.
tasks[1] = Task.Factory.StartNew(() =>
{
// Create some cancelable child tasks.
for (int i = 2; i < 10; i++)
{
// For each child task, pass the same token
// to each user delegate and to StartNew.
tasks[i] = Task.Factory.StartNew(iteration =>
DoSomeWork((int)iteration, token), i, token);
}
// Passing the same token again to do work on the parent task.
// All will be signaled by the call to tokenSource.Cancel
below.
DoSomeWork(2, token);

MCT: Luis Dueñas Pag 205 de 336


Manual de .NET Framework 4.5

}, token);
// Give the tasks a second to start.
Thread.Sleep(1000);
// Request cancellation from the UI thread.
if (Console.ReadKey().KeyChar == 'c')
{
tokenSource.Cancel();
Console.WriteLine("\nTask cancellation requested.");
// Optional: Observe the change in the Status property on the
task.
// It is not necessary to wait on tasks that have canceled.
However,
// if you do wait, you must enclose the call in a try-catch
block to
// catch the OperationCanceledExceptions that are thrown. If
you do
// not wait, no OCE is thrown if the token that was passed to
the
// StartNew method is the same token that requested the
cancellation.
#region Optional_WaitOnTasksToComplete
try
{
Task.WaitAll(tasks);
}
catch (AggregateException e)
{
// For demonstration purposes, show the OCE message.
foreach (var v in e.InnerExceptions)
Console.WriteLine("msg: " + v.Message);
}
// Prove that the tasks are now all in a canceled state.
for (int i = 0; i < tasks.Length; i++)
Console.WriteLine("task[{0}] status is now {1}", i,
tasks[i].Status);
#endregion
}
// Keep the console window open while the
// task completes its output.
Console.ReadLine();
}

static void DoSomeWork(int taskNum, CancellationToken ct)


{
// Was cancellation already requested?
if (ct.IsCancellationRequested)
{
Console.WriteLine("We were cancelled before we got started.");
Console.WriteLine("Press Enter to quit.");
ct.ThrowIfCancellationRequested();
}
int maxIterations = 1000;
// NOTE!!! A benign "OperationCanceledException was unhandled
// by user code" error might be raised here. Press F5 to continue.
Or,
// to avoid the error, uncheck the "Enable Just My Code"
// option under Tools > Options > Debugging.
for (int i = 0; i < maxIterations; i++)
{
// Do a bit of work. Not too much.
var sw = new SpinWait();
for (int j = 0; j < 3000; j++) sw.SpinOnce();
Console.WriteLine("...{0} ", taskNum);
if (ct.IsCancellationRequested)
{
Console.WriteLine("bye from {0}.", taskNum);
Console.WriteLine("\nPress Enter to quit.");
ct.ThrowIfCancellationRequested();

MCT: Luis Dueñas Pag 206 de 336


Manual de .NET Framework 4.5

}
}
}
}
}

La clase System.Threading.Tasks.Task está totalmente integrada con el modelo de cancelación


basado en los tipos System.Threading.CancellationToken y
System.Threading.CancellationTokenSource.
3.1.2.9. Cómo: Controlar excepciones iniciadas por tareas
En los siguientes ejemplos, se muestra cómo controlar las excepciones producidas en una o
varias tareas.
Ejemplo
En este primer ejemplo, se detecta la excepción System.AggregateException y, a continuación,
se examina su propiedad InnerExceptions para determinar si algunas de las excepciones se
pueden controlar mediante el código del programa.
static string[] GetAllFiles(string str)
{
// Should throw an AccessDenied exception on Vista.
return System.IO.Directory.GetFiles(str, "*.txt",
System.IO.SearchOption.AllDirectories);
}

static void HandleExceptions()


{
// Assume this is a user-entered string.
string path = @"C:\";
// Use this line to throw UnauthorizedAccessException, which we handle.
Task<string[]> task1 = Task<string[]>.Factory.StartNew(() =>
GetAllFiles(path));
// Use this line to throw an exception that is not handled.
// Task task1 = Task.Factory.StartNew(() => { throw new
IndexOutOfRangeException(); } );
try
{
task1.Wait();
}
catch (AggregateException ae)
{
ae.Handle((x) =>
{
if (x is UnauthorizedAccessException) // This we know how to
handle.
{
Console.WriteLine("You do not have permission to access all
folders in
this path.");
Console.WriteLine("See your network administrator or try
another
path.");
return true;
}
return false; // Let anything else stop the application.
});
}
Console.WriteLine("task1 has completed.");
}

En este ejemplo, se detecta la excepción System.AggregateException, pero no se intenta


controlar ninguna de sus excepciones internas. En su lugar, se utiliza el método Flatten para
extraer las excepciones internas de todas las instancias anidadas de AggregateException y
volver a iniciar una sola excepción AggregateException que contiene directamente todas las

MCT: Luis Dueñas Pag 207 de 336


Manual de .NET Framework 4.5

excepciones internas no controladas. Al reducir la excepción, resulta más fácil controlarla


mediante código de cliente.
static string[] GetValidExtensions(string path)
{
if (path == @"C:\")
throw new ArgumentException("The system root is not a valid path.");
return new string[10];
}

static void RethrowAllExceptions()


{
// Assume this is a user-entered string.
string path = @"C:\";
Task<string[]>[] tasks = new Task<string[]>[3];
tasks[0] = Task<string[]>.Factory.StartNew(() => GetAllFiles(path));
tasks[1] = Task<string[]>.Factory.StartNew(() =>
GetValidExtensions(path));
tasks[2] = Task<string[]>.Factory.StartNew(() => new string[10]);
//int index = Task.WaitAny(tasks2);
//double d = tasks2[index].Result;
try
{
Task.WaitAll(tasks);
}
catch (AggregateException ae)
{
throw ae.Flatten();
}
Console.WriteLine("task1 has completed.");
}

Para ejecutar este ejemplo, pegue el código en el ejemplo anterior y llame a


RethrowAllExceptions desde el método Main.
3.1.2.10. Cómo: Encadenar varias tareas con continuaciones
En la biblioteca TPL, una tarea cuyo método ContinueWith se invoca se denomina tarea
antecedente y la tarea que se define en el método ContinueWith se denomina continuación. En
este ejemplo se muestra cómo usar los métodos ContinueWith y ContinueWith de las clases
Task y Task<TResult> para especificar una tarea que se inicia después de que finalice su tarea
antecedente.
También muestra cómo especificar una continuación que solo se ejecuta si la tarea antecedente
se cancela.
En estos ejemplos se muestra cómo continuar desde una tarea única. También puede crear una
continuación que se ejecute después de que alguno o todos los grupos de tareas se completen o
se cancelen.
Ejemplo
En el método DoSimpleContinuation, se muestra la sintaxis básica de ContinueWith. Observe
que la tarea antecedente se proporciona como el parámetro de entrada a la expresión lambda en
el método ContinueWith. Esto le permite evaluar el estado de la tarea antecedente antes de
realizar cualquier trabajo en la continuación. Utilice esta sobrecarga simple de ContinueWith
cuando no tenga que pasar un estado de una tarea a otra.
En el método DoSimpleContinuationWithState, se muestra cómo utilizar ContinueWith para
pasar el resultado de la tarea antecedente a la tarea de continuación.
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

MCT: Luis Dueñas Pag 208 de 336


Manual de .NET Framework 4.5

namespace ContinueWith
{
class Continuations
{
static void Main()
{
SimpleContinuation();
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}

static void SimpleContinuation()


{
string path = @"C:\users\public\TPLTestFolder\";
try
{
var firstTask = new Task(() => CopyDataIntoTempFolder(path));
var secondTask = firstTask.ContinueWith((t) =>
CreateSummaryFile(path));
firstTask.Start();
}
catch (AggregateException e)
{
Console.WriteLine(e.Message);
}
}

// A toy function to simulate a workload


static void CopyDataIntoTempFolder(string path)
{
System.IO.Directory.CreateDirectory(path);
Random rand = new Random();
for (int x = 0; x < 50; x++)
{
byte[] bytes = new byte[1000];
rand.NextBytes(bytes);
string filename = Path.GetRandomFileName();
string filepath = Path.Combine(path, filename);
System.IO.File.WriteAllBytes(filepath, bytes);
}
}

static void CreateSummaryFile(string path)


{
string[] files = System.IO.Directory.GetFiles(path);
Parallel.ForEach(files, (file) =>
{
Thread.SpinWait(5000);
});
System.IO.File.WriteAllText(Path.Combine(path, "__SummaryFile.txt"),
"did my work");
Console.WriteLine("Done with task2");
}

static void SimpleContinuationWithState()


{
int[] nums = { 19, 17, 21, 4, 13, 8, 12, 7, 3, 5 };
var f0 = new Task<double>(() => nums.Average());
var f1 = f0.ContinueWith(t => GetStandardDeviation(nums, t.Result));
f0.Start();
Console.WriteLine("the standard deviation is {0}", f1.Result);
}

private static double GetStandardDeviation(int[] values, double mean)


{
double d = 0.0;
foreach (var n in values)

MCT: Luis Dueñas Pag 209 de 336


Manual de .NET Framework 4.5

{
d += Math.Pow(mean - n, 2);
}
return Math.Sqrt(d / (values.Length - 1));
}
}
}

El parámetro de tipo de Task<TResult> determina el tipo devuelto del delegado. Ese valor
devuelto se pasa a la tarea de continuación. Es posible encadenar un número arbitrario de tareas
de esta manera.
3.1.2.11. Cómo: Crear tareas precalculadas
Este documento se describe cómo usar el método de Task.FromResult<TResult> para recuperar
los resultados de las operaciones asincrónicas de descarga que se retienen en la memoria caché.
El método de FromResult<TResult> devuelve un objeto terminado de Task<TResult> que
celebre el valor proporcionado como su propiedad de Result . Este método es útil al realizar una
operación asincrónica que devuelve un objeto de Task<TResult> , y el resultado de ese objeto
de Task<TResult> se calcula ya.
Ejemplo
El ejemplo siguiente descarga las cadenas de web. define el método de DownloadStringAsync .
Este método descarga las cadenas de web de forma asincrónica. Este ejemplo también utiliza un
objeto de ConcurrentDictionary<TKey, TValue> almacenar en caché los resultados de
operaciones anteriores. Si se celebra la dirección de la entrada en esta memoria caché,
DownloadStringAsync utiliza el método de FromResult<TResult> para generar un objeto de
Task<TResult> que contiene el contenido en esa dirección. Si no, DownloadStringAsync
descarga el archivo web y agrega el resultado a la memoria caché.
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

// Demonstrates how to use Task<TResult>.FromResult to create a task


// that holds a pre-computed result.
class CachedDownloads
{
// Holds the results of download operations.
static ConcurrentDictionary<string, string> cachedDownloads =
new ConcurrentDictionary<string, string>();
// Asynchronously downloads the requested resource as a string.
public static Task<string> DownloadStringAsync(string address)
{
// First try to retrieve the content from cache.
string content;
if (cachedDownloads.TryGetValue(address, out content))
{
return Task.FromResult<string>(content);
}
// If the result was not in the cache, download the
// string and add it to the cache.
return Task.Run(async () =>
{
content = await new WebClient().DownloadStringTaskAsync(address);
cachedDownloads.TryAdd(address, content);
return content;
});
}

static void Main(string[] args)


{

MCT: Luis Dueñas Pag 210 de 336


Manual de .NET Framework 4.5

// The URLs to download.


string[] urls = new string[]
{
"http://msdn.microsoft.com",
"http://www.contoso.com",
"http://www.microsoft.com"
};

// Used to time download operations.


Stopwatch stopwatch = new Stopwatch();
// Compute the time required to download the URLs.
stopwatch.Start();
var downloads = from url in urls select DownloadStringAsync(url);
Task.WhenAll(downloads).ContinueWith(results =>
{
stopwatch.Stop();
// Print the number of characters download and the elapsed time.
Console.WriteLine("Retrieved {0} characters. Elapsed time was {1}
ms.",
results.Result.Sum(result => result.Length),
stopwatch.ElapsedMilliseconds);
})
.Wait();
// Perform the same operation a second time. The time required
// should be shorter because the results are held in the cache.
stopwatch.Restart();
downloads = from url in urls
select DownloadStringAsync(url);
Task.WhenAll(downloads).ContinueWith(results =>
{
stopwatch.Stop();
// Print the number of characters download and the elapsed time.
Console.WriteLine("Retrieved {0} characters. Elapsed time was {1}
ms.",
results.Result.Sum(result => result.Length),
stopwatch.ElapsedMilliseconds);
})
.Wait();
}
}
/* Sample output:
Retrieved 27798 characters. Elapsed time was 1045 ms.
Retrieved 27798 characters. Elapsed time was 0 ms.
*/

Este ejemplo calcula el tiempo necesario para descargar varias cadenas dos veces. El segundo
conjunto de operaciones de descarga debe tardar menos tiempo que el primer conjunto porque
los resultados se retienen en la memoria caché. El método de FromResult<TResult> permite que
el método de DownloadStringAsync para crear los objetos de Task<TResult> que contienen
estos resultados pre-calculados.
3.1.2.12. Cómo: Recorrer un árbol binario con tareas paralelas
En el siguiente ejemplo se muestran dos maneras de usar tareas paralelas para atravesar una
estructura de datos en árbol. La creación del propio árbol se deja como ejercicio.
Ejemplo
public class TreeWalk
{
static void Main()
{
Tree<MyClass> tree = new Tree<MyClass>();
// ...populate tree (left as an exercise)
// Define the Action to perform on each node.
Action<MyClass> myAction = x => Console.WriteLine("{0} : {1}",
x.Name,

MCT: Luis Dueñas Pag 211 de 336


Manual de .NET Framework 4.5

x.Number);
// Traverse the tree with parallel tasks.
DoTree(tree, myAction);
}

public class MyClass


{
public string Name { get; set; }
public int Number { get; set; }
}

public class Tree<T>


{
public Tree<T> Left;
public Tree<T> Right;
public T Data;
}

// By using tasks explcitly.


public static void DoTree<T>(Tree<T> tree, Action<T> action)
{
if (tree == null) return;
var left = Task.Factory.StartNew(() => DoTree(tree.Left, action));
var right = Task.Factory.StartNew(() => DoTree(tree.Right,
action));
action(tree.Data);
try
{
Task.WaitAll(left, right);
}
catch (AggregateException )
{
//handle exceptions here
}
}

// By using Parallel.Invoke
public static void DoTree2<T>(Tree<T> tree, Action<T> action)
{
if (tree == null) return;
Parallel.Invoke(
() => DoTree2(tree.Left, action),
() => DoTree2(tree.Right, action),
() => action(tree.Data)
);
}
}

Los dos métodos mostrados son equivalentes desde el punto de vista funcional. Cuando se usa el
método StartNew para crear y ejecutar las tareas, estas devuelven un identificador que se puede
usar para esperar en ellas y controlar las excepciones.
3.1.2.13. Cómo: Desencapsular una tarea anidada
Puede devolver una tarea de un método y esperar o continuar a partir de esa tarea, como se
muestra en el siguiente ejemplo:
static Task<string> DoWorkAsync()
{
return Task<String>.Factory.StartNew(() =>
{
//...
return "Work completed.";
});
}

static void StartTask()


{

MCT: Luis Dueñas Pag 212 de 336


Manual de .NET Framework 4.5

Task<String> t = DoWorkAsync();
t.Wait();
Console.WriteLine(t.Result);
}

En el ejemplo anterior, la propiedad Result es de tipo string (String en Visual Basic).


Sin embargo, en algunos casos le podría interesar crear una tarea dentro de otra y devolver la
tarea anidada. En este caso, TResult de la tarea que incluye es, en sí misma, una tarea. En el
siguiente ejemplo, la propiedad Result es Task<Task<string>> en C# o Task(Of Task(Of
String)) en Visual Basic.
// Note the type of t and t2.
Task<Task<string>> t = Task.Factory.StartNew(() => DoWorkAsync());
Task<Task<string>> t2 = DoWorkAsync().ContinueWith((s) => DoMoreWorkAsync());
// Outputs: System.Threading.Tasks.Task`1[System.String]
Console.WriteLine(t.Result);

Aunque es posible escribir código para desempaquetar la tarea exterior y recuperar la tarea
original y su propiedad Result, tal código no es fácil de escribir porque se deben controlar las
excepciones y también las solicitudes de cancelación. En esta situación, recomendamos utilizar
uno de los métodos de extensión Unwrap, como se muestra en el siguiente ejemplo.
// Unwrap the inner task.
Task<string> t3 = DoWorkAsync().ContinueWith((s) =>
DoMoreWorkAsync()).Unwrap();
// Outputs "More work completed."
Console.WriteLine(t.Result);

Los métodos Unwrap se pueden utilizar para transformar Task<Task> o Task<Task<TResult>>


(Task(Of Task) o Task(Of Task(Of TResult)) en Visual Basic) en Task o Task<TResult>
(Task(Of TResult) en Visual Basic). La nueva tarea representa al completo la tarea anidada
interna e incluye el estado de cancelación y todas las excepciones.
Ejemplo
En el ejemplo siguiente se muestra cómo usar los métodos de extensión Unwrap.
namespace Unwrap
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
// A program whose only use is to demonstrate Unwrap.
class Program
{
static void Main()
{
// An arbitrary threshold value.
byte threshold = 0x40;
// data is a Task<byte[]>
var data = Task<byte[]>.Factory.StartNew(() =>
{
return GetData();
});
// We want to return a task so that we can
// continue from it later in the program.
// Without Unwrap: stepTwo is a Task<Task<byte[]>>
// With Unwrap: stepTwo is a Task<byte[]>
var stepTwo = data.ContinueWith((antecedent) =>
{
return Task<byte>.Factory.StartNew( () =>
Compute(antecedent.Result));
})
.Unwrap();

MCT: Luis Dueñas Pag 213 de 336


Manual de .NET Framework 4.5

// Without Unwrap: antecedent.Result = Task<byte>


// and the following method will not compile.
// With Unwrap: antecedent.Result = byte and
// we can work directly with the result of the Compute method.
var lastStep = stepTwo.ContinueWith( (antecedant) =>
{
if (antecedant.Result >= threshold)
{
return Task.Factory.StartNew( () => Console.WriteLine
("Program complete. Final = 0x{0:x} threshold =
0x{1:x}",
stepTwo.Result, threshold));
}
else
{
return DoSomeOtherAsyncronousWork(stepTwo.Result,
threshold);
}
});
lastStep.Wait();
Console.WriteLine("Press any key");
Console.ReadKey();
}

#region Dummy_Methods
private static byte[] GetData()
{
Random rand = new Random();
byte[] bytes = new byte[64];
rand.NextBytes(bytes);
return bytes;
}

static Task DoSomeOtherAsyncronousWork(int i, byte b2)


{
return Task.Factory.StartNew(() =>
{
Thread.SpinWait(500000);
Console.WriteLine("Doing more work. Value was <=
threshold");
});
}
static byte Compute(byte[] data)
{

byte final = 0;
foreach (byte item in data)
{
final ^= item;
Console.WriteLine("{0:x}", final);
}
Console.WriteLine("Done computing");
return final;
}
#endregion
}
}

3.1.2.14. Cómo: Evitar que una tarea secundaria se adjunte a su


elemento primario
En este documento se muestra cómo evitar que una tarea secundaria adjunta a la tarea primaria.
Evitar que una tarea secundaria adjunta a su elemento primario es útil cuando se llama a un
componente escrito por terceros y que también utilice tareas. Por ejemplo, un componente de
terceros que utiliza la opción de TaskCreationOptions.AttachedToParent de crear un objeto de

MCT: Luis Dueñas Pag 214 de 336


Manual de .NET Framework 4.5

Task o de Task<TResult> puede producir problemas en el código si es de ejecución prolongada


o produce una excepción no controlada.
Ejemplo
El ejemplo siguiente se comparan los efectos de utilizar opciones predeterminadas a los efectos
de evitar que una tarea secundaria adjunta al elemento primario. El ejemplo crea un objeto de
Task que llama a una biblioteca de terceros que también utilice un objeto de Task . La biblioteca
de otro fabricante utiliza la opción de AttachedToParent de crear el objeto de Task . la
aplicación utiliza la opción de TaskCreationOptions.DenyChildAttach de crear la tarea primaria.
Esta opción indica al tiempo de ejecución para quitar la especificación de AttachedToParent en
tareas secundarias.
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

// Defines functionality that is provided by a third-party.


// In a real-world scenario, this would likely be provided
// in a separate code file or assembly.
namespace Contoso
{
public class Widget
{
public Task Run()
{
// Create a long-running task that is attached to the
// parent in the task hierarchy.
return Task.Factory.StartNew(() =>
{
// Simulate a lengthy operation.
Thread.Sleep(5000);
}, TaskCreationOptions.AttachedToParent);
}
}
}

// Demonstrates how to prevent a child task from attaching to the parent.


class DenyChildAttach
{
static void RunWidget(Contoso.Widget widget,TaskCreationOptions
parentTaskOptions)
{
// Record the time required to run the parent and child tasks.
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
Console.WriteLine("Starting widget as a background task...");
// Run the widget task in the background.
Task<Task> runWidget = Task.Factory.StartNew(() =>
{
Task widgetTask = widget.Run();
// Perform other work while the task runs...
Thread.Sleep(1000);
return widgetTask;
}, parentTaskOptions);
// Wait for the parent task to finish.
Console.WriteLine("Waiting for parent task to finish...");
runWidget.Wait();
Console.WriteLine("Parent task has finished. Elapsed time is {0} ms.",
stopwatch.ElapsedMilliseconds);
// Perform more work...
Console.WriteLine("Performing more work on the main thread...");
Thread.Sleep(2000);
Console.WriteLine("Elapsed time is {0} ms.",
stopwatch.ElapsedMilliseconds);
// Wait for the child task to finish.

MCT: Luis Dueñas Pag 215 de 336


Manual de .NET Framework 4.5

Console.WriteLine("Waiting for child task to finish...");


runWidget.Result.Wait();
Console.WriteLine("Child task has finished. Elapsed time is {0} ms.",
stopwatch.ElapsedMilliseconds);
}

static void Main(string[] args)


{
Contoso.Widget w = new Contoso.Widget();
// Perform the same operation two times. The first time, the operation
// is performed by using the default task creation options. The second
// time, the operation is performed by using the DenyChildAttach option
// in the parent task.
Console.WriteLine("Demonstrating parent/child tasks with default
options...");
RunWidget(w, TaskCreationOptions.None);
Console.WriteLine();
Console.WriteLine("Demonstrating parent/child tasks with the
DenyChildAttach
option...");
RunWidget(w, TaskCreationOptions.DenyChildAttach);
}
}
/* Sample output:
Demonstrating parent/child tasks with default options...
Starting widget as a background task...
Waiting for parent task to finish...
Parent task has finished. Elapsed time is 5014 ms.
Performing more work on the main thread...
Elapsed time is 7019 ms.
Waiting for child task to finish...
Child task has finished. Elapsed time is 7019 ms.

Demonstrating parent/child tasks with the DenyChildAttach option...


Starting widget as a background task...
Waiting for parent task to finish...
Parent task has finished. Elapsed time is 1007 ms.
Performing more work on the main thread...
Elapsed time is 3015 ms.
Waiting for child task to finish...
Child task has finished. Elapsed time is 5015 ms.
*/

Dado que una tarea primaria no finaliza hasta que todo el final secundario de tareas, una tarea
secundaria de ejecución prolongada puede provocar la aplicación total para ejecutarse mal. En
este ejemplo, cuando la aplicación utiliza las opciones predeterminadas de crear la tarea
primaria, la tarea secundaria debe finalizar antes de que la tarea primaria finaliza. Cuando la
aplicación utiliza la opción de TaskCreationOptions.DenyChildAttach , no está asociado al
elemento secundario al elemento primario. Por consiguiente, la aplicación puede realizar el
trabajo adicional después de que la tarea primaria finaliza y antes de que debe esperar la tarea
secundaria finalice.
3.1.3. Biblioteca de procesamiento paralelo basado en tareas (TPL)
La biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas)
es un conjunto de API y tipos públicos de los espacios de nombres System.Threading.Tasks y
System.Threading de .NET Framework 4. El propósito de la biblioteca TPL es aumentar la
productividad de los desarrolladores al simplificar el proceso de agregar paralelismo y
simultaneidad a las aplicaciones. La biblioteca TPL escala el grado de simultaneidad de forma
dinámica para usar más eficazmente todos los procesadores que están disponibles. Además, la
TPL se encarga de la división del trabajo, la programación de los subprocesos en ThreadPool, la
compatibilidad con la cancelación, la administración de los estados y otros detalles de bajo
nivel. Al utilizar la TPL, el usuario puede optimizar el rendimiento del código mientras se centra
en el trabajo para el que el programa está diseñado.

MCT: Luis Dueñas Pag 216 de 336


Manual de .NET Framework 4.5

A partir de .NET Framework 4, la TPL es el modo preferido de escribir código paralelo y


multiproceso. Sin embargo, no todo el código se presta para la paralelización; por ejemplo, si un
bucle realiza solo una cantidad reducida de trabajo en cada iteración o no se ejecuta para un gran
número de iteraciones, la sobrecarga de la paralelización puede dar lugar a una ejecución más
lenta del código. Además, al igual que cualquier código multiproceso, la paralelización hace que
la ejecución del programa sea más compleja. Aunque la TPL simplifica los escenarios de
multithreading, recomendamos tener conocimientos básicos sobre conceptos de
subprocesamiento, por ejemplo, bloqueos, interbloqueos y condiciones de carrera, para usar la
TPL eficazmente.
3.1.3.1. Cómo: Escribir y leer mensajes en un bloque de flujo de datos
En este documento se describe cómo utilizar la biblioteca de TPL Dataflow para escribir y leer
los mensajes de un bloque de flujo de datos. La biblioteca de TPL Dataflow proporciona
sincrónico y métodos asincrónicos para escribir y leer los mensajes de un bloque de flujo de
datos. Este documento utiliza la clase de System.Threading.Tasks.Dataflow.BufferBlock<T> .
La clase de BufferBlock<T> almacena en búfer los mensajes y se comporta como origen del
mensaje como destino del mensaje.
Sugerencia
La biblioteca de TPL Dataflow (espacio de nombres de System.Threading.Tasks.Dataflow ) no
se distribuye con .NET Framework 4.5. Para instalar el espacio de nombres
System.Threading.Tasks. Dataflow , abra el proyecto en Visual Studio 2012, elegir ***
Administrar los paquetes de NuGet *** del menú proyecto, y buscarlo en línea para el paquete
de Microsoft.Tpl.Dataflow .
Escriben y la lectura de un bloque Synchronously de flujo de datos
El ejemplo siguiente se usa el método de Post<TInput> para escribir en un bloque de flujo de
datos de BufferBlock<T> y el método de Receive para leer el mismo objeto.
// Create a BufferBlock<int> object.
var bufferBlock = new BufferBlock<int>();
// Post several messages to the block.
for (int i = 0; i < 3; i++)
{
bufferBlock.Post(i);
}
// Receive the messages back from the block.
for (int i = 0; i < 3; i++)
{
Console.WriteLine(bufferBlock.Receive());
}
/* Output:
0
1
2
*/

También puede utilizar el método de TryReceive para leer un bloque de flujo de datos, como se
muestra en el ejemplo siguiente. El método de TryReceive no bloquea el subproceso actual y es
útil cuando se sondea en ocasiones para los datos.
// Post more messages to the block.
for (int i = 0; i < 3; i++)
{
bufferBlock.Post(i);
}
// Receive the messages back from the block.
int value;
while (bufferBlock.TryReceive(out value))
{
Console.WriteLine(value);

MCT: Luis Dueñas Pag 217 de 336


Manual de .NET Framework 4.5

}
/* Output:
0
1
2
*/

Porque actúa el método de Post<TInput> sincrónica, el objeto de BufferBlock<T> en los


ejemplos anteriores recibe todos los datos antes de que el segundo bucle leer datos. El ejemplo
siguiente extiende el primer ejemplo mediante Invoke de lectura y escritura al bloque de
mensajes simultáneamente. Dado que Invoke realiza acciones en paralelo, los valores no se
escriben en el objeto de BufferBlock<T> en ningún orden determinado.
// Write to and read from the message block concurrently.
var post01 = Task.Run(() =>
{
bufferBlock.Post(0);
bufferBlock.Post(1);
});
var receive = Task.Run(() =>
{
for (int i = 0; i < 3; i++)
{
Console.WriteLine(bufferBlock.Receive());
}
});
var post2 = Task.Run(() =>
{
bufferBlock.Post(2);
});
Task.WaitAll(post01, receive, post2);
/* Sample output:
2
0
1
*/

Escriben y la lectura de un bloque Asincrónicamente de flujo de datos


El ejemplo siguiente se utiliza el método de SendAsync asincrónica para escribir en un objeto de
BufferBlock<T> y el método de ReceiveAsync asincrónica para leer el mismo objeto. Este
ejemplo utiliza operadores de async y de espera (Asincronía y Espera en Visual Basic) de forma
asincrónica para enviar y para leer datos de bloque de destino. El método de SendAsync es útil
cuando se debe permitir que un bloque de flujo de datos para aplazar mensajes. El método de
ReceiveAsync es útil cuando desea representar en datos cuando esos datos está disponible.
// Post more messages to the block asynchronously.
for (int i = 0; i < 3; i++)
{
await bufferBlock.SendAsync(i);
}
// Asynchronously receive the messages back from the block.
for (int i = 0; i < 3; i++)
{
Console.WriteLine(await bufferBlock.ReceiveAsync());
}
/* Output:
0
1
2
*/

Un ejemplo completo
El ejemplo siguiente se muestra el código completo de este documento.
using System;
using System.Threading.Tasks;

MCT: Luis Dueñas Pag 218 de 336


Manual de .NET Framework 4.5

using System.Threading.Tasks.Dataflow;

// Demonstrates a how to write to and read from a dataflow block.


class DataflowReadWrite
{
// Demonstrates asynchronous dataflow operations.
static async Task AsyncSendReceive(BufferBlock<int> bufferBlock)
{
// Post more messages to the block asynchronously.
for (int i = 0; i < 3; i++)
{
await bufferBlock.SendAsync(i);
}
// Asynchronously receive the messages back from the block.
for (int i = 0; i < 3; i++)
{
Console.WriteLine(await bufferBlock.ReceiveAsync());
}
/* Output:
0
1
2
*/
}

static void Main(string[] args)


{
// Create a BufferBlock<int> object.
var bufferBlock = new BufferBlock<int>();
// Post several messages to the block.
for (int i = 0; i < 3; i++)
{
bufferBlock.Post(i);
}
// Receive the messages back from the block.
for (int i = 0; i < 3; i++)
{
Console.WriteLine(bufferBlock.Receive());
}
/* Output:
0
1
2
*/
// Post more messages to the block.
for (int i = 0; i < 3; i++)
{
bufferBlock.Post(i);
}
// Receive the messages back from the block.
int value;
while (bufferBlock.TryReceive(out value))
{
Console.WriteLine(value);
}
/* Output:
0
1
2
*/
// Write to and read from the message block concurrently.
var post01 = Task.Run(() =>
{
bufferBlock.Post(0);
bufferBlock.Post(1);
});
var receive = Task.Run(() =>
{

MCT: Luis Dueñas Pag 219 de 336


Manual de .NET Framework 4.5

for (int i = 0; i < 3; i++)


{
Console.WriteLine(bufferBlock.Receive());
}
});
var post2 = Task.Run(() =>
{
bufferBlock.Post(2);
});
Task.WaitAll(post01, receive, post2);
/* Sample output:
2
0
1
*/
// Demonstrate asynchronous dataflow operations.
AsyncSendReceive(bufferBlock).Wait();
}
}

3.1.3.2. Cómo: Implementar un modelo de flujo de datos productor-


consumidor
En este documento se describe cómo utilizar la biblioteca de TPL Dataflow para implementar
un modelo productor-consumidor. En este modelo, el productor envía mensajes a un bloque de
mensajes y el consumidor lee los mensajes de este bloque.
Ejemplo
El ejemplo siguiente muestra un modelo básico de consumidor-productor que utilice flujo de
datos. El método de Produce escribe las matrices que contienen bytes aleatorios de datos a un
objeto de System.Threading.Tasks.Dataflow.ITargetBlock<TInput> y los bytes del método lee
de Consume de System.Threading.Tasks.Dataflow.ISourceBlock<TOutput> se oponen.
Actuando en las interfaces de ISourceBlock<TOutput> y de ITargetBlock<TInput> , en lugar de
sus tipos derivados, puede escribir código reutilizable que puede representar en una variedad de
tipos de bloques de flujo de datos. Este ejemplo utiliza la clase de BufferBlock<T> . Dado que
actúa la clase de BufferBlock<T> mientras un origen y como bloque de destino, el productor y
el consumidor pueden utilizar un objeto compartido a los datos de transferencia.
Las llamadas al método de Produce el método de Post<TInput> en un bucle para escribir
sincrónicamente datos al bloque de destino. Después de que el método de Produce escriba todos
los datos al bloque de destino, llama al método de Complete para indicar que el bloque nunca
tendrá datos adicionales disponibles. El método de Consume usan operadores de async y de
espera (Asincronía y Espera en Visual Basic) de forma asincrónica para calcular el número total
de bytes que se reciben del objeto de ISourceBlock<TOutput> . Para representar de forma
asincrónica, las llamadas al método de Consume el método de OutputAvailableAsync para
recibir una notificación cuando el bloque de origen tiene datos disponibles y cuando el bloque
de origen nunca tendrá datos adicionales disponibles.
using System;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;

// Demonstrates a basic producer and consumer pattern that uses dataflow.


class DataflowProducerConsumer
{
// Demonstrates the production end of the producer and consumer pattern.
static void Produce(ITargetBlock<byte[]> target)
{
// Create a Random object to generate random data.
Random rand = new Random();
// In a loop, fill a buffer with random data and
// post the buffer to the target block.
for (int i = 0; i < 100; i++)

MCT: Luis Dueñas Pag 220 de 336


Manual de .NET Framework 4.5

{
// Create an array to hold random byte data.
byte[] buffer = new byte[1024];
// Fill the buffer with random bytes.
rand.NextBytes(buffer);
// Post the result to the message block.
target.Post(buffer);
}
// Set the target to the completed state to signal to the consumer
// that no more data will be available.
target.Complete();
}

// Demonstrates the consumption end of the producer and consumer pattern.


static async Task<int> ConsumeAsync(ISourceBlock<byte[]> source)
{
// Initialize a counter to track the number of bytes that are processed.
int bytesProcessed = 0;
// Read from the source buffer until the source buffer has no
// available output data.
while (await source.OutputAvailableAsync())
{
byte[] data = source.Receive();
// Increment the count of bytes received.
bytesProcessed += data.Length;
}
return bytesProcessed;
}

static void Main(string[] args)


{
// Create a BufferBlock<byte[]> object. This object serves as the
// target block for the producer and the source block for the consumer.
var buffer = new BufferBlock<byte[]>();
// Start the consumer. The Consume method runs asynchronously.
var consumer = ConsumeAsync(buffer);
// Post source data to the dataflow block.
Produce(buffer);
// Wait for the consumer to process all data.
consumer.Wait();
// Print the count of bytes processed to the console.
Console.WriteLine("Processed {0} bytes.", consumer.Result);
}
}
/* Output:
Processed 102400 bytes.
*/

3.1.3.3. Cómo: Realizar una acción cuando un bloque de flujos de datos


recibe datos
Los tiposde bloques de flujo de datos de ejecución de llama usuario- proporcionó el delegado
cuando reciben datos. System.Threading.Tasks.Dataflow.ActionBlock<TInput> ,
System.Threading.Tasks. Dataflow.TransformBlock<TInput, TOutput>, y las clases de
System.Threading.Tasks.Dataflow. TransformManyBlock<TInput, TOutput> son tipos de
bloques de flujo de datos de la ejecución. Puede utilizar la palabra clave de delegate (Sub en
Visual Basic), Action<T>, Func<T, TResult>, o una expresión lambda cuando se proporciona
una función de trabajo a un bloque de flujo de datos de la ejecución. Este documento se describe
cómo usar Func<T, TResult> y expresiones lambda para realizar una acción en bloques de
ejecución.
Ejemplo
El ejemplo siguiente utiliza flujo de datos para leer un archivo desde el disco y calcula el
número de bytes de ese archivo que sean iguales a cero. Utiliza TransformBlock<TInput,

MCT: Luis Dueñas Pag 221 de 336


Manual de .NET Framework 4.5

TOutput> para leer el archivo y para calcular el número de bytes cero, y ActionBlock<TInput>
para imprimir el número de bytes cero en la consola. El objeto de TransformBlock<TInput,
TOutput> especifica un objeto de Func<T, TResult> para realizar el trabajo cuando los bloques
reciben datos. El objeto de ActionBlock<TInput> usa una expresión lambda para imprimir en la
consola el número de bytes cero se lean que.
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;

// Demonstrates how to provide delegates to exectution dataflow blocks.


class DataflowExecutionBlocks
{
// Computes the number of zero bytes that the provided file
// contains.
static int CountBytes(string path)
{
byte[] buffer = new byte[1024];
int totalZeroBytesRead = 0;
using (var fileStream = File.OpenRead(path))
{
int bytesRead = 0;
do
{
bytesRead = fileStream.Read(buffer, 0, buffer.Length);
totalZeroBytesRead += buffer.Count(b => b == 0);
} while (bytesRead > 0);
}
return totalZeroBytesRead;
}

static void Main(string[] args)


{
// Create a temporary file on disk.
string tempFile = Path.GetTempFileName();
// Write random data to the temporary file.
using (var fileStream = File.OpenWrite(tempFile))
{
Random rand = new Random();
byte[] buffer = new byte[1024];
for (int i = 0; i < 512; i++)
{
rand.NextBytes(buffer);
fileStream.Write(buffer, 0, buffer.Length);
}
}
// Create an ActionBlock<int> object that prints to the console
// the number of bytes read.
var printResult = new ActionBlock<int>(zeroBytesRead =>
{
Console.WriteLine("{0} contains {1} zero bytes.",
Path.GetFileName(tempFile), zeroBytesRead);
});

// Create a TransformBlock<string, int> object that calls the


// CountBytes function and returns its result.
var countBytes = new TransformBlock<string, int>(
new Func<string, int>(CountBytes));
// Link the TransformBlock<string, int> object to the
// ActionBlock<int> object.
countBytes.LinkTo(printResult);
// Create a continuation task that completes the ActionBlock<int>
// object when the TransformBlock<string, int> finishes.
countBytes.Completion.ContinueWith(delegate { printResult.Complete();
});

MCT: Luis Dueñas Pag 222 de 336


Manual de .NET Framework 4.5

// Post the path to the temporary file to the


// TransformBlock<string, int> object.
countBytes.Post(tempFile);
// Requests completion of the TransformBlock<string, int> object.
countBytes.Complete();
// Wait for the ActionBlock<int> object to print the message.
printResult.Completion.Wait();
// Delete the temporary file.
File.Delete(tempFile);
}
}
/* Sample output:
tmp4FBE.tmp contains 2081 zero bytes.
*/

Aunque puede proporcionar una expresión lambda a un objeto de TransformBlock<TInput,


TOutput> , las aplicaciones Func<T, TResult> de este ejemplo de permitir a otro código para
utilizar el método de CountBytes . El objeto de ActionBlock<TInput> usa una expresión
lambda porque el trabajo que se realizará es específico de esta tarea y no es probable que sea útil
de otro código.
El resumen de la sección tipos de delegado en el documento de Flujo de datos (biblioteca TPL)
se resumen los tipos de delegado que puede proporcionar a ActionBlock<TInput>, a
TransformBlock <TInput, TOutput>, y los objetos de TransformManyBlock<TInput, TOutput>.
La tabla también especifica si el tipo de delegado funciona de forma sincrónica o asincrónica.
Compilar el código
Copie el código de ejemplo y péguelo en un proyecto de Visual Studio, o péguelo en un archivo
denominado DataflowExecutionBlocks.cs (DataflowExecutionBlocks.vb para Visual Basic), y
ejecutar el siguiente comando en una ventana de símbolo del sistema de Visual Studio.
Visual C#
csc.exe /r:System.Threading.Tasks.Dataflow.dll DataflowExecutionBlocks.cs
Visual Basic
vbc.exe /r:System.Threading.Tasks.Dataflow.dll DataflowExecutionBlocks.vb
Programación eficaz
Este ejemplo proporciona un delegado de Func<T, TResult> escrito en el objeto de
TransformBlock <TInput, TOutput> para realizar la tarea de flujo de datos bloqueado
sincrónicamente. Para permitir al bloque de flujo de datos para comportarse de forma
asincrónica, proporcione un delegado de Func<TResult> escrito en el bloque de flujo de datos.
Cuando un bloque de flujo de datos se comporta de forma asincrónica, la tarea del bloque de
flujo de datos se completa cuando los finals devueltos de los objetos de Task<TResult> . El
ejemplo siguiente se modifica el método de CountBytes y utiliza async y operadores de espera
(Asincronía y Espera en Visual Basic) de forma asincrónica para calcular el número total de
bytes que se colocan en el archivo proporcionado. El método de ReadAsync realiza operaciones
de archivo leído de forma asincrónica.
// Asynchronously computes the number of zero bytes that the provided file
contains.
static async Task<int> CountBytesAsync(string path)
{
byte[] buffer = new byte[1024];
int totalZeroBytesRead = 0;
using (var fileStream = new FileStream(
path, FileMode.Open, FileAccess.Read, FileShare.Read, 0x1000, true))
{
int bytesRead = 0;
do
{

MCT: Luis Dueñas Pag 223 de 336


Manual de .NET Framework 4.5

// Asynchronously read from the file stream.


bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length);
totalZeroBytesRead += buffer.Count(b => b == 0);
} while (bytesRead > 0);
}
return totalZeroBytesRead;
}

También puede utilizar expresiones asincrónicas lambda para realizar una acción en un bloque
de flujo de datos de la ejecución. El ejemplo siguiente se modifica el objeto de
TransformBlock<TInput, TOutput> que se utiliza en el ejemplo anterior para que use una
expresión lambda para realizar el trabajo de forma asincrónica.
// Create a TransformBlock<string, int> object that calls the
// CountBytes function and returns its result.
var countBytesAsync = new TransformBlock<string, int>(async path =>
{
byte[] buffer = new byte[1024];
int totalZeroBytesRead = 0;
using (var fileStream = new FileStream(
path, FileMode.Open, FileAccess.Read, FileShare.Read, 0x1000, true))
{
int bytesRead = 0;
do
{
// Asynchronously read from the file stream.
bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length);
totalZeroBytesRead += buffer.Count(b => b == 0);
} while (bytesRead > 0);
}
return totalZeroBytesRead;
});

3.1.3.4. Tutorial: Crear una canalización de flujos de datos


Aunque puede utilizar DataflowBlock.Receive, DataflowBlock.ReceiveAsync, y métodos de
DataflowBlock.TryReceive<TOutput> para recibir los mensajes de los bloques de origen,
también puede conectar los bloques de mensajes para formar una canalización de flujo de datos.
Una canalización de flujo de datos es una serie de componentes, o bloques de flujo de datos, que
realiza una tarea concreta que contribuye a lograr un objetivo mayor. Cada bloque de flujo de
datos en una canalización de flujo de datos realiza trabajo cuando recibe un mensaje de otro
bloque de flujo de datos. Una analogía a esto es una línea de montaje para la fabricación de
automóviles. Como pasos de cada vehículo a través de la línea de montaje, una estación
ensambla el cuadro, el siguiente instala el motor, etc. Dado que una línea de montaje habilita los
vehículos para que se ensamblarán al mismo tiempo, proporciona un mejor rendimiento que los
vehículos completos de uno en uno.
En este documento se muestra una canalización de flujo de datos que descargue el libro La
Ilíada de homero de un sitio Web y crea palíndromos de las palabras que aparecen en el libro.
La formación de canalización del flujo de datos en este documento consta de los pasos
siguientes:
1. Cree bloques de flujo de datos que participan en la canalización.
2. Conectar cada bloque de flujo de datos al siguiente bloque de la canalización. Cada
bloque recibe como entrada el resultado del bloque anterior en la canalización.
3. Para cada bloque de flujo de datos, cree una tarea de continuación que establezca el
siguiente bloque al estado completado después de que el bloque anterior finaliza.
4. Enviar datos al encabezado de la canalización.
5. Marque el encabezado de la canalización como completo.
6. Espere la canalización para completar todo el trabajo.
Crear una aplicación de consola

MCT: Luis Dueñas Pag 224 de 336


Manual de .NET Framework 4.5

En Visual Studio, cree un proyecto de aplicación de consola de Visual C# o de Visual Basic .


Agregue una referencia a System.Threading.Tasks.Dataflow.dll.
Como alternativa, cree un archivo y denomínelo DataflowPalindromes.cs
(DataflowPalindromes.vb para Visual Basic), y ejecute el siguiente comando en una ventana de
símbolo del sistema de Visual Studio para compilar el proyecto.
Visual C#
csc.exe /r:System.Threading.Tasks.Dataflow.dll DataflowPalindromes.cs
Visual Basic
vbc.exe /r:System.Threading.Tasks.Dataflow.dll DataflowPalindromes.vb
Agregue el código siguiente al proyecto para crear la aplicación básica.
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;

// Demonstrates how to create a basic dataflow pipeline.


// This program downloads the book "The Iliad of Homer" by Homer from the Web
// and finds all palindromes that appear in that book.
class Program
{
static void Main(string[] args)
{
}
}

Crear los bloques de flujo de datos


Agregue el código siguiente al método de Main para crear los bloques de flujo de datos que
participan en la canalización. La tabla siguiente se resume el rol de cada miembro de la
canalización.
//
// Create the members of the pipeline.
//
// Downloads the requested resource as a string.
var downloadString = new TransformBlock<string, string>(uri =>
{
Console.WriteLine("Downloading '{0}'...", uri);
return new WebClient().DownloadString(uri);
});
// Separates the specified text into an array of words.
var createWordList = new TransformBlock<string, string[]>(text =>
{
Console.WriteLine("Creating word list...");
// Remove common punctuation by replacing all non-letter characters
// with a space character to.
char[] tokens = text.ToArray();
for(int i=0; i<tokens.Length; i++)
{
if (!char.IsLetter(tokens[i]))
tokens[i] = ' ';
}
text = new string(tokens);
// Separate the text into an array of words.
return text.Split(new char[] { ' ' },
StringSplitOptions.RemoveEmptyEntries);
});
// Removes short words, orders the resulting words alphabetically,
// and then remove duplicates.
var filterWordList = new TransformBlock<string[], string[]>(words =>

MCT: Luis Dueñas Pag 225 de 336


Manual de .NET Framework 4.5

{
Console.WriteLine("Filtering word list...");
return words.Where(word => word.Length > 3).OrderBy(word => word)
.Distinct().ToArray();
});
// Finds all words in the specified collection whose reverse also
// exists in the collection.
var findPalindromes = new TransformManyBlock<string[], string>(words =>
{
Console.WriteLine("Finding palindromes...");
// Holds palindromes.
var palindromes = new ConcurrentQueue<string>();
// Add each word in the original collection to the result whose
// palindrome also exists in the collection.
Parallel.ForEach(words, word =>
{
// Reverse the work.
string reverse = new string(word.Reverse().ToArray());
// Enqueue the word if the reversed version also exists
// in the collection.
if (Array.BinarySearch<string>(words, reverse) >= 0 &&
word != reverse)
{
palindromes.Enqueue(word);
}
});
return palindromes;
});
// Prints the provided palindrome to the console.
var printPalindrome = new ActionBlock<string>(palindrome =>
{
Console.WriteLine("Found palindrome {0}/{1}",
palindrome, new string(palindrome.Reverse().ToArray()));
});
Miembro Tipo Descripción
TransformBlock<TInput,
downloadString Descarga el texto del libro de web.
TOutput>
TransformBlock<TInput, Separa el texto del libro en una matriz de
createWordList
TOutput> palabras.
Quita palabras cortas de matriz de word,
TransformBlock<TInput,
filterWordList ordena las palabras resultantes
TOutput>
alfabéticamente, y quita los duplicados.
Busca todas las palabras en la colección
TransformManyBlock<TInput, filtrada de matriz de la palabra cuyo
findPalindromes
TOutput> reverse también aparece en la matriz de
word.
printPalindrome ActionBlock<TInput> Imprime palíndromos en la consola.
Aunque puede combinar varios pasos en la canalización de flujo de datos en este ejemplo en un
paso, el ejemplo muestra el concepto de crear tareas independientes varias de flujo de datos para
realizar una tarea mayor. El ejemplo utiliza TransformBlock<TInput, TOutput> para permitir
que cada miembro de la canalización para realizar una operación en los datos de entrada y
enviar los resultados al paso siguiente en la canalización. El miembro de findPalindromes de la
canalización es un objeto de TransformManyBlock<TInput, TOutput> porque genera los
resultados independientes varios para cada entrada. La cola de la canalización,
printPalindrome, es un objeto de ActionBlock<TInput> porque realiza una acción en la
entrada, y no genera un resultado.
Formación de canalización
Agregue el código siguiente para conectar cada bloque a bloque siguiente en la canalización.

MCT: Luis Dueñas Pag 226 de 336


Manual de .NET Framework 4.5

Cuando se llama al método de LinkTo para conectar un bloque de flujo de datos de origen a un
bloque de flujo de datos de destino, los datos de las propagaciones de bloques de flujo de datos
de origen al destino bloqueado como datos está disponible.
//
// Connect the dataflow blocks to form a pipeline.
//
downloadString.LinkTo(createWordList);
createWordList.LinkTo(filterWordList);
filterWordList.LinkTo(findPalindromes);
findPalindromes.LinkTo(printPalindrome);

Crear tareas de Finalización


Agregue el código siguiente para permitir que cada bloque de flujo de datos para realizar una
acción final después de procesar todos los datos.
// For each completion task in the pipeline, create a continuation task
// that marks the next block in the pipeline as completed.
// A completed dataflow block processes any buffered elements, but does
// not accept new elements.
downloadString.Completion.ContinueWith(t =>
{
if (t.IsFaulted) ((IDataflowBlock)createWordList).Fault(t.Exception);
else createWordList.Complete();
});
createWordList.Completion.ContinueWith(t =>
{
if (t.IsFaulted) ((IDataflowBlock)filterWordList).Fault(t.Exception);
else filterWordList.Complete();
});
filterWordList.Completion.ContinueWith(t =>
{
if (t.IsFaulted) ((IDataflowBlock)findPalindromes).Fault(t.Exception);
else findPalindromes.Complete();
});
findPalindromes.Completion.ContinueWith(t =>
{
if (t.IsFaulted) ((IDataflowBlock)printPalindrome).Fault(t.Exception);
else printPalindrome.Complete();
});

Para propagar la finalización a través de la canalización, cada tarea de finalización establece el


siguiente bloque de flujo de datos al estado completado. Por ejemplo, cuando el administrador
de la canalización se establece en el estado completado, procesa cualquier mensaje almacenado
en búfer restante y después ejecute la tarea de finalización, que establece en el segundo
miembro de canalización al estado completado. Que queda el segundo miembro de canalización
a su vez procesa cualquier mensaje almacenado en búfer y después ejecute la tarea de
finalización, que establece el tercer miembro de canalización al estado completado. Este proceso
continúa hasta que todos los miembros del final de la canalización. Este ejemplo utiliza la
palabra clave de delegate (Function en Visual Basic) para definir las tareas de continuación.
Datos de asociación a la canalización
Agregue el código siguiente para enviar la dirección url del libro La Ilíada de homero al
encabezado de la canalización de flujo de datos.
// Process "The Iliad of Homer" by Homer.
downloadString.Post("http://www.gutenberg.org/files/6130/6130-0.txt");

Este ejemplo utiliza DataflowBlock.Post<TInput> para enviar sincrónicamente datos al


encabezado de la canalización. Utilice el método de DataflowBlock.SendAsync cuando debe
asincrónica enviar datos a un nodo de flujo de datos.
Completar actividad de canalización

MCT: Luis Dueñas Pag 227 de 336


Manual de .NET Framework 4.5

Agregue el código siguiente para marcar el principio de la canalización como completo. El


encabezado de la canalización ejecutar la tarea de continuación después de procesar todos los
mensajes almacenados en búfer. Esta tarea de continuación se propaga al estado completado a
través de la canalización.
// Mark the head of the pipeline as complete. The continuation tasks
// propagate completion through the pipeline as each part of the
// pipeline finishes.
downloadString.Complete();

Este ejemplo envía una dirección URL a través de la canalización de flujo de datos que se
procese. Si envía más de uno escrito a través de una canalización, llame al método de
IDataflowBlock.Complete después de enviar todas las entradas. Puede omitir este paso si la
aplicación no tiene ningún punto bien definido en el que los datos ya no están disponibles o la
aplicación no tiene que esperar la canalización finalice.
Esperar la canalización para finalizar
Agregue el código siguiente para esperar la canalización finalice. Dado que este ejemplo utiliza
tareas de continuación para propagar la finalización a través de la canalización, finaliza la
operación global cuando la cola de los finals de canalización.
// Wait for the last block in the pipeline to process all messages.
printPalindrome.Completion.Wait();

Puede esperar termina el flujo de datos de cualquier subproceso o de varios subprocesos al


mismo tiempo.
Ejemplo completo
El ejemplo siguiente se muestra el código completo de este tutorial.
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;

// Demonstrates how to create a basic dataflow pipeline.


// This program downloads the book "The Iliad of Homer" by Homer from the Web
// and finds all palindromes that appear in that book.
class DataflowPalindromes
{
static void Main(string[] args)
{
// Create the members of the pipeline.
// Downloads the requested resource as a string.
var downloadString = new TransformBlock<string, string>(uri =>
{
Console.WriteLine("Downloading '{0}'...", uri);
return new WebClient().DownloadString(uri);
});

// Separates the specified text into an array of words.


var createWordList = new TransformBlock<string, string[]>(text =>
{
Console.WriteLine("Creating word list...");
// Remove common punctuation by replacing all non-letter characters
// with a space character to.
char[] tokens = text.ToArray();
for(int i=0; i<tokens.Length; i++)
{
if (!char.IsLetter(tokens[i])) tokens[i] = ' ';
}
text = new string(tokens);
// Separate the text into an array of words.

MCT: Luis Dueñas Pag 228 de 336


Manual de .NET Framework 4.5

return text.Split(new char[] { ' ' },


StringSplitOptions.RemoveEmptyEntries);
});
// Removes short words, orders the resulting words alphabetically,
// and then remove duplicates.
var filterWordList = new TransformBlock<string[], string[]>(words =>
{
Console.WriteLine("Filtering word list...");
return words.Where(word => word.Length > 3).OrderBy(word => word)
.Distinct().ToArray();
});
// Finds all words in the specified collection whose reverse also
// exists in the collection.
var findPalindromes = new TransformManyBlock<string[], string>(words =>
{
Console.WriteLine("Finding palindromes...");
// Holds palindromes.
var palindromes = new ConcurrentQueue<string>();
// Add each word in the original collection to the result whose
// palindrome also exists in the collection.
Parallel.ForEach(words, word =>
{
// Reverse the work.
string reverse = new string(word.Reverse().ToArray());
// Enqueue the word if the reversed version also exists
// in the collection.
if (Array.BinarySearch<string>(words, reverse) >= 0 &&
word != reverse)
{
palindromes.Enqueue(word);
}
});
return palindromes;
});
// Prints the provided palindrome to the console.
var printPalindrome = new ActionBlock<string>(palindrome =>
{
Console.WriteLine("Found palindrome {0}/{1}",
palindrome, new string(palindrome.Reverse().ToArray()));
});
// Connect the dataflow blocks to form a pipeline.
downloadString.LinkTo(createWordList);
createWordList.LinkTo(filterWordList);
filterWordList.LinkTo(findPalindromes);
findPalindromes.LinkTo(printPalindrome);
// For each completion task in the pipeline, create a continuation task
// that marks the next block in the pipeline as completed.
// A completed dataflow block processes any buffered elements, but does
// not accept new elements.
downloadString.Completion.ContinueWith(t =>
{
if (t.IsFaulted) ((IDataflowBlock)createWordList).Fault(t.Exception);
else createWordList.Complete();
});
createWordList.Completion.ContinueWith(t =>
{
if (t.IsFaulted) ((IDataflowBlock)filterWordList).Fault(t.Exception);
else filterWordList.Complete();
});
filterWordList.Completion.ContinueWith(t =>
{
if (t.IsFaulted)
((IDataflowBlock)findPalindromes).Fault(t.Exception);
else findPalindromes.Complete();
});
findPalindromes.Completion.ContinueWith(t =>
{

MCT: Luis Dueñas Pag 229 de 336


Manual de .NET Framework 4.5

if (t.IsFaulted)
((IDataflowBlock)printPalindrome).Fault(t.Exception);
else printPalindrome.Complete();
});
// Process "The Iliad of Homer" by Homer.
downloadString.Post("http://www.gutenberg.org/files/6130/6130-0.txt");
// Mark the head of the pipeline as complete. The continuation tasks
// propagate completion through the pipeline as each part of the
// pipeline finishes.
downloadString.Complete();
// Wait for the last block in the pipeline to process all messages.
printPalindrome.Completion.Wait();
}
}
/* Sample output:
Downloading 'http://www.gutenberg.org/files/6130/6130-0.txt'...
Creating word list...
Filtering word list...
Finding palindromes...
Found palindrome doom/mood
Found palindrome draw/ward
Found palindrome live/evil
Found palindrome seat/taes
Found palindrome aera/area
Found palindrome mood/doom
Found palindrome moor/room
Found palindrome sleek/keels
Found palindrome area/aera
Found palindrome evil/live
Found palindrome speed/deeps
Found palindrome spot/tops
Found palindrome spots/stops
Found palindrome stops/spots
Found palindrome taes/seat
Found palindrome port/trop
Found palindrome tops/spot
Found palindrome trop/port
Found palindrome reed/deer
Found palindrome deeps/speed
Found palindrome deer/reed
Found palindrome ward/draw
Found palindrome room/moor
Found palindrome keels/sleek
*/

3.1.3.5. Cómo: Desvincular bloques de flujos de datos


Este documento describe cómo desenlazar un flujo de datos de destino del origen.
Ejemplo
El ejemplo siguiente se crean tres objetos de TransformBlock<TInput, TOutput> , que llama al
método de TrySolution para calcular un valor. Este ejemplo requiere sólo el resultado de la
primera llamada a TrySolution finalice.
using System;
using System.Threading;
using System.Threading.Tasks.Dataflow;

// Demonstrates how to unlink dataflow blocks.


class DataflowReceiveAny
{
// Receives the value from the first provided source that has a message.
public static T ReceiveFromAny<T>(params ISourceBlock<T>[] sources)
{
// Create a WriteOnceBlock<T> object and link it to each source block.
var writeOnceBlock = new WriteOnceBlock<T>(e => e);
foreach (var source in sources)

MCT: Luis Dueñas Pag 230 de 336


Manual de .NET Framework 4.5

{
// Setting MaxMessages to one instructs
// the source block to unlink from the WriteOnceBlock<T> object
// after offering the WriteOnceBlock<T> object one message.
source.LinkTo(writeOnceBlock, new DataflowLinkOptions { MaxMessages =
1 });
}
// Return the first value that is offered to the WriteOnceBlock object.
return writeOnceBlock.Receive();
}

// Demonstrates a function that takes several seconds to produce a result.


static int TrySolution(int n, CancellationToken ct)
{
// Simulate a lengthy operation that completes within three seconds
// or when the provided CancellationToken object is cancelled.
SpinWait.SpinUntil(() => ct.IsCancellationRequested, new
Random().Next(3000));
// Return a value.
return n + 42;
}

static void Main(string[] args)


{
// Create a shared CancellationTokenSource object to enable the
// TrySolution method to be cancelled.
var cts = new CancellationTokenSource();
// Create three TransformBlock<int, int> objects.
// Each TransformBlock<int, int> object calls the TrySolution method.
Func<int, int> action = n => TrySolution(n, cts.Token);
var trySolution1 = new TransformBlock<int, int>(action);
var trySolution2 = new TransformBlock<int, int>(action);
var trySolution3 = new TransformBlock<int, int>(action);
// Post data to each TransformBlock<int, int> object.
trySolution1.Post(11);
trySolution2.Post(21);
trySolution3.Post(31);
// Call the ReceiveFromAny<T> method to receive the result from the
// first TransformBlock<int, int> object to finish.
int result = ReceiveFromAny(trySolution1, trySolution2, trySolution3);
// Cancel all calls to TrySolution that are still active.
cts.Cancel();
// Print the result to the console.
Console.WriteLine("The solution is {0}.", result);
}
}
/* Sample output:
The solution is 53.
*/

Para recibir el valor de primer TransformBlock<TInput, TOutput> opóngase que los finals, este
ejemplo definen el método de ReceiveFromAny(T) . El método de ReceiveFromAny(T)
acepta una matriz de los objetos de ISourceBlock<TOutput> y vínculos cada uno de estos
objetos a un objeto de WriteOnceBlock<T> . Cuando se utiliza el método del LinkTo para
vincular un bloque de flujo de datos de origen a un bloque de destino, el origen propaga
mensajes al destino mientras los datos disponible. Dado que la clase de WriteOnceBlock<T>
solo acepta el primer mensaje que proporciona, el método de ReceiveFromAny(T) genera el
resultado llamando al método de Receive . Esto muestra el primer mensaje que se proporciona
al objeto de WriteOnceBlock<T>. El método de LinkTo tiene una versión sobrecargada que
toma un parámetro de Boolean , unlinkAfterOne que, cuando se establece en True, pida al
origen para bloquear desenlazar de destino después del destino recibe un mensaje de origen. Es
importante que el objeto de WriteOnceBlock<T> desvincular de sus orígenes porque la relación
entre la matriz de orígenes y el objeto de WriteOnceBlock<T> ya no se requiere después de que
el objeto de WriteOnceBlock<T> recibe un mensaje.

MCT: Luis Dueñas Pag 231 de 336


Manual de .NET Framework 4.5

Para que las llamadas restantes a TrySolution para finalizar después de que una de ellas calcula
un valor, el método de TrySolution toma un objeto de CancellationToken que se cancele
después de la llamada a ReceiveFromAny(T) vuelva. El método de SpinUntil devuelve cuando
este objeto de CancellationToken se cancela.
3.1.3.6. Tutorial: Usar flujos de datos en aplicaciones de Windows
Forms
Este documento se muestra cómo crear una red de bloques de flujo de datos que realizan el
procesamiento de imágenes en una aplicación de Windows Forms.
Este ejemplo carga los archivos de imagen de la carpeta especificada, crea una imagen
compuesta, y muestra el resultado. El ejemplo utiliza el modelo de flujo de datos para enrutar
imágenes a través de la red. En el modelo de flujo de datos, los componentes independientes de
un programa se comunican entre sí enviando mensajes. Cuando un componente recibe un
mensaje, realiza alguna acción y pasa el resultado a otro componente. Compare esto con el
modelo de flujo de control, en el que una aplicación usa estructuras de control, como
instrucciones condicionales, bucles, etc., para controlar el orden de las operaciones en un
programa.
Crear la aplicación de formularios Windows Forms
En esta sección se describe cómo crear una aplicación básica de Windows Forms y agregar
controles al formulario principal.
Para crear la aplicación de Windows Forms
1. En Visual Studio, cree Visual C# o un proyecto de Visual Basic Aplicación de
Windows Forms . En este documento, el proyecto se denomina CompositeImages.
2. En el diseñador de formularios para el formulario principal, Form1.cs (Form1.vb para
Visual Basic), agregue un control de ToolStrip .
3. Agregue un control de ToolStripButton al control de ToolStrip . Establezca la propiedad
de DisplayStyle a Text y la propiedad de Text para elegir la carpeta.
4. Agregue un control de ToolStripButton de segundo al control de ToolStrip . Establezca
la propiedad de DisplayStyle a Text, la propiedad de Text para cancelar, y la propiedad
de Enabled a False.
5. Agregue un objeto de PictureBox al formulario principal. Establezca la propiedad Dock
en Fill.
Crear la red de flujo de datos
En esta sección se describe cómo crear la red de flujo de datos que realiza el procesamiento de
imágenes.
Para crear la red de flujo de datos
1. Agregue una referencia a System.Threading.Tasks.Dataflow.dll al proyecto.
2. Asegúrese de que Form1.cs (Form1.vb para Visual Basic) contiene los siguientes
extractos de using (Using en Visual Basic):
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;

3. Agregue los siguientes miembros de datos a la clase de Form1 :

MCT: Luis Dueñas Pag 232 de 336


Manual de .NET Framework 4.5

// The head of the dataflow network.


ITargetBlock<string> headBlock = null;
// Enables the user interface to signal cancellation to the network.
CancellationTokenSource cancellationTokenSource;

4. Agregue el método siguiente, CreateImageProcessingNetwork, a la clase de Form1 .


Este método crea la red de procesamiento de imágenes.
// Creates the image processing dataflow network and returns the
// head node of the network.
ITargetBlock<string> CreateImageProcessingNetwork()
{
//
// Create the dataflow blocks that form the network.
// Create a dataflow block that takes a folder path as input
// and returns a collection of Bitmap objects.
var loadBitmaps = new TransformBlock<string,
IEnumerable<Bitmap>>(path =>
{
try
{
return LoadBitmaps(path);
}
catch (OperationCanceledException)
{
// Handle cancellation by passing the empty collection
// to the next stage of the network.
return Enumerable.Empty<Bitmap>();
}
});
// Create a dataflow block that takes a collection of Bitmap objects
// and returns a single composite bitmap.
var createCompositeBitmap = new TransformBlock<IEnumerable<Bitmap>,
Bitmap>(bitmaps =>
{
try
{
return CreateCompositeBitmap(bitmaps);
}
catch (OperationCanceledException)
{
// Handle cancellation by passing null to the next stage
// of the network.
return null;
}
});

// Create a dataflow block that displays the provided bitmap on the


form.
var displayCompositeBitmap = new ActionBlock<Bitmap>(bitmap =>
{
// Display the bitmap.
pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
pictureBox1.Image = bitmap;

// Enable the user to select another folder.


toolStripButton1.Enabled = true;
toolStripButton2.Enabled = false;
Cursor = DefaultCursor;
},
// Specify a task scheduler from the current synchronization
context
// so that the action runs on the UI thread.
new ExecutionDataflowBlockOptions
{
TaskScheduler =
TaskScheduler.FromCurrentSynchronizationContext()
});
// Create a dataflow block that responds to a cancellation request by

MCT: Luis Dueñas Pag 233 de 336


Manual de .NET Framework 4.5

// displaying an image to indicate that the operation is cancelled


and
// enables the user to select another folder.
var operationCancelled = new ActionBlock<object>(delegate
{
// Display the error image to indicate that the operation
// was cancelled.
pictureBox1.SizeMode = PictureBoxSizeMode.CenterImage;
pictureBox1.Image = pictureBox1.ErrorImage;
// Enable the user to select another folder.
toolStripButton1.Enabled = true;
toolStripButton2.Enabled = false;
Cursor = DefaultCursor;
},
// Specify a task scheduler from the current synchronization
context
// so that the action runs on the UI thread.
new ExecutionDataflowBlockOptions
{
TaskScheduler =
TaskScheduler.FromCurrentSynchronizationContext()
});
// Connect the network.
// Link loadBitmaps to createCompositeBitmap.
// The provided predicate ensures that createCompositeBitmap accepts
the
// collection of bitmaps only if that collection has at least one
member.
loadBitmaps.LinkTo(createCompositeBitmap, bitmaps => bitmaps.Count()
> 0);
// Also link loadBitmaps to operationCancelled.
// When createCompositeBitmap rejects the message, loadBitmaps
// offers the message to operationCancelled.
// operationCancelled accepts all messages because we do not provide
a
// predicate.
loadBitmaps.LinkTo(operationCancelled);
// Link createCompositeBitmap to displayCompositeBitmap.
// The provided predicate ensures that displayCompositeBitmap accepts
the
// bitmap only if it is non-null.
createCompositeBitmap.LinkTo(displayCompositeBitmap, bitmap => bitmap
!= null);
// Also link createCompositeBitmap to operationCancelled.
// When displayCompositeBitmap rejects the message,
createCompositeBitmap
// offers the message to operationCancelled.
// operationCancelled accepts all messages because we do not provide
a
// predicate.
createCompositeBitmap.LinkTo(operationCancelled);
// Return the head of the network.
return loadBitmaps;
}

5. Implemente el método LoadBitmaps.


// Loads all bitmap files that exist at the provided path.
IEnumerable<Bitmap> LoadBitmaps(string path)
{
List<Bitmap> bitmaps = new List<Bitmap>();
// Load a variety of image types.
foreach (string bitmapType in
new string[] { "*.bmp", "*.gif", "*.jpg", "*.png", "*.tif" })
{
// Load each bitmap for the current extension.
foreach (string fileName in Directory.GetFiles(path, bitmapType))
{

MCT: Luis Dueñas Pag 234 de 336


Manual de .NET Framework 4.5

// Throw OperationCanceledException if cancellation is


requested.
cancellationTokenSource.Token.ThrowIfCancellationRequested();
try
{
// Add the Bitmap object to the collection.
bitmaps.Add(new Bitmap(fileName));
}
catch (Exception)
{
// TODO: A complete application might handle the error.
}
}
}
return bitmaps;
}

6. Implemente el método CreateCompositeBitmap.


// Creates a composite bitmap from the provided collection of Bitmap
objects.
// This method computes the average color of each pixel among all
bitmaps
// to create the composite image.
Bitmap CreateCompositeBitmap(IEnumerable<Bitmap> bitmaps)
{
Bitmap[] bitmapArray = bitmaps.ToArray();
// Compute the maximum width and height components of all
// bitmaps in the collection.
Rectangle largest = new Rectangle();
foreach (var bitmap in bitmapArray)
{
if (bitmap.Width > largest.Width) largest.Width = bitmap.Width;
if (bitmap.Height > largest.Height) largest.Height =
bitmap.Height;
}
// Create a 32-bit Bitmap object with the greatest dimensions.
Bitmap result = new Bitmap(largest.Width, largest.Height,
PixelFormat.Format32bppArgb);
// Lock the result Bitmap.
var resultBitmapData = result.LockBits(
new Rectangle(new Point(), result.Size), ImageLockMode.WriteOnly,
result.PixelFormat);
// Lock each source bitmap to create a parallel list of BitmapData
objects.
var bitmapDataList = (from bitmap in bitmapArray
select bitmap.LockBits(
new Rectangle(new Point(), bitmap.Size),

ImageLockMode.ReadOnly,PixelFormat.Format32bppArgb))
.ToList();
// Compute each column in parallel.
Parallel.For(0, largest.Width, new ParallelOptions
{
CancellationToken = cancellationTokenSource.Token
},
i =>
{
// Compute each row.
for (int j = 0; j < largest.Height; j++)
{
// Counts the number of bitmaps whose dimensions
// contain the current location.
int count = 0;
// The sum of all alpha, red, green, and blue components.
int a = 0, r = 0, g = 0, b = 0;
// For each bitmap, compute the sum of all color components.
foreach (var bitmapData in bitmapDataList)
{

MCT: Luis Dueñas Pag 235 de 336


Manual de .NET Framework 4.5

// Ensure that we stay within the bounds of the image.


if (bitmapData.Width > i && bitmapData.Height > j)
{
unsafe
{
byte*row=(byte*)(bitmapData.Scan0 +
(j*bitmapData.Stride));
byte* pix = (byte*)(row + (4 * i));
a += *pix; pix++;
r += *pix; pix++;
g += *pix; pix++;
b += *pix;
}
count++;
}
}
unsafe
{
// Compute the average of each color component.
a /= count;
r /= count;
g /= count;
b /= count;
// Set the result pixel.
byte*row=(byte*)(resultBitmapData.Scan0 +
(j*resultBitmapData.Stride));
byte* pix = (byte*)(row + (4 * i));
*pix = (byte)a; pix++;
*pix = (byte)r; pix++;
*pix = (byte)g; pix++;
*pix = (byte)b;
}
}
});
// Unlock the source bitmaps.
for (int i = 0; i < bitmapArray.Length; i++)
{
bitmapArray[i].UnlockBits(bitmapDataList[i]);
}
// Unlock the result bitmap.
result.UnlockBits(resultBitmapData);
// Return the result.
return result;
}
Nota
La versión de C# del método de CreateCompositeBitmap utiliza punteros para
habilitar el procesamiento eficaz de los objetos de System.Drawing.Bitmap . Por
consiguiente, debe habilitar la opción de Permitir código no seguro en el proyecto para
utilizar la palabra clave de seguro.
En la tabla siguiente se describen los miembros de la red.
Miembro Tipo Descripción
Toma una ruta de la carpeta como
TransformBlock<TInput,
loadBitmaps entrada y genera una colección de
TOutput>
objetos Bitmap como resultado.
Toma una colección de objetos Bitmap
TransformBlock<TInput,
createCompositeBitmap como entrada y presenta un mapa de
TOutput>
bits compuesto como resultado.
Muestra el mapa de bits compuesto en
displayCompositeBitmap ActionBlock<TInput>
el formulario.
operationCancelled ActionBlock<TInput> Muestra una imagen para indicar que se

MCT: Luis Dueñas Pag 236 de 336


Manual de .NET Framework 4.5

cancele la operación y permite al


usuario seleccionar otra carpeta.
Para conectar los bloques de flujo de datos para formar una red, este ejemplo utiliza el método
de LinkTo. El método de LinkTo contiene una versión sobrecargada que toma un objeto de
Predicate<T> que determina si el bloque de destino acepta o rechaza un mensaje. Este
mecanismo de filtrado habilita los bloques de mensajes a ciertos valores RO. En este ejemplo, la
red puede crear bifurcaciones de dos maneras. La bifurcación principal carga las imágenes
desde el disco, crea la imagen compuesta, y muestra esa imagen en el formulario. Alternar las
cancelaciones de bifurcación la operación actual. Los objetos de Predicate<T> permiten a los
bloques de flujo de datos a lo largo de la bifurcación principal para cambiar a la alternativa
bifurcación rechazando determinados mensajes. Por ejemplo, si el usuario cancela la operación,
el bloque createCompositeBitmap de flujo de datos genera null (Nothing en Visual Basic)
como resultado. El flujo de datos bloquea valores de entrada de null de los rechazos de
displayCompositeBitmap y, por consiguiente, el mensaje se proporciona a
operationCancelled. El bloque operationCancelled de flujo de datos acepta todos los
mensajes y por lo tanto, muestra una imagen para indicar que la operación se cancela.
La siguiente ilustración se muestra la red de procesamiento de imágenes.

Dado que los bloques de flujo de datos de displayCompositeBitmap y de operationCancelled


representar en la interfaz de usuario, es importante que estas acciones aparecen en el subproceso
de interfaz de usuario. Para ello, durante la creación, estos objetos cada proporcionan un objeto
de ExecutionDataflowBlockOptions que tiene la propiedad de TaskScheduler establecida en
TaskScheduler.FromCurrentSynchronizationContext. El método de TaskScheduler.FromCurrent
SynchronizationContext crea un objeto de TaskScheduler que realice el trabajo en el contexto de
sincronización. Dado que el método de CreateImageProcessingNetwork se denomina del
controlador del botón de la carpeta de elegir, que se ejecuta en el subproceso de interfaz de
usuario, acciones para los bloques de flujo de datos de displayCompositeBitmap y de
operationCancelled también se ejecutan en el subproceso de interfaz de usuario.
Este ejemplo utiliza un token compartido de cancelación en lugar de establecer la propiedad de
CancellationToken porque la propiedad de CancellationToken cancela permanentemente la
ejecución del bloque de flujo de datos. Un token de cancelación permite a este ejemplo reutilizar
los mismos varias veces la red de flujo de datos, incluso cuando el usuario cancela una o más
operaciones.
Conexión de red de flujo de datos con la interfaz de usuario
Esta sección describe cómo conectar la red de flujo de datos a la interfaz de usuario. Creación
de imagen compuesta y cancelar la operación se inician de carpeta y de los botones Cancelar
choose. Cuando el usuario elige cualquiera de estos botones, la acción adecuada se inicia de
forma asincrónica.
Para conectar la red de flujo de datos a la interfaz de usuario
1. En el diseñador de formularios para el formulario principal, cree un controlador de
eventos para el evento Click para el botón de la carpeta de elegir.
2. Implemente el evento de Click para el botón de la carpeta de elegir.
// Event handler for the Choose Folder button.

MCT: Luis Dueñas Pag 237 de 336


Manual de .NET Framework 4.5

private void toolStripButton1_Click(object sender, EventArgs e)


{
// Create a FolderBrowserDialog object to enable the user to
// select a folder.
FolderBrowserDialog dlg = new FolderBrowserDialog
{
ShowNewFolderButton = false
};
// Set the selected path to the common Sample Pictures folder
// if it exists.
string initialDirectory = Path.Combine(

Environment.GetFolderPath(Environment.SpecialFolder.CommonPictures),
"Sample Pictures");
if (Directory.Exists(initialDirectory))
{
dlg.SelectedPath = initialDirectory;
}
// Show the dialog and process the dataflow network.
if (dlg.ShowDialog() == DialogResult.OK)
{
// Create a new CancellationTokenSource object to enable
cancellation.
cancellationTokenSource = new CancellationTokenSource();
// Create the image processing network if needed.
if (headBlock == null)
{
headBlock = CreateImageProcessingNetwork();
}
// Post the selected path to the network.
headBlock.Post(dlg.SelectedPath);
// Enable the Cancel button and disable the Choose Folder button.
toolStripButton1.Enabled = false;
toolStripButton2.Enabled = true;
// Show a wait cursor.
Cursor = Cursors.WaitCursor;
}
}

3. En el diseñador de formularios para el formulario principal, cree un controlador de


eventos para el evento Click para el botón cancel.
4. Implemente el evento de Click para el botón cancel.
// Event handler for the Cancel button.
private void toolStripButton2_Click(object sender, EventArgs e)
{
// Signal the request for cancellation. The current component of
// the dataflow network will respond to the cancellation request.
cancellationTokenSource.Cancel();
}

Ejemplo completo
El ejemplo siguiente se muestra el código completo de este tutorial.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;

namespace CompositeImages
{

MCT: Luis Dueñas Pag 238 de 336


Manual de .NET Framework 4.5

public partial class Form1 : Form


{
// The head of the dataflow network.
ITargetBlock<string> headBlock = null;
// Enables the user interface to signal cancellation to the network.
CancellationTokenSource cancellationTokenSource;

public Form1()
{
InitializeComponent();
}

// Creates the image processing dataflow network and returns the


// head node of the network.
ITargetBlock<string> CreateImageProcessingNetwork()
{
// Create the dataflow blocks that form the network.
// Create a dataflow block that takes a folder path as input
// and returns a collection of Bitmap objects.
var loadBitmaps = new TransformBlock<string,
IEnumerable<Bitmap>>(path =>
{
try
{
return LoadBitmaps(path);
}
catch (OperationCanceledException)
{
// Handle cancellation by passing the empty collection
// to the next stage of the network.
return Enumerable.Empty<Bitmap>();
}
});
// Create a dataflow block that takes a collection of Bitmap objects
// and returns a single composite bitmap.
var createCompositeBitmap = new TransformBlock<IEnumerable<Bitmap>,
Bitmap>(bitmaps =>
{
try
{
return CreateCompositeBitmap(bitmaps);
}
catch (OperationCanceledException)
{
// Handle cancellation by passing null to the next stage
// of the network.
return null;
}
});
// Create a dataflow block that displays the provided bitmap on the
form.
var displayCompositeBitmap = new ActionBlock<Bitmap>(bitmap =>
{
// Display the bitmap.
pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
pictureBox1.Image = bitmap;
// Enable the user to select another folder.
toolStripButton1.Enabled = true;
toolStripButton2.Enabled = false;
Cursor = DefaultCursor;
},
// Specify a task scheduler from the current synchronization
context
// so that the action runs on the UI thread.
new ExecutionDataflowBlockOptions
{
TaskScheduler =
TaskScheduler.FromCurrentSynchronizationContext()

MCT: Luis Dueñas Pag 239 de 336


Manual de .NET Framework 4.5

});
// Create a dataflow block that responds to a cancellation request by
// displaying an image to indicate that the operation is cancelled
and
// enables the user to select another folder.
var operationCancelled = new ActionBlock<object>(delegate
{
// Display the error image to indicate that the operation
// was cancelled.
pictureBox1.SizeMode = PictureBoxSizeMode.CenterImage;
pictureBox1.Image = pictureBox1.ErrorImage;
// Enable the user to select another folder.
toolStripButton1.Enabled = true;
toolStripButton2.Enabled = false;
Cursor = DefaultCursor;
},
// Specify a task scheduler from the current synchronization
context
// so that the action runs on the UI thread.
new ExecutionDataflowBlockOptions
{
TaskScheduler =
TaskScheduler.FromCurrentSynchronizationContext()
});
// Connect the network.
// Link loadBitmaps to createCompositeBitmap.
// The provided predicate ensures that createCompositeBitmap accepts
the
// collection of bitmaps only if that collection has at least one
member.
loadBitmaps.LinkTo(createCompositeBitmap, bitmaps => bitmaps.Count()
> 0);
// Also link loadBitmaps to operationCancelled.
// When createCompositeBitmap rejects the message, loadBitmaps
// offers the message to operationCancelled.
// operationCancelled accepts all messages because we do not provide
a
// predicate.
loadBitmaps.LinkTo(operationCancelled);
// Link createCompositeBitmap to displayCompositeBitmap.
// The provided predicate ensures that displayCompositeBitmap accepts
the
// bitmap only if it is non-null.
createCompositeBitmap.LinkTo(displayCompositeBitmap, bitmap => bitmap
!=
null);
// Also link createCompositeBitmap to operationCancelled.
// When displayCompositeBitmap rejects the message,
createCompositeBitmap
// offers the message to operationCancelled.
// operationCancelled accepts all messages because we do not provide
a
// predicate.
createCompositeBitmap.LinkTo(operationCancelled);
// Return the head of the network.
return loadBitmaps;
}
// Loads all bitmap files that exist at the provided path.
IEnumerable<Bitmap> LoadBitmaps(string path)
{
List<Bitmap> bitmaps = new List<Bitmap>();
// Load a variety of image types.
foreach (string bitmapType in
new string[] { "*.bmp", "*.gif", "*.jpg", "*.png", "*.tif" })
{
// Load each bitmap for the current extension.
foreach (string fileName in Directory.GetFiles(path, bitmapType))
{

MCT: Luis Dueñas Pag 240 de 336


Manual de .NET Framework 4.5

// Throw OperationCanceledException if cancellation is


requested.
cancellationTokenSource.Token.ThrowIfCancellationRequested();
try
{
// Add the Bitmap object to the collection.
bitmaps.Add(new Bitmap(fileName));
}
catch (Exception)
{
// TODO: A complete application might handle the error.
}
}
}
return bitmaps;
}
// Creates a composite bitmap from the provided collection of Bitmap
objects.
// This method computes the average color of each pixel among all
bitmaps
// to create the composite image.
Bitmap CreateCompositeBitmap(IEnumerable<Bitmap> bitmaps)
{
Bitmap[] bitmapArray = bitmaps.ToArray();
// Compute the maximum width and height components of all
// bitmaps in the collection.
Rectangle largest = new Rectangle();
foreach (var bitmap in bitmapArray)
{
if (bitmap.Width > largest.Width) largest.Width = bitmap.Width;
if (bitmap.Height > largest.Height) largest.Height =
bitmap.Height;
}
// Create a 32-bit Bitmap object with the greatest dimensions.
Bitmap result = new Bitmap(largest.Width, largest.Height,
PixelFormat.Format32bppArgb);
// Lock the result Bitmap.
var resultBitmapData = result.LockBits(
new Rectangle(new Point(), result.Size), ImageLockMode.WriteOnly,
result.PixelFormat);
// Lock each source bitmap to create a parallel list of BitmapData
objects.
var bitmapDataList = (from bitmap in bitmapArray
select bitmap.LockBits(
new Rectangle(new Point(), bitmap.Size),
ImageLockMode.ReadOnly,
PixelFormat.Format32bppArgb))
.ToList();
// Compute each column in parallel.
Parallel.For(0, largest.Width, new ParallelOptions
{
CancellationToken = cancellationTokenSource.Token
},
i =>
{
// Compute each row.
for (int j = 0; j < largest.Height; j++)
{
// Counts the number of bitmaps whose dimensions
// contain the current location.
int count = 0;
// The sum of all alpha, red, green, and blue components.
int a = 0, r = 0, g = 0, b = 0;
// For each bitmap, compute the sum of all color components.
foreach (var bitmapData in bitmapDataList)
{
// Ensure that we stay within the bounds of the image.
if (bitmapData.Width > i && bitmapData.Height > j)

MCT: Luis Dueñas Pag 241 de 336


Manual de .NET Framework 4.5

{
unsafe
{
byte*row=(byte*)(bitmapData.Scan0 + (j *
bitmapData.Stride));
byte* pix = (byte*)(row + (4 * i));
a += *pix; pix++;
r += *pix; pix++;
g += *pix; pix++;
b += *pix;
}
count++;
}
}
unsafe
{
// Compute the average of each color component.
a /= count;
r /= count;
g /= count;
b /= count;
// Set the result pixel.
byte*row=(byte*)(resultBitmapData.Scan0 +
(j*resultBitmapData.Stride));
byte* pix = (byte*)(row + (4 * i));
*pix = (byte)a; pix++;
*pix = (byte)r; pix++;
*pix = (byte)g; pix++;
*pix = (byte)b;
}
}
});
// Unlock the source bitmaps.
for (int i = 0; i < bitmapArray.Length; i++)
{
bitmapArray[i].UnlockBits(bitmapDataList[i]);
}
// Unlock the result bitmap.
result.UnlockBits(resultBitmapData);
// Return the result.
return result;
}
// Event handler for the Choose Folder button.
private void toolStripButton1_Click(object sender, EventArgs e)
{
// Create a FolderBrowserDialog object to enable the user to
// select a folder.
FolderBrowserDialog dlg = new FolderBrowserDialog
{
ShowNewFolderButton = false
};
// Set the selected path to the common Sample Pictures folder
// if it exists.
string initialDirectory = Path.Combine(

Environment.GetFolderPath(Environment.SpecialFolder.CommonPictures),
"Sample Pictures");
if (Directory.Exists(initialDirectory))
{
dlg.SelectedPath = initialDirectory;
}
// Show the dialog and process the dataflow network.
if (dlg.ShowDialog() == DialogResult.OK)
{
// Create a new CancellationTokenSource object to enable
cancellation.
cancellationTokenSource = new CancellationTokenSource();
// Create the image processing network if needed.

MCT: Luis Dueñas Pag 242 de 336


Manual de .NET Framework 4.5

if (headBlock == null)
{
headBlock = CreateImageProcessingNetwork();
}
// Post the selected path to the network.
headBlock.Post(dlg.SelectedPath);
// Enable the Cancel button and disable the Choose Folder button.
toolStripButton1.Enabled = false;
toolStripButton2.Enabled = true;
// Show a wait cursor.
Cursor = Cursors.WaitCursor;
}
}
// Event handler for the Cancel button.
private void toolStripButton2_Click(object sender, EventArgs e)
{
// Signal the request for cancellation. The current component of
// the dataflow network will respond to the cancellation request.
cancellationTokenSource.Cancel();
}
}
}

La ilustración siguiente muestra el resultado típico para las imágenes \ carpeta de común \ de
ejemplo.

3.1.3.7. Cómo: Cancelar un bloque de flujos de datos


En este documento se muestra cómo habilitar la cancelación en la aplicación. Este ejemplo
utiliza Windows Forms para mostrar donde están activos los elementos de trabajo en una
canalización de flujo de datos y también los efectos de cancelación.
Para crear la aplicación de Windows Forms
1. Cree un proyecto de C# o Visual Basic Aplicación de Windows Forms . En los
siguientes pasos, el proyecto se denomina CancellationWinForms.
2. En el diseñador de formularios para el formulario principal, Form1.cs (Form1.vb para
Visual Basic), agregue un control de ToolStrip .
3. Agregue un control de ToolStripButton al control de ToolStrip . Establezca la propiedad
de DisplayStyle a Text y la propiedad de Text para agregar elementos de trabajo.
4. Agregue un control de ToolStripButton de segundo al control de ToolStrip . Establezca
la propiedad de DisplayStyle a Text, la propiedad de Text para cancelar, y la propiedad
de Enabled a False.

MCT: Luis Dueñas Pag 243 de 336


Manual de .NET Framework 4.5

5. Agregue cuatro objetos de ToolStripProgressBar al control de ToolStrip .


Crear la canalización de flujo de datos
En esta sección se describe cómo crear la canalización de flujo de datos que los elementos de
trabajo de procesos y actualiza las barras de progreso.

Para crear la canalización de flujo de datos


1. En el proyecto, agregue una referencia a System.Threading.Tasks.Dataflow.dll.
2. Asegúrese de que Form1.cs (Form1.vb para Visual Basic) contiene los siguientes
extractos de using (Imports en Visual Basic).
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;

3. Agregue la clase de WorkItem como tipo interno de la clase de Form1 .


// A placeholder type that performs work.
class WorkItem
{
// Performs work for the provided number of milliseconds.
public void DoWork(int milliseconds)
{
// For demonstration, suspend the current thread.
Thread.Sleep(milliseconds);
}
}

4. Agregue los siguientes miembros de datos a la clase de Form1 .


// Enables the user interface to signal cancellation.
CancellationTokenSource cancellationSource;
// The first node in the dataflow pipeline.
TransformBlock<WorkItem, WorkItem> startWork;
// The second, and final, node in the dataflow pipeline.
ActionBlock<WorkItem> completeWork;
// Increments the value of the provided progress bar.
ActionBlock<ToolStripProgressBar> incrementProgress;
// Decrements the value of the provided progress bar.
ActionBlock<ToolStripProgressBar> decrementProgress;
// Enables progress bar actions to run on the UI thread.
TaskScheduler uiTaskScheduler;

5. Agregue el método siguiente, CreatePipeline, a la clase de Form1 .


// Creates the blocks that participate in the dataflow pipeline.
private void CreatePipeline()
{
// Create the cancellation source.
cancellationSource = new CancellationTokenSource();
// Create the first node in the pipeline.
startWork = new TransformBlock<WorkItem, WorkItem>(workItem =>
{
// Perform some work.
workItem.DoWork(250);
// Decrement the progress bar that tracks the count of
// active work items in this stage of the pipeline.
decrementProgress.Post(toolStripProgressBar1);
// Increment the progress bar that tracks the count of
// active work items in the next stage of the pipeline.
incrementProgress.Post(toolStripProgressBar2);
// Send the work item to the next stage of the pipeline.
return workItem;
},
new ExecutionDataflowBlockOptions

MCT: Luis Dueñas Pag 244 de 336


Manual de .NET Framework 4.5

{
CancellationToken = cancellationSource.Token
});
// Create the second, and final, node in the pipeline.
completeWork = new ActionBlock<WorkItem>(workItem =>
{
// Perform some work.
workItem.DoWork(1000);
// Decrement the progress bar that tracks the count of
// active work items in this stage of the pipeline.
decrementProgress.Post(toolStripProgressBar2);
// Increment the progress bar that tracks the overall
// count of completed work items.
incrementProgress.Post(toolStripProgressBar3);
},
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token,
MaxDegreeOfParallelism = 2
});
// Connect the two nodes of the pipeline.
startWork.LinkTo(completeWork);
// When the first node completes, set the second node also to
// the completed state.
startWork.Completion.ContinueWith(delegate { completeWork.Complete();
});
// Create the dataflow action blocks that increment and decrement
// progress bars.
// These blocks use the task scheduler that is associated with
// the UI thread.
incrementProgress = new ActionBlock<ToolStripProgressBar>(
progressBar => progressBar.Value++,
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token,
TaskScheduler = uiTaskScheduler
});
decrementProgress = new ActionBlock<ToolStripProgressBar>(
progressBar => progressBar.Value--,
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token,
TaskScheduler = uiTaskScheduler
});
}

Dado que los bloques de flujo de datos de incrementProgress y de decrementProgress


representar en la interfaz de usuario, es importante que estas acciones aparecen en el subproceso
de interfaz de usuario. Para ello, durante la construcción estos objetos cada proporcionan un
objeto de ExecutionDataflowBlockOptions que tiene la propiedad de TaskScheduler establecida
en TaskScheduler.FromCurrentSynchronizationContext. El método de
TaskScheduler.FromCurrentSynchronizationContext crea un objeto de TaskScheduler que
realice el trabajo en el contexto de sincronización. Dado que se llama al constructor de Form1
el subproceso, acciones para los bloques de flujo de datos de incrementProgress y de
decrementProgress también se ejecutan en el subproceso de interfaz de usuario.
Este ejemplo establece la propiedad de CancellationToken cuando construye los miembros de la
canalización. Dado que la propiedad de CancellationToken cancela permanentemente la
ejecución del bloque de flujo de datos, la canalización de conjunto debe volver a crear después
de que el usuario cancele la operación y la desee agregar más elementos de trabajo a la
canalización.
Conexión de canalización de flujo de datos con la interfaz de usuario

MCT: Luis Dueñas Pag 245 de 336


Manual de .NET Framework 4.5

Esta sección describe cómo conectar la canalización de flujo de datos a la interfaz de usuario.
Creando la canalización y agregando elementos de trabajo a la canalización son controlados por
el controlador de eventos para el botón de los elementos de trabajo add. La cancelación es
iniciada por el botón Cancelar. Cuando el usuario hace clic en cualquiera de ellos, la acción
adecuada se inicia de forma asincrónica.

Para conectar la canalización de flujo de datos a la interfaz de usuario


1. En el diseñador de formularios para el formulario principal, cree un controlador de
eventos para el evento Click para el botón de los elementos de trabajo add.
2. Implemente el evento de Click para el botón de los elementos de trabajo add.
// Event handler for the Add Work Items button.
private void toolStripButton1_Click(object sender, EventArgs e)
{
// The Cancel button is disabled when the pipeline is not active.
// Therefore, create the pipeline and enable the Cancel button
// if the Cancel button is disabled.
if (!toolStripButton2.Enabled)
{
CreatePipeline();
// Enable the Cancel button.
toolStripButton2.Enabled = true;
}
// Post several work items to the head of the pipeline.
for (int i = 0; i < 5; i++)
{
toolStripProgressBar1.Value++;
startWork.Post(new WorkItem());
}
}

3. En el diseñador de formularios para el formulario principal, cree un controlador de


eventos para el controlador de eventos Click para el botón cancel.
4. Implemente el controlador de eventos de Click para el botón cancel.
// Event handler for the Cancel button.
private async void toolStripButton2_Click(object sender, EventArgs e)
{
// Disable both buttons.
toolStripButton1.Enabled = false;
toolStripButton2.Enabled = false;
// Trigger cancellation.
cancellationSource.Cancel();
try
{
// Asynchronously wait for the pipeline to complete processing and
for
// the progress bars to update.
await
Task.WhenAll(completeWork.Completion,incrementProgress.Completion,
decrementProgress.Completion);
}
catch (OperationCanceledException)
{
}
// Increment the progress bar that tracks the number of cancelled
// work items by the number of active work items.
toolStripProgressBar4.Value += toolStripProgressBar1.Value;
toolStripProgressBar4.Value += toolStripProgressBar2.Value;
// Reset the progress bars that track the number of active work
items.
toolStripProgressBar1.Value = 0;
toolStripProgressBar2.Value = 0;
// Enable the Add Work Items button.
toolStripButton1.Enabled = true;

MCT: Luis Dueñas Pag 246 de 336


Manual de .NET Framework 4.5

Ejemplo
El ejemplo siguiente se muestra el código completo de Form1.cs (Form1.vb para Visual Basic).
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;

namespace CancellationWinForms
{
public partial class Form1 : Form
{
// A placeholder type that performs work.
class WorkItem
{
// Performs work for the provided number of milliseconds.
public void DoWork(int milliseconds)
{
// For demonstration, suspend the current thread.
Thread.Sleep(milliseconds);
}
}
// Enables the user interface to signal cancellation.
CancellationTokenSource cancellationSource;
// The first node in the dataflow pipeline.
TransformBlock<WorkItem, WorkItem> startWork;
// The second, and final, node in the dataflow pipeline.
ActionBlock<WorkItem> completeWork;
// Increments the value of the provided progress bar.
ActionBlock<ToolStripProgressBar> incrementProgress;
// Decrements the value of the provided progress bar.
ActionBlock<ToolStripProgressBar> decrementProgress;
// Enables progress bar actions to run on the UI thread.
TaskScheduler uiTaskScheduler;

public Form1()
{
InitializeComponent();
// Create the UI task scheduler from the current sychronization
// context.
uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
}

// Creates the blocks that participate in the dataflow pipeline.


private void CreatePipeline()
{
// Create the cancellation source.
cancellationSource = new CancellationTokenSource();
// Create the first node in the pipeline.
startWork = new TransformBlock<WorkItem, WorkItem>(workItem =>
{
// Perform some work.
workItem.DoWork(250);
// Decrement the progress bar that tracks the count of
// active work items in this stage of the pipeline.
decrementProgress.Post(toolStripProgressBar1);
// Increment the progress bar that tracks the count of
// active work items in the next stage of the pipeline.
incrementProgress.Post(toolStripProgressBar2);
// Send the work item to the next stage of the pipeline.
return workItem;
},
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token

MCT: Luis Dueñas Pag 247 de 336


Manual de .NET Framework 4.5

});
// Create the second, and final, node in the pipeline.
completeWork = new ActionBlock<WorkItem>(workItem =>
{
// Perform some work.
workItem.DoWork(1000);
// Decrement the progress bar that tracks the count of
// active work items in this stage of the pipeline.
decrementProgress.Post(toolStripProgressBar2);
// Increment the progress bar that tracks the overall
// count of completed work items.
incrementProgress.Post(toolStripProgressBar3);
},
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token,
MaxDegreeOfParallelism = 2
});
// Connect the two nodes of the pipeline.
startWork.LinkTo(completeWork);
// When the first node completes, set the second node also to
// the completed state.
startWork.Completion.ContinueWith(delegate { completeWork.Complete();
});
// Create the dataflow action blocks that increment and decrement
// progress bars.
// These blocks use the task scheduler that is associated with
// the UI thread.
incrementProgress = new ActionBlock<ToolStripProgressBar>(
progressBar => progressBar.Value++,
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token,
TaskScheduler = uiTaskScheduler
});
decrementProgress = new ActionBlock<ToolStripProgressBar>(
progressBar => progressBar.Value--,
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationSource.Token,
TaskScheduler = uiTaskScheduler
});
}

// Event handler for the Add Work Items button.


private void toolStripButton1_Click(object sender, EventArgs e)
{
// The Cancel button is disabled when the pipeline is not active.
// Therefore, create the pipeline and enable the Cancel button
// if the Cancel button is disabled.
if (!toolStripButton2.Enabled)
{
CreatePipeline();
// Enable the Cancel button.
toolStripButton2.Enabled = true;
}
// Post several work items to the head of the pipeline.
for (int i = 0; i < 5; i++)
{
toolStripProgressBar1.Value++;
startWork.Post(new WorkItem());
}
}

// Event handler for the Cancel button.


private async void toolStripButton2_Click(object sender, EventArgs e)
{
// Disable both buttons.

MCT: Luis Dueñas Pag 248 de 336


Manual de .NET Framework 4.5

toolStripButton1.Enabled = false;
toolStripButton2.Enabled = false;
// Trigger cancellation.
cancellationSource.Cancel();
try
{
// Asynchronously wait for the pipeline to complete processing and
for
// the progress bars to update.
await
Task.WhenAll(completeWork.Completion,incrementProgress.Completion,
decrementProgress.Completion);
}
catch (OperationCanceledException)
{
}
// Increment the progress bar that tracks the number of cancelled
// work items by the number of active work items.
toolStripProgressBar4.Value += toolStripProgressBar1.Value;
toolStripProgressBar4.Value += toolStripProgressBar2.Value;
// Reset the progress bars that track the number of active work
items.
toolStripProgressBar1.Value = 0;
toolStripProgressBar2.Value = 0;
// Enable the Add Work Items button.
toolStripButton1.Enabled = true;
}
}
}

La ilustración siguiente muestra la aplicación en ejecución.

3.1.3.8. Tutorial: Crear tipos de bloques de flujos de datos


personalizados
Aunque la biblioteca de TPL Dataflow proporciona varios tipos de bloques de flujo de datos que
habilitan una variedad de funcionalidades, también puede crear tipos de bloques de
personalizadas. Este documento se describe cómo crear un flujo de datos en bloques que
implementa un comportamiento personalizado.
Definición del bloque de flujo de datos de la ventana el deslizar
Considere una aplicación de flujo de datos que requiere que escribir valores están almacenados
en búfer y después generarse de una manera de la ventana el deslizar. Por ejemplo, los valores
de entrada {0, 1, 2, 3, 4, 5} y un tamaño de la ventana de tres, un bloque de flujo de datos de la
ventana el deslizar genera las matrices de salida {0, 1, 2}, {1, 2, 3}, {2, 3, 4}, y {3, 4, 5}. Las
secciones siguientes se describen dos maneras de crear un flujo de datos en bloques que
implementa este comportamiento personalizado. La primera técnica utiliza el método de
Encapsulate<TInput, TOutput> para combinar la funcionalidad de un objeto de
ISourceBlock<TOutput> y un objeto de ITargetBlock<TInput> en un bloque propagador. La
segunda técnica define una clase que deriva de IPropagatorBlock<TInput, TOutput> y combinar
la funcionalidad existente para realizar el comportamiento personalizado.
Mediante el método de encapsular los Define el bloque de flujo de datos de la ventana el
deslizar
El ejemplo siguiente se utiliza el método de Encapsulate<TInput, TOutput> para crear un
bloque propagador de un destino y un origen. Un bloque propagador habilita un bloque de
origen y un destino bloqueados actúe como receptor y remitente de los datos.

MCT: Luis Dueñas Pag 249 de 336


Manual de .NET Framework 4.5

Esta técnica es útil si necesita funcionalidad personalizada de flujo de datos, pero no requiere un
tipo que proporciona métodos adicionales, propiedades, campos o.
// Creates a IPropagatorBlock<T, T[]> object propagates data in a
// sliding window fashion.
public static IPropagatorBlock<T, T[]> CreateSlidingWindow<T>(int windowSize)
{
// Create a queue to hold messages.
var queue = new Queue<T>();

// The source part of the propagator holds arrays of size windowSize


// and propagates data out to any connected targets.
var source = new BufferBlock<T[]>();

// The target part receives data and adds them to the queue.
var target = new ActionBlock<T>(item =>
{
// Add the item to the queue.
queue.Enqueue(item);
// Remove the oldest item when the queue size exceeds the window size.
if (queue.Count > windowSize)
queue.Dequeue();
// Post the data in the queue to the source block when the queue size
// equals the window size.
if (queue.Count == windowSize)
source.Post(queue.ToArray());
});

// When the target is set to the completed state, propagate out any
// remaining data and set the source to the completed state.
target.Completion.ContinueWith(delegate
{
if (queue.Count > 0 && queue.Count < windowSize)
source.Post(queue.ToArray());
source.Complete();
});

// Return a IPropagatorBlock<T, T[]> object that encapsulates the


// target and source blocks.
return DataflowBlock.Encapsulate(target, source);
}

Derivando de IPropagatorBlock para Definir el bloque de flujo de datos de la ventana el


deslizar
En el siguiente ejemplo se muestra la clase SlidingWindowBlock. Esta clase se deriva de
IPropagatorBlock<TInput, TOutput> de modo que pueda actuar como origen y destino de datos.
Como en el ejemplo anterior, la clase de SlidingWindowBlock se compila en tipos existentes
de bloques de flujo de datos. Sin embargo, la clase de SlidingWindowBlock también
implementa los métodos requeridos por ISourceBlock<TOutput>, ITargetBlock<TInput>, e
interfaces de IDataflowBlock . Todos estos métodos transmiten a trabajo los miembros en
bloques predefinidos de flujo de datos. Por ejemplo, el método de Post deja el trabajo al
miembro de datos de m_target , que también es un objeto de ITargetBlock <TInput> .
Esta técnica es útil si necesita funcionalidad personalizada de flujo de datos, y también requiere
un tipo que proporciona métodos adicionales, propiedades, campos o. Por ejemplo, la clase de
SlidingWindowBlock también se deriva de IReceivableSourceBlock<TOutput> para poder
proporcionar métodos de TryReceive y de TryReceiveAll . La clase de SlidingWindowBlock
también muestra extensibilidad proporcionando la propiedad de WindowSize , que recupera el
número de elementos en la ventana el deslizar.
// Propagates data in a sliding window fashion.
public class SlidingWindowBlock<T> : IPropagatorBlock<T, T[]>,
IReceivableSourceBlock<T[]>
{

MCT: Luis Dueñas Pag 250 de 336


Manual de .NET Framework 4.5

// The size of the window.


private readonly int m_windowSize;
// The target part of the block.
private readonly ITargetBlock<T> m_target;
// The source part of the block.
private readonly IReceivableSourceBlock<T[]> m_source;
// Constructs a SlidingWindowBlock object.
public SlidingWindowBlock(int windowSize)
{
// Create a queue to hold messages.
var queue = new Queue<T>();
// The source part of the propagator holds arrays of size windowSize
// and propagates data out to any connected targets.
var source = new BufferBlock<T[]>();
// The target part receives data and adds them to the queue.
var target = new ActionBlock<T>(item =>
{
// Add the item to the queue.
queue.Enqueue(item);
// Remove the oldest item when the queue size exceeds the window
size.
if (queue.Count > windowSize)
queue.Dequeue();
// Post the data in the queue to the source block when the queue size
// equals the window size.
if (queue.Count == windowSize)
source.Post(queue.ToArray());
});
// When the target is set to the completed state, propagate out any
// remaining data and set the source to the completed state.
target.Completion.ContinueWith(delegate
{
if (queue.Count > 0 && queue.Count < windowSize)
source.Post(queue.ToArray());
source.Complete();
});
m_windowSize = windowSize;
m_target = target;
m_source = source;
}
// Retrieves the size of the window.
public int WindowSize { get { return m_windowSize; } }

#region IReceivableSourceBlock<TOutput> members

// Attempts to synchronously receive an item from the source.


public bool TryReceive(Predicate<T[]> filter, out T[] item)
{
return m_source.TryReceive(filter, out item);
}

// Attempts to remove all available elements from the source into a new
// array that is returned.
public bool TryReceiveAll(out IList<T[]> items)
{
return m_source.TryReceiveAll(out items);
}

#endregion

#region ISourceBlock<TOutput> members

// Links this dataflow block to the provided target.


public IDisposable LinkTo(ITargetBlock<T[]> target, DataflowLinkOptions
linkOptions)
{
return m_source.LinkTo(target, linkOptions);
}

MCT: Luis Dueñas Pag 251 de 336


Manual de .NET Framework 4.5

// Called by a target to reserve a message previously offered by a source


// but not yet consumed by this target.
bool ISourceBlock<T[]>.ReserveMessage(DataflowMessageHeader messageHeader,
ITargetBlock<T[]> target)
{
return m_source.ReserveMessage(messageHeader, target);
}

// Called by a target to consume a previously offered message from a


source.
T[] ISourceBlock<T[]>.ConsumeMessage(DataflowMessageHeader messageHeader,
ITargetBlock<T[]> target, out bool messageConsumed)
{
return m_source.ConsumeMessage(messageHeader,
target, out messageConsumed);
}

// Called by a target to release a previously reserved message from a


source.
void ISourceBlock<T[]>.ReleaseReservation(DataflowMessageHeader
messageHeader,
ITargetBlock<T[]> target)
{
m_source.ReleaseReservation(messageHeader, target);
}

#endregion

#region ITargetBlock<TInput> members

// Asynchronously passes a message to the target block, giving the target


the
// opportunity to consume the message.
DataflowMessageStatus ITargetBlock<T>.OfferMessage(DataflowMessageHeader
messageHeader,
T messageValue, ISourceBlock<T> source, bool consumeToAccept)
{
return m_target.OfferMessage(messageHeader, messageValue, source,
consumeToAccept);
}

#endregion

#region IDataflowBlock members

// Gets a Task that represents the completion of this dataflow block.


public Task Completion { get { return m_source.Completion; } }

// Signals to this target block that it should not accept any more
messages,
// nor consume postponed messages.
public void Complete()
{
m_target.Complete();
}

public void Fault(Exception error)


{
m_target.Fault(error);
}

#endregion
}

Ejemplo completo

MCT: Luis Dueñas Pag 252 de 336


Manual de .NET Framework 4.5

El ejemplo siguiente se muestra el código completo de este tutorial. También muestra cómo
utilizar el ambos que deslizan bloques de ventana en un método que escriba el bloque, lea de
ella, e imprimir los resultados en la consola.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;

// Demonstrates how to create a custom dataflow block type.


class Program
{
// Creates a IPropagatorBlock<T, T[]> object propagates data in a
// sliding window fashion.
public static IPropagatorBlock<T, T[]> CreateSlidingWindow<T>(int
windowSize)
{
// Create a queue to hold messages.
var queue = new Queue<T>();
// The source part of the propagator holds arrays of size windowSize
// and propagates data out to any connected targets.
var source = new BufferBlock<T[]>();
// The target part receives data and adds them to the queue.
var target = new ActionBlock<T>(item =>
{
// Add the item to the queue.
queue.Enqueue(item);
// Remove the oldest item when the queue size exceeds the window
size.
if (queue.Count > windowSize) queue.Dequeue();
// Post the data in the queue to the source block when the queue size
// equals the window size.
if (queue.Count == windowSize) source.Post(queue.ToArray());
});
// When the target is set to the completed state, propagate out any
// remaining data and set the source to the completed state.
target.Completion.ContinueWith(delegate
{
if (queue.Count > 0 && queue.Count < windowSize)
source.Post(queue.ToArray());
source.Complete();
});
// Return a IPropagatorBlock<T, T[]> object that encapsulates the
// target and source blocks.
return DataflowBlock.Encapsulate(target, source);
}

// Propagates data in a sliding window fashion.


public class SlidingWindowBlock<T> : IPropagatorBlock<T, T[]>,
IReceivableSourceBlock<T[]>
{
// The size of the window.
private readonly int m_windowSize;
// The target part of the block.
private readonly ITargetBlock<T> m_target;
// The source part of the block.
private readonly IReceivableSourceBlock<T[]> m_source;

// Constructs a SlidingWindowBlock object.


public SlidingWindowBlock(int windowSize)
{
// Create a queue to hold messages.
var queue = new Queue<T>();
// The source part of the propagator holds arrays of size windowSize
// and propagates data out to any connected targets.
var source = new BufferBlock<T[]>();
// The target part receives data and adds them to the queue.

MCT: Luis Dueñas Pag 253 de 336


Manual de .NET Framework 4.5

var target = new ActionBlock<T>(item =>


{
// Add the item to the queue.
queue.Enqueue(item);
// Remove the oldest item when the queue size exceeds the window
size.
if (queue.Count > windowSize) queue.Dequeue();
// Post the data in the queue to the source block when the queue
size
// equals the window size.
if (queue.Count == windowSize) source.Post(queue.ToArray());
});
// When the target is set to the completed state, propagate out any
// remaining data and set the source to the completed state.
target.Completion.ContinueWith(delegate
{
if (queue.Count > 0 && queue.Count < windowSize)
source.Post(queue.ToArray());
source.Complete();
});
m_windowSize = windowSize;
m_target = target;
m_source = source;
}

// Retrieves the size of the window.


public int WindowSize { get { return m_windowSize; } }

#region IReceivableSourceBlock<TOutput> members

// Attempts to synchronously receive an item from the source.


public bool TryReceive(Predicate<T[]> filter, out T[] item)
{
return m_source.TryReceive(filter, out item);
}

// Attempts to remove all available elements from the source into a new
// array that is returned.
public bool TryReceiveAll(out IList<T[]> items)
{
return m_source.TryReceiveAll(out items);
}

#endregion

#region ISourceBlock<TOutput> members

// Links this dataflow block to the provided target.


public IDisposable LinkTo(ITargetBlock<T[]> target, DataflowLinkOptions
linkOptions)
{
return m_source.LinkTo(target, linkOptions);
}

// Called by a target to reserve a message previously offered by a


source
// but not yet consumed by this target.
bool ISourceBlock<T[]>.ReserveMessage(DataflowMessageHeader
messageHeader,
ITargetBlock<T[]> target)
{
return m_source.ReserveMessage(messageHeader, target);
}

// Called by a target to consume a previously offered message from a


source.
T[] ISourceBlock<T[]>.ConsumeMessage(DataflowMessageHeader
messageHeader,

MCT: Luis Dueñas Pag 254 de 336


Manual de .NET Framework 4.5

ITargetBlock<T[]> target, out bool messageConsumed)


{
return m_source.ConsumeMessage(messageHeader,
target, out messageConsumed);
}

// Called by a target to release a previously reserved message from a


source.
void ISourceBlock<T[]>.ReleaseReservation(DataflowMessageHeader
messageHeader,
ITargetBlock<T[]> target)
{
m_source.ReleaseReservation(messageHeader, target);
}

#endregion

#region ITargetBlock<TInput> members

// Asynchronously passes a message to the target block, giving the


target the
// opportunity to consume the message.
DataflowMessageStatus ITargetBlock<T>.OfferMessage(DataflowMessageHeader
messageHeader,
T messageValue, ISourceBlock<T> source, bool consumeToAccept)
{
return m_target.OfferMessage(messageHeader, messageValue, source,
consumeToAccept);
}

#endregion

#region IDataflowBlock members

// Gets a Task that represents the completion of this dataflow block.


public Task Completion { get { return m_source.Completion; } }

// Signals to this target block that it should not accept any more
messages,
// nor consume postponed messages.
public void Complete()
{
m_target.Complete();
}

public void Fault(Exception error)


{
m_target.Fault(error);
}

#endregion
}

// Demonstrates usage of the sliding window block by sending the provided


// values to the provided propagator block and printing the output of
// that block to the console.
static void DemonstrateSlidingWindow<T>(IPropagatorBlock<T, T[]>
slidingWindow,
IEnumerable<T> values)
{
// Create an action block that prints arrays of data to the console.
string windowComma = string.Empty;
var printWindow = new ActionBlock<T[]>(window =>
{
Console.Write(windowComma);
Console.Write("{");
string comma = string.Empty;
foreach (T item in window)

MCT: Luis Dueñas Pag 255 de 336


Manual de .NET Framework 4.5

{
Console.Write(comma);
Console.Write(item);
comma = ",";
}
Console.Write("}");
windowComma = ", ";
});
// Link the printer block to the sliding window block.
slidingWindow.LinkTo(printWindow);
// Set the printer block to the completed state when the sliding window
// block completes.
slidingWindow.Completion.ContinueWith(delegate { printWindow.Complete();
});
// Print an additional newline to the console when the printer block
completes.
var completion = printWindow.Completion.ContinueWith(delegate {
Console.WriteLine(); });
// Post the provided values to the sliding window block and then wait
// for the sliding window block to complete.
foreach (T value in values)
{
slidingWindow.Post(value);
}
slidingWindow.Complete();
// Wait for the printer to complete and perform its final action.
completion.Wait();
}

static void Main(string[] args)


{
Console.Write("Using the DataflowBlockExtensions.Encapsulate method ");
Console.WriteLine("(T=int, windowSize=3):");
DemonstrateSlidingWindow(CreateSlidingWindow<int>(3),
Enumerable.Range(0, 10));
Console.WriteLine();
var slidingWindow = new SlidingWindowBlock<char>(4);
Console.Write("Using SlidingWindowBlock<T> ");
Console.WriteLine("(T=char, windowSize={0}):",
slidingWindow.WindowSize);
DemonstrateSlidingWindow(slidingWindow, from n in Enumerable.Range(65,
10)
select (char)n);
}
}
/* Output:
Using the DataflowBlockExtensions.Encapsulate method (T=int, windowSize=3):
{0,1,2}, {1,2,3}, {2,3,4}, {3,4,5}, {4,5,6}, {5,6,7}, {6,7,8}, {7,8,9}

Using SlidingWindowBlock<T> (T=char, windowSize=4):


{A,B,C,D}, {B,C,D,E}, {C,D,E,F}, {D,E,F,G}, {E,F,G,H}, {F,G,H,I}, {G,H,I,J}*/

3.1.3.9. Cómo: Usar JoinBlock para leer datos de diferentes orígenes


En este documento se explica cómo utilizar la clase de JoinBlock<T1, T2> para realizar una
operación cuando los datos están disponibles de varios orígenes. También muestra cómo utilizar
el modo no expansivo para permitir a los bloques de combinación múltiple para compartir un
origen de datos más eficazmente.
Ejemplo
El ejemplo siguiente define tres tipos de recursos, NetworkResource, FileResource, y
MemoryResource, y realizar operaciones cuando los recursos estén disponibles. Este ejemplo
requiere un par de NetworkResource y de MemoryResource para realizar la primera
operación y un par de FileResource y de MemoryResource para realizar la segunda operación.
Para permitir que estas operaciones se produzca cuando todos los recursos necesarios están

MCT: Luis Dueñas Pag 256 de 336


Manual de .NET Framework 4.5

disponibles, aplicaciones de este ejemplo la clase de JoinBlock<T1, T2> . Cuando un objeto de


JoinBlock<T1, T2> recibe los datos de todos los orígenes, propaga ese datos a su destino, que
en este ejemplo es un objeto de ActionBlock <TInput> . La lectura de los objetos de
JoinBlock<T1, T2> de un conjunto compartido de Memory Resource se opone.
using System;
using System.Threading;
using System.Threading.Tasks.Dataflow;

// Demonstrates how to use non-greedy join blocks to distribute


// resources among a dataflow network.
class Program
{
// Represents a resource. A derived class might represent
// a limited resource such as a memory, network, or I/O device.
abstract class Resource
{
}

// Represents a memory resource. For brevity, the details of


// this class are omitted.
class MemoryResource : Resource
{
}

// Represents a network resource. For brevity, the details of


// this class are omitted.
class NetworkResource : Resource
{
}

// Represents a file resource. For brevity, the details of


// this class are omitted.
class FileResource : Resource
{
}

static void Main(string[] args)


{
// Create three BufferBlock<T> objects. Each object holds a different
// type of resource.
var networkResources = new BufferBlock<NetworkResource>();
var fileResources = new BufferBlock<FileResource>();
var memoryResources = new BufferBlock<MemoryResource>();
// Create two non-greedy JoinBlock<T1, T2> objects.
// The first join works with network and memory resources;
// the second pool works with file and memory resources.
var joinNetworkAndMemoryResources =
new JoinBlock<NetworkResource, MemoryResource>(
new GroupingDataflowBlockOptions
{
Greedy = false
});
var joinFileAndMemoryResources =
new JoinBlock<FileResource, MemoryResource>(
new GroupingDataflowBlockOptions
{
Greedy = false
});
// Create two ActionBlock<T> objects.
// The first block acts on a network resource and a memory resource.
// The second block acts on a file resource and a memory resource.
var networkMemoryAction =
new ActionBlock<Tuple<NetworkResource, MemoryResource>>(
data =>
{
// Perform some action on the resources.
// Print a message.

MCT: Luis Dueñas Pag 257 de 336


Manual de .NET Framework 4.5

Console.WriteLine("Network worker: using resources...");


// Simulate a lengthy operation that uses the resources.
Thread.Sleep(new Random().Next(500, 2000));
// Print a message.
Console.WriteLine("Network worker: finished using
resources...");
// Release the resources back to their respective pools.
networkResources.Post(data.Item1);
memoryResources.Post(data.Item2);
});
var fileMemoryAction =
new ActionBlock<Tuple<FileResource, MemoryResource>>(
data =>
{
// Perform some action on the resources.
// Print a message.
Console.WriteLine("File worker: using resources...");
// Simulate a lengthy operation that uses the resources.
Thread.Sleep(new Random().Next(500, 2000));
// Print a message.
Console.WriteLine("File worker: finished using resources...");
// Release the resources back to their respective pools.
fileResources.Post(data.Item1);
memoryResources.Post(data.Item2);
});
// Link the resource pools to the JoinBlock<T1, T2> objects.
// Because these join blocks operate in non-greedy mode, they do not
// take the resource from a pool until all resources are available from
// all pools.
networkResources.LinkTo(joinNetworkAndMemoryResources.Target1);
memoryResources.LinkTo(joinNetworkAndMemoryResources.Target2);
fileResources.LinkTo(joinFileAndMemoryResources.Target1);
memoryResources.LinkTo(joinFileAndMemoryResources.Target2);
// Link the JoinBlock<T1, T2> objects to the ActionBlock<T> objects.
joinNetworkAndMemoryResources.LinkTo(networkMemoryAction);
joinFileAndMemoryResources.LinkTo(fileMemoryAction);
// Populate the resource pools. In this example, network and
// file resources are more abundant than memory resources.
networkResources.Post(new NetworkResource());
networkResources.Post(new NetworkResource());
networkResources.Post(new NetworkResource());
memoryResources.Post(new MemoryResource());
fileResources.Post(new FileResource());
fileResources.Post(new FileResource());
fileResources.Post(new FileResource());
// Allow data to flow through the network for several seconds.
Thread.Sleep(10000);
}
}
/* Sample output:
File worker: using resources...
File worker: finished using resources...
Network worker: using resources...
Network worker: finished using resources...
File worker: using resources...
File worker: finished using resources...
Network worker: using resources...
Network worker: finished using resources...
File worker: using resources...
File worker: finished using resources...
File worker: using resources...
File worker: finished using resources...
Network worker: using resources...
Network worker: finished using resources...
Network worker: using resources...
Network worker: finished using resources...
File worker: using resources...
*/

MCT: Luis Dueñas Pag 258 de 336


Manual de .NET Framework 4.5

Para habilitar el uso eficaz de conjunto compartido de objetos de MemoryResource , este


ejemplo especifica un objeto de GroupingDataflowBlockOptions que tiene la propiedad de
Greedy establecida en False para crear los objetos de JoinBlock<T1, T2> que actúan en modo
no expansivo. Un bloque no expansivo join posponer todos los mensajes entrantes hasta que uno
esté disponible de cada origen. Si los mensajes pospuestos cualquiera de los se aceptados por
otro bloque, el bloque de unión reinicie el proceso. Los bloques no expansivos de la unión de los
permisos de modo que comparten uno o más bloques de origen para progresar como los otros
bloques esperan datos. En este ejemplo, si un objeto de MemoryResource se agrega al conjunto
de memoryResources , el primer bloque de unión para recibir el segundo origen de datos puede
progresar. Si este ejemplo es utilizar el modo expansivo, que es el valor predeterminado, un
bloque combinado puede tomar el objeto y espera de MemoryResource para que el segundo
recurso está disponible. Sin embargo, si otro bloque de unión tiene el segundo origen de datos
disponible, no puede progresar porque el objeto de MemoryResource ha realizado por otro
bloque de unión.
3.1.3.10. Cómo: Especificar el grado de paralelismo en un bloque de
flujos de datos
En este documento se describe cómo establecer la propiedad de
ExecutionDataflowBlockOptions. MaxDegreeOfParallelism para permitir que un bloque de
flujo de datos de la ejecución para procesar varios mensajes al mismo tiempo. Esto es útil
cuando se realiza un flujo de datos bloquear que realiza un cálculo de ejecución prolongada y se
puede beneficiar de mensajes de procesamiento en paralelo. Este ejemplo utiliza la clase de
System.Threading.Tasks.Dataflow.ActionBlock<TInput> para realizar varias operaciones de
flujo de datos en paralelo; sin embargo, puede especificar el grado máximo de paralelismo en
tipos predefinidos cualquiera de los del bloque de ejecución que la biblioteca de TPL Dataflow
proporciona, ActionBlock<TInput>,
System.Threading.Tasks.Dataflow.TransformBlock<TInput, TOutput>, y
System.Threading.Tasks.Dataflow.Transform ManyBlock<TInput, TOutput>.
Ejemplo
El ejemplo siguiente realiza dos cálculos de flujo de datos y se imprime el tiempo transcurrido
necesario para cada cálculo. El primer cálculo especifica un grado máximo de paralelismo de 1,
que es el valor predeterminado. Un grado máximo de paralelismo de 1 hace que el bloque de
flujo de datos a los mensajes de proceso en ejecución. El segundo cálculo se parece a primero,
salvo que especifica un grado máximo de paralelismo que sea igual al número de procesadores
disponibles. Esto permite al bloque de flujo de datos para realizar diversas operaciones en
paralelo.
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks.Dataflow;

// Demonstrates how to specify the maximum degree of parallelism


// when using dataflow.
class Program
{
// Performs several computations by using dataflow and returns the elapsed
// time required to perform the computations.
static TimeSpan TimeDataflowComputations(int maxDegreeOfParallelism,
int messageCount)
{
// Create an ActionBlock<int> that performs some work.
var workerBlock = new ActionBlock<int>(
// Simulate work by suspending the current thread.
millisecondsTimeout => Thread.Sleep(millisecondsTimeout),
// Specify a maximum degree of parallelism.
new ExecutionDataflowBlockOptions
{

MCT: Luis Dueñas Pag 259 de 336


Manual de .NET Framework 4.5

MaxDegreeOfParallelism = maxDegreeOfParallelism
});
// Compute the time that it takes for several messages to
// flow through the dataflow block.
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < messageCount; i++)
{
workerBlock.Post(1000);
}
workerBlock.Complete();
// Wait for all messages to propagate through the network.
workerBlock.Completion.Wait();
// Stop the timer and return the elapsed number of milliseconds.
stopwatch.Stop();
return stopwatch.Elapsed;
}
static void Main(string[] args)
{
int processorCount = Environment.ProcessorCount;
int messageCount = processorCount;
// Print the number of processors on this computer.
Console.WriteLine("Processor count = {0}.", processorCount);
TimeSpan elapsed;
// Perform two dataflow computations and print the elapsed
// time required for each.
// This call specifies a maximum degree of parallelism of 1.
// This causes the dataflow block to process messages serially.
elapsed = TimeDataflowComputations(1, messageCount);
Console.WriteLine("Degree of parallelism = {0}; message count = {1}; " +
"elapsed time = {2}ms.", 1, messageCount,
(int)elapsed.TotalMilliseconds);
// Perform the computations again. This time, specify the number of
// processors as the maximum degree of parallelism. This causes
// multiple messages to be processed in parallel.
elapsed = TimeDataflowComputations(processorCount, messageCount);
Console.WriteLine("Degree of parallelism = {0}; message count = {1}; " +
"elapsed time = {2}ms.", processorCount, messageCount,
(int)elapsed.TotalMilliseconds);
}
}
/* Sample output:
Processor count = 4.
Degree of parallelism = 1; message count = 4; elapsed time = 4032ms.
Degree of parallelism = 4; message count = 4; elapsed time = 1001ms.
*/

3.1.3.11. Cómo: Especificar un programador de tareas en un bloque de


flujos de datos
En este documento se muestra cómo asociar un programador de tareas específico cuando utilice
flujo de datos. El ejemplo utiliza la clase de
System.Threading.Tasks.ConcurrentExclusiveSchedulerPair en una aplicación de Windows
Forms para mostrar cuando las tareas de lector están activos y cuando una tarea del programador
está activa. También usa el método de TaskScheduler.FromCurrentSynchronization Context
para habilitar un flujo de datos bloqueado para ejecutarse en el subproceso de interfaz de
usuario.

Para crear la aplicación de Windows Forms


1. Cree Visual C# o un proyecto de Visual Basic Aplicación de Windows Forms . En los
siguientes pasos, el proyecto se denomina WriterReadersWinForms.
2. En el diseñador de formularios para el formulario principal, Form1.cs (Form1.vb para
Visual Basic), agregue cuatro controles de CheckBox . Establezca la propiedad de Text
a Lector 1 para checkBox1, a Lector 2 para checkBox2, a Lector 3 para checkBox3, y

MCT: Luis Dueñas Pag 260 de 336


Manual de .NET Framework 4.5

el programador para checkBox4. Establezca la propiedad de Enabled para cada control


a False.
3. Agregue un control Timer al formulario. Establezca la propiedad Interval en 2500.
Funcionalidad de flujo de datos de suma
En esta sección se describe cómo crear los bloques de flujo de datos que participan en la
aplicación y cómo asociar cada uno con un programador de tareas.

Para agregar la funcionalidad de flujo de datos a la aplicación


1. En el proyecto, agregue una referencia a System.Threading.Tasks.Dataflow.dll.
2. Asegúrese de que Form1.cs (Form1.vb para Visual Basic) contiene los siguientes
extractos de using (Imports en Visual Basic).
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;

3. Agregue un miembro de datos de BroadcastBlock<T> a la clase de Form1 .


// Broadcasts values to an ActionBlock<int> object that is associated
// with each check box.
BroadcastBlock<int> broadcaster = new BroadcastBlock<int>(null);

4. En el constructor de Form1 , después de la llamada a InitializeComponent, cree un


objeto de ActionBlock<TInput> que alterna el estado de los objetos de CheckBox .
// Create an ActionBlock<CheckBox> object that toggles the state
// of CheckBox objects.
// Specifying the current synchronization context enables the
// action to run on the user-interface thread.
var toggleCheckBox = new ActionBlock<CheckBox>(checkBox =>
{
checkBox.Checked = !checkBox.Checked;
},
new ExecutionDataflowBlockOptions
{
TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext()
});

5. En el constructor de Form1 , cree un objeto y cuatro objetos de ActionBlock<TInput> ,


un objeto de ConcurrentExclusiveSchedulerPair de ActionBlock<TInput> para cada
objeto de CheckBox . Para cada objeto de ActionBlock<TInput> , especifique un objeto
de ExecutionDataflowBlock Options que tiene la propiedad de TaskScheduler
establecida en la propiedad de Concurrent Scheduler para los lectores, y la propiedad de
ExclusiveScheduler para el programador.
// Create a ConcurrentExclusiveSchedulerPair object.
// Readers will run on the concurrent part of the scheduler pair.
// The writer will run on the exclusive part of the scheduler pair.
var taskSchedulerPair = new ConcurrentExclusiveSchedulerPair();
// Create an ActionBlock<int> object for each reader CheckBox object.
// Each ActionBlock<int> object represents an action that can read
// from a resource in parallel to other readers.
// Specifying the concurrent part of the scheduler pair enables the
// reader to run in parallel to other actions that are managed by
// that scheduler.
var readerActions =
from checkBox in new CheckBox[] {checkBox1, checkBox2, checkBox3}
select new ActionBlock<int>(milliseconds =>
{
// Toggle the check box to the checked state.
toggleCheckBox.Post(checkBox);

MCT: Luis Dueñas Pag 261 de 336


Manual de .NET Framework 4.5

// Perform the read action. For demonstration, suspend the current


// thread to simulate a lengthy read operation.
Thread.Sleep(milliseconds);
// Toggle the check box to the unchecked state.
toggleCheckBox.Post(checkBox);
},
new ExecutionDataflowBlockOptions
{
TaskScheduler = taskSchedulerPair.ConcurrentScheduler
});
// Create an ActionBlock<int> object for the writer CheckBox object.
// This ActionBlock<int> object represents an action that writes to
// a resource, but cannot run in parallel to readers.
// Specifying the exclusive part of the scheduler pair enables the
// writer to run in exclusively with respect to other actions that are
// managed by the scheduler pair.
var writerAction = new ActionBlock<int>(milliseconds =>
{
// Toggle the check box to the checked state.
toggleCheckBox.Post(checkBox4);
// Perform the write action. For demonstration, suspend the current
// thread to simulate a lengthy write operation.
Thread.Sleep(milliseconds);
// Toggle the check box to the unchecked state.
toggleCheckBox.Post(checkBox4);
},
new ExecutionDataflowBlockOptions
{
TaskScheduler = taskSchedulerPair.ExclusiveScheduler
});

// Link the broadcaster to each reader and writer block.


// The BroadcastBlock<T> class propagates values that it
// receives to all connected targets.
foreach (var readerAction in readerActions)
{
broadcaster.LinkTo(readerAction);
}
broadcaster.LinkTo(writerAction);

6. En el constructor de Form1 , inicie el objeto de Timer .


// Start the timer.
timer1.Start();

7. En el diseñador de formularios para el formulario principal, cree un controlador de


eventos para el evento Tick para el temporizador.
8. Implemente el evento de Tick para el temporizador.
// Event handler for the timer.
private void timer1_Tick(object sender, EventArgs e)
{
// Post a value to the broadcaster. The broadcaster
// sends this message to each target.
broadcaster.Post(1000);
}

Dado que el bloque de flujo de datos de toggleCheckBox actúa en la interfaz de usuario, es


importante que esta acción aparece en el subproceso de interfaz de usuario. Para ello, durante la
construcción este objeto proporciona un objeto de ExecutionDataflowBlockOptions que tiene la
propiedad de TaskScheduler establecida en
TaskScheduler.FromCurrentSynchronizationContext. El método de
FromCurrentSynchronizationContext crea un objeto de TaskScheduler que realice el trabajo en
el contexto de sincronización. Dado que se llama al constructor de Form1 el subproceso, la
acción para ejecuciones también bloqueados de flujo de datos toggleCheckBox en el
subproceso de interfaz de usuario.

MCT: Luis Dueñas Pag 262 de 336


Manual de .NET Framework 4.5

Este ejemplo también utiliza la clase de ConcurrentExclusiveSchedulerPair para permitir a


algunos bloques de flujo de datos para representar simultáneamente, y otro bloque de flujo de
datos para representar exclusiva con respecto al resto de los bloques de flujo de datos que se
ejecutan en el mismo objeto de ConcurrentExclusiveSchedulerPair . Esta técnica es útil cuando
los varios bloques de flujo de datos comparten un recurso y algunos requieren acceso exclusivo
a ese recurso, porque elimina el requisito para sincronizar manualmente el acceso a ese recurso.
La eliminación de sincronización manual puede hacer que el código más eficaz.
Ejemplo
El ejemplo siguiente se muestra el código completo de Form1.cs (Form1.vb para Visual Basic).
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;

namespace WriterReadersWinForms
{
public partial class Form1 : Form
{
// Broadcasts values to an ActionBlock<int> object that is associated
// with each check box.
BroadcastBlock<int> broadcaster = new BroadcastBlock<int>(null);

public Form1()
{
InitializeComponent();
// Create an ActionBlock<CheckBox> object that toggles the state
// of CheckBox objects.
// Specifying the current synchronization context enables the
// action to run on the user-interface thread.
var toggleCheckBox = new ActionBlock<CheckBox>(checkBox =>
{
checkBox.Checked = !checkBox.Checked;
},
new ExecutionDataflowBlockOptions
{
TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext()
});
// Create a ConcurrentExclusiveSchedulerPair object.
// Readers will run on the concurrent part of the scheduler pair.
// The writer will run on the exclusive part of the scheduler pair.
var taskSchedulerPair = new ConcurrentExclusiveSchedulerPair();
// Create an ActionBlock<int> object for each reader CheckBox object.
// Each ActionBlock<int> object represents an action that can read
// from a resource in parallel to other readers.
// Specifying the concurrent part of the scheduler pair enables the
// reader to run in parallel to other actions that are managed by
// that scheduler.
var readerActions =
from checkBox in new CheckBox[] {checkBox1, checkBox2, checkBox3}
select new ActionBlock<int>(milliseconds =>
{
// Toggle the check box to the checked state.
toggleCheckBox.Post(checkBox);
// Perform the read action. For demonstration, suspend the
current
// thread to simulate a lengthy read operation.
Thread.Sleep(milliseconds);
// Toggle the check box to the unchecked state.
toggleCheckBox.Post(checkBox);
},
new ExecutionDataflowBlockOptions
{

MCT: Luis Dueñas Pag 263 de 336


Manual de .NET Framework 4.5

TaskScheduler = taskSchedulerPair.ConcurrentScheduler
});
// Create an ActionBlock<int> object for the writer CheckBox object.
// This ActionBlock<int> object represents an action that writes to
// a resource, but cannot run in parallel to readers.
// Specifying the exclusive part of the scheduler pair enables the
// writer to run in exclusively with respect to other actions that
are
// managed by the scheduler pair.
var writerAction = new ActionBlock<int>(milliseconds =>
{
// Toggle the check box to the checked state.
toggleCheckBox.Post(checkBox4);
// Perform the write action. For demonstration, suspend the
current
// thread to simulate a lengthy write operation.
Thread.Sleep(milliseconds);
// Toggle the check box to the unchecked state.
toggleCheckBox.Post(checkBox4);
},
new ExecutionDataflowBlockOptions
{
TaskScheduler = taskSchedulerPair.ExclusiveScheduler
});

// Link the broadcaster to each reader and writer block.


// The BroadcastBlock<T> class propagates values that it
// receives to all connected targets.
foreach (var readerAction in readerActions)
{
broadcaster.LinkTo(readerAction);
}
broadcaster.LinkTo(writerAction);
// Start the timer.
timer1.Start();
}
// Event handler for the timer.
private void timer1_Tick(object sender, EventArgs e)
{
// Post a value to the broadcaster. The broadcaster
// sends this message to each target.
broadcaster.Post(1000);
}
}
}

3.1.3.12. Tutorial: Usar BatchBlock y BatchedJoinBlock para mejorar


la eficacia
La biblioteca de TPL Dataflow proporciona clases de
System.Threading.Tasks.Dataflow.BatchBlock<T> y de
System.Threading.Tasks.Dataflow.BatchedJoinBlock<T1, T2> para poder recibir y
almacenamiento en búfer datos de uno o más orígenes y después propagar out esos datos
almacenados en búfer como una colección. Este mecanismo de procesamiento por lotes es útil
cuando se recopilan datos de uno o más orígenes y después procesa datos como un lote. Por
ejemplo, considere una aplicación que utilice flujo de datos para insertar registros en una base
de datos. Esta operación puede ser más eficaz si varios elementos se insertan al mismo tiempo
en lugar de uno a la vez secuencialmente. Este documento se describe cómo utilizar la clase de
BatchBlock<T> para mejorar la eficacia de operaciones de inserción de la base de datos.
También describe cómo utilizar la clase de BatchedJoinBlock<T1, T2> para capturar los
resultados y cualquier excepción que se produce cuando el programa lee de una base de datos.
Importante
En algunas versiones de Windows, no puede conectarse a Northwind.sdf si Visual

MCT: Luis Dueñas Pag 264 de 336


Manual de .NET Framework 4.5

Studio se está ejecutando en modo de usuario no administrador. Para conectarse a


Northwind.sdf, inicie Visual Studio o un símbolo del sistema de Visual Studio en modo
de Ejecutar como administrador .

Para crear la aplicación de consola


1. En Visual Studio, cree un proyecto de Visual C# o Visual Basic Aplicación de consola .
En este documento, el proyecto se denomina DataflowBatchDatabase.
2. En el proyecto, agregue una referencia a System.Data.SqlServerCe.dll y una referencia
a System.Threading.Tasks.Dataflow.dll.
3. Asegúrese de que Form1.cs (Form1.vb para Visual Basic) contiene los siguientes
extractos de using (Imports en Visual Basic).
using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks.Dataflow;

4. Agregue los siguientes miembros de datos a la clase de Program .


// The number of employees to add to the database.
// TODO: Change this value to experiment with different numbers of
// employees to insert into the database.
static readonly int insertCount = 256;
// The size of a single batch of employees to add to the database.
// TODO: Change this value to experiment with different batch sizes.
static readonly int insertBatchSize = 96;
// The source database file.
// TODO: Change this value if Northwind.sdf is at a different location
// on your computer.
static readonly string sourceDatabase =
@"C:\Program Files\Microsoft SQL Server Compact
Edition\v3.5\Samples\Northwind.sdf";
// TODO: Change this value if you require a different temporary
location.
static readonly string scratchDatabase =
@"C:\Temp\Northwind.sdf";

Definición de la clase employee


Agregue a la clase de Program la clase de Employee .
// Describes an employee. Each property maps to a
// column in the Employees table in the Northwind database.
// For brevity, the Employee class does not contain
// all columns from the Employees table.
class Employee
{
public int EmployeeID { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
// A random number generator that helps tp generate
// Employee property values.
static Random rand = new Random(42);
// Possible random first names.
static readonly string[] firstNames = { "Tom", "Mike", "Ruth", "Bob",
"John" };
// Possible random last names.
static readonly string[] lastNames = { "Jones", "Smith", "Johnson",
"Walker" };
// Creates an Employee object that contains random
// property values.
public static Employee Random()
{

MCT: Luis Dueñas Pag 265 de 336


Manual de .NET Framework 4.5

return new Employee


{
EmployeeID = -1,
LastName = lastNames[rand.Next() % lastNames.Length],
FirstName = firstNames[rand.Next() % firstNames.Length]
};
}
}

La clase de Employee contiene tres propiedades, EmployeeID, LastName, y FirstName. Estas


propiedades corresponden a Employee ID, a Last Name, y las columnas de First Name en la
tabla de Employees en la base de datos Northwind. Para esta demostración, la clase de
Employee también define el método de Random , que crea un objeto de Em