Sie sind auf Seite 1von 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 Employee con valores
aleatorios para sus propiedades.
Definición de operaciones de base de datos de empleados
Agregue a la clase de ProgramInsertEmployees, GetEmployeeCount, y los métodos de
GetEmployeeID.
// Adds new employee records to the database.
static void InsertEmployees(Employee[] employees, string connectionString)
{
using (SqlCeConnection connection = new SqlCeConnection(connectionString))
{
try
{
// Create the SQL command.
SqlCeCommand command = new SqlCeCommand(
"INSERT INTO Employees ([Last Name], [First Name])" +
"VALUES (@lastName, @firstName)", connection);
connection.Open();
for (int i = 0; i < employees.Length; i++)
{
// Set parameters.
command.Parameters.Clear();
command.Parameters.Add("@lastName", employees[i].LastName);
command.Parameters.Add("@firstName", employees[i].FirstName);
// Execute the command.
command.ExecuteNonQuery();
}
}
finally
{
connection.Close();
}
}
}
// Retrieves the number of entries in the Employees table in
// the Northwind database.
static int GetEmployeeCount(string connectionString)
{
int result = 0;
using (SqlCeConnection sqlConnection = new
SqlCeConnection(connectionString))
{
SqlCeCommand sqlCommand = new SqlCeCommand(
"SELECT COUNT(*) FROM Employees", sqlConnection);
sqlConnection.Open();
try
{
result = (int)sqlCommand.ExecuteScalar();
}
finally
{
sqlConnection.Close();
}
}

MCT: Luis Dueñas Pag 266 de 336


Manual de .NET Framework 4.5

return result;
}
// Retrieves the ID of the first employee that has the provided name.
static int GetEmployeeID(string lastName, string firstName, string
connectionString)
{
using (SqlCeConnection connection = new SqlCeConnection(connectionString))
{
SqlCeCommand command = new SqlCeCommand(
string.Format("SELECT [Employee ID] FROM Employees " +
"WHERE [Last Name] = '{0}' AND [First Name] = '{1}'",lastName,
firstName),
connection);
connection.Open();
try
{
return (int)command.ExecuteScalar();
}
finally
{
connection.Close();
}
}
}

El método de InsertEmployees agrega nuevos registros de empleados en la base de datos. El


método de GetEmployeeCount recupera el número de entradas de la tabla de Employees . El
método de GetEmployeeID recupera el identificador del primer empleado que tiene el nombre
proporcionado. Cada uno de estos métodos lleva una cadena de conexión la base de datos
Northwind y utiliza la funcionalidad del espacio de nombres System.Data.SqlServerCe para
comunicarse con la base de datos.
Datos de empleados al almacenamiento en búfer de Sin utilizar la base de datos
Agregue la clase de Program a los métodos de AddEmployees y de PostRandomEmployees .
// Posts random Employee data to the provided target block.
static void PostRandomEmployees(ITargetBlock<Employee> target, int count)
{
Console.WriteLine("Adding {0} entries to Employee table...", count);
for (int i = 0; i < count; i++)
{
target.Post(Employee.Random());
}
}
// Adds random employee data to the database by using dataflow.
static void AddEmployees(string connectionString, int count)
{
// Create an ActionBlock<Employee> object that adds a single
// employee entry to the database.
var insertEmployee = new ActionBlock<Employee>(e =>
InsertEmployees(new Employee[] { e }, connectionString));
// Post several random Employee objects to the dataflow block.
PostRandomEmployees(insertEmployee, count);
// Set the dataflow block to the completed state and wait for
// all insert operations to complete.
insertEmployee.Complete();
insertEmployee.Completion.Wait();
}

El método de AddEmployees agrega datos aleatorios employee en la base de datos con el flujo
de datos. Crea un objeto de ActionBlock<TInput> que llame al método de InsertEmployees
para agregar una entrada de empleados en la base de datos. El método de AddEmployees llama
al método de PostRandomEmployees para enviar varios objetos de Employee al objeto de
ActionBlock<TInput> . El método de AddEmployees esperar todas las operaciones de
inserción finalice.

MCT: Luis Dueñas Pag 267 de 336


Manual de .NET Framework 4.5

Mediante el almacenamiento en búfer para agregar datos de empleados en la base de datos


Agregue a la clase de Program el método de AddEmployeesBatched .
// Adds random employee data to the database by using dataflow.
// This method is similar to AddEmployees except that it uses batching
// to add multiple employees to the database at a time.
static void AddEmployeesBatched(string connectionString, int batchSize,
int count)
{
// Create a BatchBlock<Employee> that holds several Employee objects and
// then propagates them out as an array.
var batchEmployees = new BatchBlock<Employee>(batchSize);
// Create an ActionBlock<Employee[]> object that adds multiple
// employee entries to the database.
var insertEmployees = new ActionBlock<Employee[]>(a =>
InsertEmployees(a, connectionString));
// Link the batch block to the action block.
batchEmployees.LinkTo(insertEmployees);
// When the batch block completes, set the action block also to complete.
batchEmployees.Completion.ContinueWith(delegate {
insertEmployees.Complete(); });
// Post several random Employee objects to the batch block.
PostRandomEmployees(batchEmployees, count);
// Set the batch block to the completed state and wait for
// all insert operations to complete.
batchEmployees.Complete();
insertEmployees.Completion.Wait();
}

Este método es similar a AddEmployees, excepto en que también utiliza la clase de


BatchBlock<T> para almacenar en búfer varios objetos de Employee antes de enviar esos
objetos al objeto de ActionBlock<TInput> . Dado que la clase de BatchBlock<T> propaga
varios elementos como una colección, el objeto de ActionBlock<TInput> se modifica para
representar en una matriz de los objetos de Employee . Como en el método de AddEmployees ,
AddEmployeesBatched llama al método de PostRandomEmployees para enviar varios
objetos de Employee ; sin embargo, AddEmployeesBatched envía estos objetos al objeto de
BatchBlock<T> . El método de AddEmployeesBatched también espera todas las operaciones
de inserción finalice.
Usar una combinación almacenado en búfer para leer datos de los empleados de la base de
datos
Agregue a la clase de Program el método de GetRandomEmployees .
Este método imprime información sobre empleados aleatorios en la consola. Crea varios objetos
random de Employee y llama al método de GetEmployeeID para recuperar el identificador
único para cada objeto. Dado que el método de GetEmployeeID produce una excepción si no
hay ningún empleado coincidente con con nombres y apellidos, el método de
GetRandomEmployees utiliza la clase de BatchedJoinBlock<T1, T2> para almacenar objetos
de Employee para las llamadas correctas a GetEmployeeID y los objetos de System.Exception
para las llamadas que el error. El objeto de ActionBlock<TInput> en este ejemplo actúa en un
objeto de Tuple<T1, T2> que contiene una lista de objetos de Employee y una lista de objetos
de Exception . El objeto de BatchedJoinBlock<T1, T2> propaga estos datos a la suma de
Employee y object recibidos de Exception cuenta igual al tamaño de lote.
Ejemplo completo
El ejemplo siguiente muestra el código completo: El método de Main compara el tiempo
necesario para realizar inserciones por lotes de la base de datos contra la hora de realizar
inserciones no por lotes de la base de datos. También muestra el uso de unión almacenado en
búfer para los datos de los empleados de la base de datos y también señala errores.
using System;

MCT: Luis Dueñas Pag 268 de 336


Manual de .NET Framework 4.5

using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks.Dataflow;

// Demonstrates how to use batched dataflow blocks to improve


// the performance of database operations.
namespace DataflowBatchDatabase
{
class 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";
// 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()
{
return new Employee
{
EmployeeID = -1,
LastName = lastNames[rand.Next() % lastNames.Length],
FirstName = firstNames[rand.Next() % firstNames.Length]
};
}
}

// Adds new employee records to the database.


static void InsertEmployees(Employee[] employees, string
connectionString)
{
using (SqlCeConnection connection =
new SqlCeConnection(connectionString))
{
try

MCT: Luis Dueñas Pag 269 de 336


Manual de .NET Framework 4.5

{
// Create the SQL command.
SqlCeCommand command = new SqlCeCommand(
"INSERT INTO Employees ([Last Name], [First Name])" +
"VALUES (@lastName, @firstName)",connection);
connection.Open();
for (int i = 0; i < employees.Length; i++)
{
// Set parameters.
command.Parameters.Clear();
command.Parameters.Add("@lastName", employees[i].LastName);
command.Parameters.Add("@firstName",
employees[i].FirstName);
// Execute the command.
command.ExecuteNonQuery();
}
}
finally
{
connection.Close();
}
}
}

// Retrieves the number of entries in the Employees table in the


Northwind DB.
static int GetEmployeeCount(string connectionString)
{
int result = 0;
using (SqlCeConnection sqlConnection = new
SqlCeConnection(connectionString))
{
SqlCeCommand sqlCommand = new SqlCeCommand(
"SELECT COUNT(*) FROM Employees", sqlConnection);
sqlConnection.Open();
try
{
result = (int)sqlCommand.ExecuteScalar();
}
finally
{
sqlConnection.Close();
}
}
return result;
}

// Retrieves the ID of the first employee that has the provided name.
static int GetEmployeeID(string lastName, string firstName,
string connectionString)
{
using (SqlCeConnection connection = new
SqlCeConnection(connectionString))
{
SqlCeCommand command = new SqlCeCommand(
string.Format("SELECT [Employee ID] FROM Employees " +
"WHERE [Last Name] = '{0}' AND [First Name] = '{1}'",
lastName, firstName), connection);
connection.Open();
try
{
return (int)command.ExecuteScalar();
}
finally
{
connection.Close();
}
}

MCT: Luis Dueñas Pag 270 de 336


Manual de .NET Framework 4.5

// Posts random Employee data to the provided target block.


static void PostRandomEmployees(ITargetBlock<Employee> target, int
count)
{
Console.WriteLine("Adding {0} entries to Employee table...", count);
for (int i = 0; i < count; i++)
{
target.Post(Employee.Random());
}
}

// Adds random employee data to the database by using dataflow.


static void AddEmployees(string connectionString, int count)
{
// Create an ActionBlock<Employee> object that adds a single
// employee entry to the database.
var insertEmployee = new ActionBlock<Employee>(e =>
InsertEmployees(new Employee[] { e }, connectionString));
// Post several random Employee objects to the dataflow block.
PostRandomEmployees(insertEmployee, count);
// Set the dataflow block to the completed state and wait for
// all insert operations to complete.
insertEmployee.Complete();
insertEmployee.Completion.Wait();
}

// Adds random employee data to the database by using dataflow.


// This method is similar to AddEmployees except that it uses batching
// to add multiple employees to the database at a time.
static void AddEmployeesBatched(string connectionString, int batchSize,
int count)
{
// Create a BatchBlock<Employee> that holds several Employee objects
and
// then propagates them out as an array.
var batchEmployees = new BatchBlock<Employee>(batchSize);
// Create an ActionBlock<Employee[]> object that adds multiple
// employee entries to the database.
var insertEmployees = new ActionBlock<Employee[]>(a =>
InsertEmployees(a, connectionString));
// Link the batch block to the action block.
batchEmployees.LinkTo(insertEmployees);
// When the batch block completes, set the action block also to
complete.

batchEmployees.Completion.ContinueWith(delegate{insertEmployees.Complete();
});
// Post several random Employee objects to the batch block.
PostRandomEmployees(batchEmployees, count);
// Set the batch block to the completed state and wait for
// all insert operations to complete.
batchEmployees.Complete();
insertEmployees.Completion.Wait();
}

// Prints information about several random employees to the console.


static void GetRandomEmployees(string connectionString, int batchSize,
int count)
{
// Create a BatchedJoinBlock<Employee, Exception> object that holds
// both employee and exception data.
var selectEmployees = new BatchedJoinBlock<Employee,
Exception>(batchSize);
// Holds the total number of exceptions that occurred.
int totalErrors = 0;
// Create an action block that prints employee and error information

MCT: Luis Dueñas Pag 271 de 336


Manual de .NET Framework 4.5

// to the console.
var printEmployees =
new ActionBlock<Tuple<IList<Employee>, IList<Exception>>>(data =>
{
// Print information about the employees in this batch.
Console.WriteLine("Received a batch...");
foreach (Employee e in data.Item1)
{
Console.WriteLine("Last={0} First={1} ID={2}",
e.FirstName, e.LastName, e.EmployeeID);
}
// Print the error count for this batch.
Console.WriteLine("There were {0} errors in this batch...",
data.Item2.Count);
// Update total error count.
totalErrors += data.Item2.Count;
});
// Link the batched join block to the action block.
selectEmployees.LinkTo(printEmployees);
// When the batched join block completes, set the action block also to
complete

selectEmployees.Completion.ContinueWith(delegate{printEmployees.Complete();});
// Try to retrieve the ID for several random employees.
Console.WriteLine("Selecting random entries from Employees
table...");
for (int i = 0; i < count; i++)
{
try
{
// Create a random employee.
Employee e = Employee.Random();
// Try to retrieve the ID for the employee from the database.
e.EmployeeID = GetEmployeeID(e.LastName, e.FirstName,
connectionString);
// Post the Employee object to the Employee target of
// the batched join block.
selectEmployees.Target1.Post(e);
}
catch (NullReferenceException e)
{
// GetEmployeeID throws NullReferenceException when there is
// no such employee with the given name. When this happens,
// post the Exception object to the Exception target of
// the batched join block.
selectEmployees.Target2.Post(e);
}
}
// Set the batched join block to the completed state and wait for
// all retrieval operations to complete.
selectEmployees.Complete();
printEmployees.Completion.Wait();
// Print the total error count.
Console.WriteLine("Finished. There were {0} total errors.",
totalErrors);
}

static void Main(string[] args)


{
// Create a connection string for accessing the database.
// The connection string refers to the temporary database location.
string connectionString = string.Format(@"Data Source={0}",
scratchDatabase);
// Create a Stopwatch object to time database insert operations.
Stopwatch stopwatch = new Stopwatch();
// Start with a clean database file by copying the source database to
// the temporary location.
File.Copy(sourceDatabase, scratchDatabase, true);

MCT: Luis Dueñas Pag 272 de 336


Manual de .NET Framework 4.5

// Demonstrate multiple insert operations without batching.


Console.WriteLine("Demonstrating non-batched database insert
operations...");
Console.WriteLine("Original size of Employee table: {0}.",
GetEmployeeCount(connectionString));
stopwatch.Start();
AddEmployees(connectionString, insertCount);
stopwatch.Stop();
Console.WriteLine("New size of Employee table: {0}; elapsed insert time:
{1} ms.",
GetEmployeeCount(connectionString),
stopwatch.ElapsedMilliseconds);
Console.WriteLine();
// Start again with a clean database file.
File.Copy(sourceDatabase, scratchDatabase, true);
// Demonstrate multiple insert operations, this time with batching.
Console.WriteLine("Demonstrating batched database insert
operations...");
Console.WriteLine("Original size of Employee table: {0}.",
GetEmployeeCount(connectionString));
stopwatch.Restart();
AddEmployeesBatched(connectionString, insertBatchSize, insertCount);
stopwatch.Stop();
Console.WriteLine("New size of Employee table: {0}; elapsed insert time:
{1} ms.",
GetEmployeeCount(connectionString),
stopwatch.ElapsedMilliseconds);
Console.WriteLine();
// Start again with a clean database file.
File.Copy(sourceDatabase, scratchDatabase, true);
// Demonstrate multiple retrieval operations with error reporting.
Console.WriteLine("Demonstrating batched join database select
operations...");
// Add a small number of employees to the database.
AddEmployeesBatched(connectionString, insertBatchSize, 16);
// Query for random employees.
GetRandomEmployees(connectionString, insertBatchSize, 10);
}
}
}
/* Sample output:
Demonstrating non-batched database insert operations...
Original size of Employee table: 15.
Adding 256 entries to Employee table...
New size of Employee table: 271; elapsed insert time: 11035 ms.

Demonstrating batched database insert operations...


Original size of Employee table: 15.
Adding 256 entries to Employee table...
New size of Employee table: 271; elapsed insert time: 197 ms.

Demonstrating batched join database insert operations...


Adding 16 entries to Employee table...
Selecting items from Employee table...
Received a batch...
Last=Tom First=Jones ID=21
Last=John First=Jones ID=24
Last=Tom First=Smith ID=26
Last=Tom First=Jones ID=21
There were 4 errors in this batch...
Received a batch...
Last=Tom First=Smith ID=26
Last=Mike First=Jones ID=28
There were 0 errors in this batch...
Finished. There were 4 total errors.
*/

3.1.4. TPL con otros modelos asincrónicos

MCT: Luis Dueñas Pag 273 de 336


Manual de .NET Framework 4.5

La biblioteca TPL se puede utilizar con los modelos asincrónicos tradicionales de programación
de .NET Framework de maneras diferentes.
3.1.4.1. TPL y la programación asincrónica tradicional de .NET
.NET Framework proporciona los siguientes dos modelos estándar para realizar las operaciones
asincrónicas enlazadas a E/S y enlazadas a cálculos:
 Modelo de programación asincrónica (APM), en el que las operaciones asincrónicas se
representan mediante un par de métodos Begin/End como FileStream.BeginRead y
Stream. EndRead.
 Modelo asincrónico basado en eventos (EAP), en el que las operaciones asincrónicas se
representan mediante un par método-evento que se denomina OperationNameAsync y
OperationNameCompleted, por ejemplo, WebClient.DownloadStringAsync y
WebClient. DownloadStringCompleted. (EAP apareció por primera vez en .NET
Framework versión 2.0).
La biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas)
se puede usar de varias maneras junto con cualquiera de los modelos asincrónicos. Puede
exponer las operaciones de APM y EAP como tareas a los consumidores de la biblioteca o
puede exponer los modelos de APM, pero usar objetos de tarea para implementarlos
internamente. En ambos escenarios, al usar los objetos de tarea, puede simplificar el código y
aprovechar la siguiente funcionalidad útil:
 Registre las devoluciones de llamada, en el formulario de continuaciones de la tarea, en
cualquier momento después de que se haya iniciado la tarea.
 Coordine varias operaciones que se ejecutan en respuesta a un método Begin_,
mediante los métodos ContinueWhenAny, ContinueWhenAll, WaitAll o WaitAny.
 Encapsule las operaciones asincrónicas enlazadas a E/S y enlazadas a cálculos en el
mismo objeto de tarea.
 Supervise el estado del objeto de tarea.
 Calcule las referencias del estado una operación para un objeto de tarea mediante
TaskCompletionSource<TResult>.
Ajustar las operaciones de APM en una tarea
Las clases System.Threading.Tasks.TaskFactory y System.Threading.Tasks.TaskFactory
<TResult> proporcionan varias sobrecargas de los métodosFromAsync yFromAsyncque
permiten encapsular un par de métodos Begin/End en una instancia de Task o de
Task<TResult>. Las diversas sobrecargas hospedan cualquier par de métodos de Begin/End que
tenga entre cero y tres parámetros de entrada.
Para los pares que tienen métodos End que devuelven un valor (Function en Visual Basic), use
los métodos de TaskFactory<TResult>, que crean un objeto Task<TResult>. Para los métodos
End que devuelven un valor void (Sub en Visual Basic), use los métodos de TaskFactory, que
crean un objeto Task.
En los pocos casos en los que el método Begin tiene más de tres parámetros o contiene
parámetros out o ref, se proporcionan las sobrecargas FromAsync adicionales que encapsulan
sólo el método End.
En el ejemplo de código siguiente se muestra la signatura para la sobrecarga FromAsync que
coincide con los métodos FileStream.BeginRead y FileStream.EndRead. Esta sobrecarga toma
los tres parámetros de entrada siguientes.
public Task<TResult> FromAsync<TArg1, TArg2, TArg3>(
Func<TArg1, TArg2, TArg3, AsyncCallback, object, IAsyncResult>
beginMethod, //BeginRead
Func<IAsyncResult, TResult> endMethod, //EndRead
TArg1 arg1, // the byte[] buffer
TArg2 arg2, // the offset in arg1 at which to start writing data
TArg3 arg3, // the maximum number of bytes to read

MCT: Luis Dueñas Pag 274 de 336


Manual de .NET Framework 4.5

object state // optional state information


)

El primer parámetro es un delegado Func<T1, T2, T3, T4, T5, TResult> que coincide con la
signatura del método FileStream.BeginRead. El segundo parámetro es un delegado Func<T,
TResult> que toma una interfaz IAsyncResult y devuelve TResult. Dado que EndRead devuelve
un entero, el compilador deduce el tipo de TResult como Int32 y el tipo de la tarea como Task.
Los últimos cuatro parámetros son idénticos a los del método FileStream.BeginRead:
 Búfer donde se van a almacenar los datos de archivo.
 Desplazamiento en el búfer donde deben comenzar a escribirse los datos.
 Cantidad máxima de datos que se van a leer del archivo.
 Un objeto opcional que almacena los datos de estado definidos por el usuario que se van
a pasar a la devolución de llamada.
Usar ContinueWith para la funcionalidad de devolución de llamada
Si necesita obtener acceso a los datos del archivo, en contraposición a solo el número de bytes,
el método FromAsync no es suficiente. En su ligar, use Task, cuya propiedad Result contiene
los datos de archivo. Puede hacer si agrega una continuación a la tarea original. La continuación
realiza el trabajo que normalmente realizaría el delegado AsyncCallback. Se invoca cuando se
completa el antecedente y se ha rellenado el búfer de datos. (El objeto FileStream se debería
cerrar antes de devolver un valor).
En el siguiente ejemplo se muestra cómo devolver un objeto Task que encapsula el par
BeginRead/ EndRead de la clase FileStream.
const int MAX_FILE_SIZE = 14000000;
public static Task<string> GetFileStringAsync(string path)
{
FileInfo fi = new FileInfo(path);
byte[] data = null;
data = new byte[fi.Length];
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read,
FileShare.Read, data.Length, true);
//Task<int> returns the number of bytes read
Task<int> task = Task<int>.Factory.FromAsync(
fs.BeginRead, fs.EndRead, data, 0, data.Length, null);
// It is possible to do other work here while waiting
// for the antecedent task to complete.
// Add the continuation, which returns a Task<string>.
return task.ContinueWith((antecedent) =>
{
fs.Close();
// Result = "number of bytes read" (if we need it.)
if (antecedent.Result < 100)
{
return "Data is too small to bother with.";
}
else
{
// If we did not receive the entire file, the end of the
// data buffer will contain garbage.
if (antecedent.Result < data.Length)
Array.Resize(ref data, antecedent.Result);
// Will be returned in the Result property of the Task<string>
// at some future point after the asynchronous file I/O operation
completes.
return new UTF8Encoding().GetString(data);
}
});
}

A continuación, se puede llamar al método de la forma siguiente.


Task<string> t = GetFileStringAsync(path);

MCT: Luis Dueñas Pag 275 de 336


Manual de .NET Framework 4.5

// Do some other work:


try
{
Console.WriteLine(t.Result.Substring(0, 500));
}
catch (AggregateException ae)
{
Console.WriteLine(ae.InnerException.Message);
}

Proporcionar los datos de estado personalizados


En las operaciones IAsyncResult típicas, si el delegado AsyncCallback requiere algún dato de
estado personalizado, tiene que pasarlo a través del último parámetro Begin para que los datos
se puedan empaquetar en el objeto IAsyncResult que se pasará finalmente al método de
devolución de llamada. Normalmente no se requiere esto cuando se usan los métodos
FromAsync. Si los datos personalizados son conocidos para la continuación, se pueden capturar
directamente en el delegado de continuación. El siguiente ejemplo se parece el ejemplo anterior,
pero en lugar de examinar la propiedad Result del antecedente, la continuación examina los
datos de estado personalizados que son directamente accesibles al delegado de usuario de la
continuación.
public Task<string> GetFileStringAsync2(string path)
{
FileInfo fi = new FileInfo(path);
byte[] data = new byte[fi.Length];
MyCustomState state = GetCustomState();
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read,
FileShare.Read, data.Length, true);
// We still pass null for the last parameter because
// the state variable is visible to the continuation delegate.
Task<int> task = Task<int>.Factory.FromAsync(
fs.BeginRead, fs.EndRead, data, 0, data.Length, null);
return task.ContinueWith((antecedent) =>
{
// It is safe to close the filestream now.
fs.Close();
// Capture custom state data directly in the user delegate.
// No need to pass it through the FromAsync method.
if (state.StateData.Contains("New York, New York"))
{
return "Start spreading the news!";
}
else
{
// If we did not receive the entire file, the end of the
// data buffer will contain garbage.
if (antecedent.Result < data.Length)
Array.Resize(ref data, antecedent.Result);
// Will be returned in the Result property of the Task<string>
// at some future point after the asynchronous file I/O operation
completes.
return new UTF8Encoding().GetString(data);
}
});
}

Sincronizar varias tareas FromAsync


Los métodos estáticos ContinueWhenAny y ContinueWhenAll proporcionan flexibilidad
adicional cuando se usan junto con los métodos FromAsync. El siguiente ejemplo muestra
cómo iniciar varias operaciones asincrónicas de E/S y, a continuación, espera a que todos ellas
se completen antes de ejecutar la continuación.
public Task<string> GetMultiFileData(string[] filesToRead)
{
FileStream fs;

MCT: Luis Dueñas Pag 276 de 336


Manual de .NET Framework 4.5

Task<string>[] tasks = new Task<string>[filesToRead.Length];


byte[] fileData = null;
for (int i = 0; i < filesToRead.Length; i++)
{
fileData = new byte[0x1000];
fs = new FileStream(filesToRead[i], FileMode.Open, FileAccess.Read,
FileShare.Read, fileData.Length, true);
// By adding the continuation here, the Result of each task will be a
string.
tasks[i] = Task<int>.Factory.FromAsync(
fs.BeginRead, fs.EndRead, fileData, 0, fileData.Length, null)
.ContinueWith((antecedent) =>
{
fs.Close();
// If we did not receive the entire file, the end of
the
// data buffer will contain garbage.
if (antecedent.Result < fileData.Length)
Array.Resize(ref fileData, antecedent.Result);
// Will be returned in the Result property of the
Task<string>
// at some future point after the asynchronous file I/O operation
completes.
return new UTF8Encoding().GetString(fileData);
});
}
// Wait for all tasks to complete.
return Task<string>.Factory.ContinueWhenAll(tasks, (data) =>
{
// Propagate all exceptions and mark all faulted tasks as observed.
Task.WaitAll(data);
// Combine the results from all tasks.
StringBuilder sb = new StringBuilder();
foreach (var t in data)
{
sb.Append(t.Result);
}
// Final result to be returned eventually on the calling thread.
return sb.ToString();
});
}

Tareas FromAsync solo para el método End


En los pocos casos en los que el método Begin requiere más de tres parámetros de entrada o
tiene parámetros out o ref, puede usar las sobrecargas FromAsync, por ejemplo,
TaskFactory<TResult>. FromAsync(IAsyncResult, Func<IAsyncResult, TResult>), que
representa sólo el método End. Estos métodos también se pueden usar en cualquier escenario en
el que se pasa IAsyncResult y desea encapsularlo en una tarea.
static Task<String> ReturnTaskFromAsyncResult()
{
IAsyncResult ar = DoSomethingAsynchronously();
Task<String> t = Task<string>.Factory.FromAsync(ar, _ =>
{
return (string)ar.AsyncState;
});
return t;
}

Iniciar y cancelar las tareas FromAsync


La tarea devuelta por un método FromAsync tiene un estado de WaitingForActivation y la
iniciará el sistema en algún momento una vez creada la tarea. Si intenta llamar a Start en este
tipo de tarea, se producirá una excepción.

MCT: Luis Dueñas Pag 277 de 336


Manual de .NET Framework 4.5

No puede cancelar una tarea FromAsync, porque las API subyacentes de .NET Framework
admiten actualmente la cancelación en curso del la E/S de archivo o red. Puede agregar la
funcionalidad de cancelación a un método que encapsula una llamada FromAsync, pero sólo
puede responder a la cancelación antes de que se llame a FromAsync o después de completar
(por ejemplo, en una tarea de continuación).
Algunas clases que admiten EAP, por ejemplo, WebClient, admiten la cancelación y esa
funcionalidad de cancelación nativa se puede integrar mediante los tokens de cancelación.
Exponer las operaciones de EAP complejas como tareas
La TPL no proporciona ningún método diseñado específicamente para encapsular una operación
asincrónica basada en eventos del mismo modo que la familia de métodos FromAsync ajusta el
modelo IAsyncResult. Sin embargo, TPL proporciona la clase
System.Threading.Tasks.TaskCompletion Source<TResult>, que se puede usar para representar
cualquier conjunto arbitrario de operaciones como Task<TResult>. Las operaciones pueden ser
sincrónicas o asincrónicas y pueden ser enlazadas a E/S o enlazadas a cálculo, o ambos.
En el siguiente ejemplo se muestra cómo usar TaskCompletionSource<TResult> para exponer
un conjunto de operaciones WebClient asincrónicas al código de cliente como un objeto
Task<TResult> básico. El método permite escribir una matriz de direcciones URL de web y un
término o nombre que se va a buscar y, a continuación, devuelve el número de veces que
aparece el término de búsqueda en cada sitio.
Task<string[]> GetWordCountsSimplified(string[] urls, string name,
CancellationToken token)
{
TaskCompletionSource<string[]> tcs = new TaskCompletionSource<string[]>();
WebClient[] webClients = new WebClient[urls.Length];
object m_lock = new object();
int count = 0;
List<string> results = new List<string>();
// If the user cancels the CancellationToken, then we can use the
// WebClient's ability to cancel its own async operations.
token.Register(() =>
{
foreach (var wc in webClients)
{
if (wc != null)
wc.CancelAsync();
}
});
for (int i = 0; i < urls.Length; i++)
{
webClients[i] = new WebClient();
#region callback
// Specify the callback for the DownloadStringCompleted
// event that will be raised by this WebClient instance.
webClients[i].DownloadStringCompleted += (obj, args) =>
{
// Argument validation and exception handling omitted for brevity.
// Split the string into an array of words, then count the number
// of elements that match the search term.
string[] words = args.Result.Split(' ');
string NAME = name.ToUpper();
int nameCount = (from word in words.AsParallel()
where word.ToUpper().Contains(NAME)
select word)
.Count();

// Associate the results with the url, and add new string to the
array
// that the underlying Task object will return in its Result
property.

MCT: Luis Dueñas Pag 278 de 336


Manual de .NET Framework 4.5

results.Add(String.Format("{0} has {1} instances of {2}",


args.UserState,
nameCount, name));
// If this is the last async operation to complete,
// then set the Result property on the underlying Task.
lock (m_lock)
{
count++;
if (count == urls.Length)
{
tcs.TrySetResult(results.ToArray());
}
}
};
#endregion

// Call DownloadStringAsync for each URL.


Uri address = null;
address = new Uri(urls[i]);
webClients[i].DownloadStringAsync(address, address);
} // end for
// Return the underlying Task. The client code
// waits on the Result property, and handles exceptions
// in the try-catch block there.
return tcs.Task;
}

Recuerde que TaskCompletionSource iniciará cualquier tarea creada por


TaskCompletionSource <TResult> y, por consiguiente, el código de usuario no debería llamar al
método Start en esa tarea.
Implementar el modelo de APM usando las tareas
En algunos escenarios, puede ser deseable exponer directamente el modelo IAsyncResult
mediante pares de métodos Begin/End en una API. Por ejemplo, quizás desee mantener la
coherencia con las API existentes o puede haber automatizado herramientas que requieren este
modelo. En tales casos, puede usar las tareas para simplificar la forma en que se implementa
internamente el modelo de APM.
En el siguiente ejemplo se muestra cómo usar las tareas para implementar un par de métodos
Begin/End de APM para un método enlazado a cálculo de ejecución prolongada.
class Calculator
{
public IAsyncResult BeginCalculate(int decimalPlaces, AsyncCallback ac,
object state)
{
Console.WriteLine("Calling BeginCalculate on thread {0}",
Thread.CurrentThread.ManagedThreadId);
Task<string> f = Task<string>.Factory.StartNew(_ =>
Compute(decimalPlaces),
state);
if (ac != null) f.ContinueWith((res) => ac(f));
return f;
}

public string Compute(int numPlaces)


{
Console.WriteLine("Calling compute on thread {0}",
Thread.CurrentThread.ManagedThreadId);
// Simulating some heavy work.
Thread.SpinWait(500000000);
// Actual implemenation left as exercise for the reader.
// Several examples are available on the Web.
return "3.14159265358979323846264338327950288";
}

MCT: Luis Dueñas Pag 279 de 336


Manual de .NET Framework 4.5

public string EndCalculate(IAsyncResult ar)


{
Console.WriteLine("Calling EndCalculate on thread {0}",
Thread.CurrentThread.ManagedThreadId);
return ((Task<string>)ar).Result;
}
}

public class CalculatorClient


{
static int decimalPlaces = 12;
public static void Main()
{
Calculator calc = new Calculator();
int places = 35;
AsyncCallback callBack = new AsyncCallback(PrintResult);
IAsyncResult ar = calc.BeginCalculate(places, callBack, calc);
// Do some work on this thread while the calulator is busy.
Console.WriteLine("Working...");
Thread.SpinWait(500000);
Console.ReadLine();
}

public static void PrintResult(IAsyncResult result)


{
Calculator c = (Calculator)result.AsyncState;
string piString = c.EndCalculate(result);
Console.WriteLine("Calling PrintResult on thread {0}; result = {1}",
Thread.CurrentThread.ManagedThreadId, piString);
}
}

Usar el código de ejemplo de StreamExtensions


El archivo Streamextensions.cs, en la página Samples for Parallel Programming with the .NET
Framework 4 del sitio web de MSDN, contiene varias implementaciones de la referencia que
usan los objetos de tarea para la E/S asincrónica de archivo y red.
3.1.4.2. Cómo: Encapsular modelos de EAP en una tarea
El ejemplo siguiente muestra cómo exponer una secuencia arbitraria de operaciones
asincrónicas de modelo Evento- Basar (EAP) como una tarea mediante
TaskCompletionSource<TResult>. El ejemplo también muestra cómo usar CancellationToken
para invocar los métodos de cancelación integrados en los objetos WebClient.
Ejemplo
class WebDataDownloader
{
static void Main()
{
WebDataDownloader downloader = new WebDataDownloader();
string[] addresses = { "http://www.msnbc.com", "http://www.yahoo.com",
"http://www.nytimes.com",
"http://www.washingtonpost.com",
"http://www.latimes.com", "http://www.newsday.com"
};
CancellationTokenSource cts = new CancellationTokenSource();
// Create a UI thread from which to cancel the entire operation
Task.Factory.StartNew(() =>
{
Console.WriteLine("Press c to cancel");
if (Console.ReadKey().KeyChar == 'c')
cts.Cancel();
});
// Using a neutral search term that is sure to get some hits.

MCT: Luis Dueñas Pag 280 de 336


Manual de .NET Framework 4.5

Task<string[]> webTask = downloader.GetWordCounts(addresses, "the",


cts.Token);
// Do some other work here unless the method has already completed.
if (!webTask.IsCompleted)
{
// Simulate some work.
Thread.SpinWait(5000000);
}
string[] results = null;
try
{
results = webTask.Result;
}
catch (AggregateException e)
{
foreach (var ex in e.InnerExceptions)
{
OperationCanceledException oce = ex as
OperationCanceledException;
if (oce != null)
{
if (oce.CancellationToken == cts.Token)
{
Console.WriteLine("Operation canceled by user.");
}
}
else
Console.WriteLine(ex.Message);
}
}
if (results != null)
{
foreach (var item in results)
Console.WriteLine(item);
}
Console.ReadKey();
}

Task<string[]> GetWordCounts(string[] urls, string name, CancellationToken


token)
{
TaskCompletionSource<string[]> tcs = new
TaskCompletionSource<string[]>();
WebClient[] webClients = new WebClient[urls.Length];
// If the user cancels the CancellationToken, then we can use the
// WebClient's ability to cancel its own async operations.
token.Register(() =>
{
foreach (var wc in webClients)
{
if (wc != null)
wc.CancelAsync();
}
});
object m_lock = new object();
int count = 0;
List<string> results = new List<string>();
for (int i = 0; i < urls.Length; i++)
{
webClients[i] = new WebClient();
#region callback
// Specify the callback for the DownloadStringCompleted
// event that will be raised by this WebClient instance.
webClients[i].DownloadStringCompleted += (obj, args) =>
{
if (args.Cancelled == true)
{
tcs.TrySetCanceled();

MCT: Luis Dueñas Pag 281 de 336


Manual de .NET Framework 4.5

return;
}
else if (args.Error != null)
{
// Pass through to the underlying Task
// any exceptions thrown by the WebClient
// during the asynchronous operation.
tcs.TrySetException(args.Error);
return;
}
else
{
// Split the string into an array of words,
// then count the number of elements that match
// the search term.
string[] words = null;
words = args.Result.Split(' ');
string NAME = name.ToUpper();
int nameCount = (from word in words.AsParallel()
where word.ToUpper().Contains(NAME)
select word)
.Count();
// Associate the results with the url, and add new string to
the array
// that the underlying Task object will return in its Result
property.
results.Add(String.Format("{0} has {1} instances of {2}",
args.UserState, nameCount, name));
}
// If this is the last async operation to complete,
// then set the Result property on the underlying Task.
lock (m_lock)
{
count++;
if (count == urls.Length)
{
tcs.TrySetResult(results.ToArray());
}
}
};
#endregion
// Call DownloadStringAsync for each URL.
Uri address = null;
try
{
address = new Uri(urls[i]);
// Pass the address, and also use it for the userToken
// to identify the page when the delegate is invoked.
webClients[i].DownloadStringAsync(address, address);
}
catch (UriFormatException ex)
{
// Abandon the entire operation if one url is malformed.
// Other actions are possible here.
tcs.TrySetException(ex);
return tcs.Task;
}
}
// Return the underlying Task. The client code
// waits on the Result property, and handles exceptions
// in the try-catch block there.
return tcs.Task;
}

3.1.5. Problemas potenciales en el paralelismo de datos y tareas


En muchos casos, Parallel.For y Parallel.ForEach pueden proporcionar mejoras de rendimiento
significativas en comparación con los bucles secuenciales normales. Sin embargo, el trabajo de

MCT: Luis Dueñas Pag 282 de 336


Manual de .NET Framework 4.5

paralelizar el bucle aporta una complejidad que puede llevar a problemas que, en código
secuencial, no son tan comunes o que no se producen en absoluto. En este tema se indican
algunas prácticas que se deben evitar al escribir bucles paralelos.
No se debe suponer que la ejecución en paralelo es siempre más rápida
En algunos casos, un bucle paralelo se podría ejecutar más lentamente que su equivalente
secuencial. La regla básica es que no es probable que los bucles en paralelo que tienen pocas
iteraciones y delegados de usuario rápidos aumenten gran cosa la velocidad. Sin embargo, como
son muchos los factores implicados en el rendimiento, recomendamos siempre medir los
resultados reales.
Evitar la escritura en ubicaciones de memoria compartidas
En código secuencial, no es raro leer o escribir en variables estáticas o en campos de clase. Sin
embargo, cuando varios subprocesos tienen acceso a estas variables de forma simultánea, hay
grandes posibilidades de que se produzcan condiciones de carrera. Aunque se pueden usar
bloqueos para sincronizar el acceso a la variable, el costo de la sincronización puede afectar
negativamente al rendimiento. Por tanto, se recomienda evitar, o al menos limitar, el acceso al
estado compartido en un bucle en paralelo en la medida de lo posible. La manera mejor de
hacerlo es mediante las sobrecargas de Parallel.For y Parallel.ForEach que utilizan una variable
System.Threading.ThreadLocal<T> para almacenar el estado local del subproceso durante la
ejecución del bucle.
Evitar la paralelización excesiva
Si usa bucles en paralelo, incurrirá en costos de sobrecarga al crear particiones de la colección
de origen y sincronizar los subprocesos de trabajo. El número de procesadores del equipo reduce
también las ventajas de la paralelización. Si se ejecutan varios subprocesos enlazados a cálculos
en un único procesador, no se gana en velocidad. Por tanto, debe tener cuidado para no
paralelizar en exceso un bucle.
El escenario más común en el que se puede producir un exceso de paralelización son los bucles
anidados. En la mayoría de los casos, es mejor paralelizar únicamente el bucle exterior, a menos
que se cumplan una o más de las siguientes condiciones:
 Se sabe que el bucle interno es muy largo.
 Se realiza un cálculo caro en cada pedido. (La operación que se muestra en el ejemplo
no es cara.)
 Se sabe que el sistema de destino tiene suficientes procesadores como para controlar el
número de subprocesos que se producirán al paralelizar la consulta de cust.Orders.
En todos los casos, la mejor manera de determinar la forma óptima de la consulta es mediante la
prueba y la medición.
Evitar llamadas a métodos que no son seguros para subprocesos
La escritura en métodos de instancia que no son seguros para subprocesos de un bucle en
paralelo puede producir daños en los datos, que pueden pasar o no inadvertidos para el
programa. También puede dar lugar a excepciones. En el siguiente ejemplo, varios subprocesos
estarían intentando llamar simultáneamente al método FileStream.WriteByte, lo que no se
admite en la clase.
FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));

Limitar las llamadas a métodos seguros para subprocesos

MCT: Luis Dueñas Pag 283 de 336


Manual de .NET Framework 4.5

La mayoría de los métodos estáticos de .NET Framework son seguros para subprocesos y se les
pueden llamar simultáneamente desde varios subprocesos. Sin embargo, incluso en estos casos,
la sincronización que esto supone puede conducir a una ralentización importante en la consulta.
Nota
Puede comprobarlo si inserta algunas llamadas a WriteLine en las consultas. Aunque este
método se usa en los ejemplos de la documentación para fines de demostración, no debe usarlo
en bucles paralelos a menos que sea necesario.
Ser consciente de los problemas de afinidad de los subprocesos
Algunas tecnologías, como la interoperabilidad COM para componentes STA (contenedor
uniproceso), Windows Forms y Windows Presentation Foundation (WPF), imponen
restricciones de afinidad de subprocesos que exigen que el código se ejecute en un subproceso
determinado. Por ejemplo, tanto en Windows Forms como en WPF, solo se puede tener acceso a
un control en el subproceso donde se creó. Por ejemplo, esto significa que no puede actualizar
un control de lista desde un bucle paralelo a menos que configure el programador del
subproceso para que programe trabajo solo en el subproceso de la interfaz de usuario.
Tener precaución cuando se espera en delegados a los que llama Parallel.Invoke
En algunas circunstancias, Task Parallel Library incluirá una tarea, lo que significa que se
ejecuta en la tarea del subproceso que se está ejecutando actualmente. Esta optimización de
rendimiento puede acabar en interbloqueo en algunos casos. Por ejemplo, dos tareas podrían
ejecutar el mismo código de delegado, que señala cuándo se genera un evento, y después esperar
a que la otra tarea señale. Si la segunda tarea está alineada en el mismo subproceso que la
primera y la primero entra en un estado de espera, la segunda tarea nunca podrá señalar su
evento. Para evitar que suceda, puede especificar un tiempo de espera en la operación de espera
o utilizar constructores de subproceso explícitos para ayudar a asegurarse de que una tarea no
puede bloquear la otra.
No se debe suponer que las iteraciones de ForEach, For y ForAll siempre se ejecutan en
paralelo
Es importante tener presente que las iteraciones individuales de un bucle For, ForEach o
ForAll<TSource> tal vez no tengan que ejecutarse en paralelo. Por consiguiente, se debe evitar
escribir código cuya exactitud dependa de la ejecución en paralelo de las iteraciones o de la
ejecución de las iteraciones en algún orden concreto. Por ejemplo, es probable que este código
lleve a un interbloqueo:
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100)
.AsParallel()
.ForAll((j) =>
{
if (j == Environment.ProcessorCount)
{
Console.WriteLine("Set on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j);
mre.Set();
}
else
{
Console.WriteLine("Waiting on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j);
mre.Wait();
}
}); //deadlocks

En este ejemplo, una iteración establece un evento y el resto de las iteraciones esperan el evento.
Ninguna de las iteraciones que esperan puede completarse hasta que se haya completado la

MCT: Luis Dueñas Pag 284 de 336


Manual de .NET Framework 4.5

iteración del valor de evento. Sin embargo, es posible que las iteraciones que esperan bloqueen
todos los subprocesos que se utilizan para ejecutar el bucle paralelo, antes de que la iteración del
valor de evento haya tenido oportunidad de ejecutarse. Esto produce un interbloqueo: la
iteración del valor de evento nunca se ejecutará y las iteraciones que esperan nunca se activarán.
En concreto, una iteración de un bucle paralelo no debe esperar nunca otra iteración del bucle
para progresar. Si el bucle paralelo decide programar las iteraciones secuencialmente pero en el
orden contrario, se producirá un interbloqueo.
Evitar la ejecución de bucles en paralelo en el subproceso de la interfaz de usuario
Es importante mantener la interfaz de usuario de la aplicación (UI) capaz de reaccionar. Si una
operación contiene bastante trabajo para garantizar la paralelización, no se debería ejecutar en el
subproceso de la interfaz de usuario. Conviene descargarla para que se ejecute en un subproceso
en segundo plano. Por ejemplo, si desea utilizar un bucle paralelo para calcular datos que
después se presentarán en un control de IU, considere ejecutar el bucle dentro de una instancia
de la tarea, en lugar de directamente en un controlador de eventos de IU. Solo si el cálculo
básico se ha completado se deberían calcular las referencias de la actualización de nuevo en el
subproceso de la interfaz de usuario.
Si ejecuta bucles paralelos en el subproceso de la interfaz de usuario, tenga el cuidado de evitar
la actualización de los controles de la interfaz de usuario desde el interior del bucle. Si se intenta
actualizar los controles de la interfaz de usuario desde dentro de un bucle paralelo que se está
ejecutando en el subproceso de la interfaz de usuario, se puede llegar a dañar el estado, a
producir excepciones, actualizaciones atrasadas e incluso interbloqueos, dependiendo de cómo
se invoque la actualización de la interfaz de usuario. En el siguiente ejemplo, el bucle paralelo
bloquea el subproceso de la interfaz de usuario en el que se está ejecutando hasta que todas las
iteraciones se completan. Sin embargo, si se está ejecutando una iteración del bucle en un
subproceso en segundo plano (como puede hacer For), la llamada a Invoke produce que se envíe
un mensaje al subproceso de la interfaz de usuario, que se bloquea mientras espera a que ese
mensaje se procese. Puesto que se bloquea el subproceso de la interfaz de usuario cuando se
ejecuta For, el mensaje no se procesa nunca y el subproceso de la interfaz de usuario se
interbloquea.
private void button1_Click(object sender, EventArgs e)
{
Parallel.For(0, N, i =>
{
// do work for i
button1.Invoke((Action)delegate { DisplayProgress(i); });
});
}

En el siguiente ejemplo se muestra cómo evitar el interbloqueo mediante la ejecución del bucle
dentro de una instancia de la tarea. El bucle no bloquea el subproceso de la interfaz de usuario y
se puede procesar el mensaje.
private void button1_Click(object sender, EventArgs e)
{
Task.Factory.StartNew(() =>
Parallel.For(0, N, i =>
{
// do work for i
button1.Invoke((Action)delegate { DisplayProgress(i); });
})
);
}

3.2. Parallel LINQ (PLINQ)


Parallel LINQ (PLINQ) es una implementación paralela de LINQ to Objects. PLINQ
implementa el conjunto completo de operadores de consulta estándar de LINQ como métodos
de extensión para el espacio de nombres T:System.Linq y tiene operadores adicionales para las

MCT: Luis Dueñas Pag 285 de 336


Manual de .NET Framework 4.5

operaciones paralelas. PLINQ combina la simplicidad y legibilidad de la sintaxis de LINQ con


la eficacia de la programación paralela. De la misma forma que el código destinado a la
biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas),
las consultas PLINQ aumentan el grado de simultaneidad en función de la capacidad del equipo
host.
En muchos escenarios, PLINQ puede aumentar significativamente la velocidad de las consultas
LINQ to Objects utilizando todos los núcleos disponibles en el equipo host de una forma más
eficaz. Este mayor rendimiento aporta al escritorio una alta capacidad de computación.
3.2.1. Introducción a PLINQ
¿Qué es una consulta paralela?
Language-Integrated Query (LINQ) se incorporó en .NET Framework 3.5. Él características un
modelo unificado para consultar cualquier origen de datos de System.Collections.IEnumerable o
de System.Collections.Generic.IEnumerable<T> de forma tipo- segura. LINQ to Objects es el
nombre para los consultas LINQ que se ejecutan con las colecciones en memoria, como
List<T>, y las matrices. En este artículo se supone que tiene un conocimiento básico de LINQ.
Parallel LINQ (PLINQ) es una implementación paralela del modelo LINQ. Una consulta
PLINQ se parece de muchas maneras a una consulta LINQ to Objects no paralela. Las consultas
PLINQ, al igual que las consultas LINQ secuenciales, funcionan con cualquier origen de datos
IEnumerable o IEnumerable<T> en memoria y usan la ejecución diferida, lo que significa que
no empiezan a ejecutarse hasta que se enumere la consulta. La diferencia primaria es que
PLINQ intenta realizar el uso completo de todos los procesadores en el sistema. Para hacer esto,
divide el origen de datos en segmentos y, a continuación, ejecuta la consulta en cada segmento
en subprocesos de trabajo independientes en paralelo en varios procesadores. En muchos casos,
la ejecución en paralelo significa que la consulta se ejecuta bastante más rápidamente.
Con la ejecución en paralelo, PLINQ puede lograr mejoras de rendimiento significativas
respecto al código heredado para ciertos tipos de consultas, a menudo con solo agregar la
operación de consulta de AsParallel al origen de datos. Sin embargo, el paralelismo puede
presentar sus propias complejidades y no todas las operaciones de consulta se ejecutan más
rápidamente en PLINQ. De hecho, la ejecución en paralelo realmente reduce la velocidad de
ciertas consultas. Por consiguiente, debería entender en qué grado afectan a las consultas en
paralelo ciertos aspectos como la ordenación.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la PLINQ.
El resto de este artículo proporciona información general de las clases PLINQ principales y
describe cómo crear las consultas PLINQ. Cada sección contiene vínculos a ejemplos de código
e información más detallada.
La clase ParallelEnumerable
La clase System.Linq.ParallelEnumerable expone casi toda la funcionalidad de PLINQ. Esta
clase y el resto de tipos del espacio de nombres System.Linq se compilan en el ensamblado
System.Core.dll. Los proyectos predeterminados de C# y Visual Basic en Visual Studio hacen
referencia el ensamblado e importan el espacio de nombres.
ParallelEnumerable incluye implementaciones de todos los operadores de consulta estándar que
admite LINQ to Objects, aunque no intenta ejecutar cada una en paralelo.
Además de los operadores de consulta estándar, la clase ParallelEnumerable contiene un
conjunto de métodos que habilitan los comportamientos específicos de la ejecución en paralelo.
Estos métodos específicos de PLINQ se muestran en la siguiente tabla.
Operador ParallelEnumerable Descripción

MCT: Luis Dueñas Pag 286 de 336


Manual de .NET Framework 4.5

Punto de entrada para PLINQ. Especifica que el resto de


AsParallel
la consulta se debería ejecutar en paralelo, si es posible.
Especifica que el resto de la consulta se debería ejecutar
AsSequential<TSource>
secuencialmente, como un consulta LINQ no paralela.
Especifica que PLINQ debería conservar la clasificación
de la secuencia de origen para el resto de la consulta, o
AsOrdered hasta que se cambie la clasificación, por ejemplo
mediante una cláusula orderby (Order By en Visual
Basic).
Especifica que no es necesario que PLINQ conserve la
AsUnordered<TSource> clasificación de la secuencia de origen durante el resto de
la consulta.
Especifica que PLINQ debería supervisar periódicamente
WithCancellation<TSource> el estado del token de cancelación proporcionado y
cancelar la ejecución si se solicita.
Especifica el número máximo de procesadores que
WithDegreeOfParallelism<TSource>
PLINQ debería usar para ejecutar la consulta en paralelo.
Proporciona una sugerencia sobre cómo PLINQ debe, si
WithMergeOptions<TSource> es posible, volver a combinar los resultados paralelos en
solo una secuencia en el subproceso utilizado.
Especifica si PLINQ debería ejecutar la consulta en
WithExecutionMode<TSource> paralelo incluso cuando el comportamiento
predeterminado sería ejecutarla secuencialmente.
Un método de enumeración multiproceso que, a diferencia
de iterar por los resultados de la consulta, permite volver a
ForAll<TSource>
procesar los resultados en paralelo sin volver a combinar
primero el subproceso de consumidor.
Una sobrecarga que es exclusiva de PLINQ y habilita la
agregación intermedia sobre las particiones locales de
sobrecarga de Aggregate
subprocesos, más una última función de agregación para
combinar los resultados de todas las particiones.
El modelo de opción
Al escribir una consulta, elija PLINQ invocando el método de extensión
ParallelEnumerable.AsParallel en el origen de datos, como se muestra en el siguiente ejemplo.
var source = Enumerable.Range(1, 10000);
// Opt-in to PLINQ with AsParallel
var evenNums = from num in source.AsParallel()
where Compute(num) > 0
select num;

El método de extensión AsParallel enlaza a los operadores de consulta subsiguientes, en este


caso, where y select, a las implementaciones de System.Linq.ParallelEnumerable.
Modos de ejecución
De forma predeterminada, PLINQ es conservador. En tiempo de ejecución, la infraestructura
PLINQ analiza la estructura general de la consulta. Si es probable que la consulta produzca
aumentos de velocidad por la ejecución en paralelo, PLINQ divide la secuencia de origen en
tareas que se pueden ejecutar simultáneamente. Si no es seguro para ejecutar una consulta en
paralelo, PLINQ simplemente ejecuta la consulta de forma secuencial. Si PLINQ puede elegir
entre un algoritmo paralelo potencialmente costoso o un algoritmo secuencial poco costoso,
elige el algoritmo secuencial de forma predeterminada. Puede usar el método
WithExecutionMode<TSource> y la enumeración System.Linq.ParallelExecutionMode para

MCT: Luis Dueñas Pag 287 de 336


Manual de .NET Framework 4.5

indicar a PLINQ que seleccione el algoritmo paralelo. Esto resulta útil cuando sabe por las
pruebas y mediciones que una consulta determinada se ejecuta más rápidamente en paralelo.
Grado de paralelismo
De forma predeterminada, PLINQ usa todos los procesadores en el equipo host hasta un
máximo de 64. Puede indicar a PLINQ que no utilice más que un número especificado de
procesadores usando el método WithDegreeOfParallelism<TSource>. Esto resulta útil si desea
asegurarse de que los otros procesos que se ejecutan en el equipo reciben cierta cantidad de
tiempo de CPU. El siguiente fragmento de código limita la consulta a usar un máximo de dos
procesadores.
var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
where Compute(item) > 42
select item;

En los casos donde una consulta está realizando una cantidad significativa de trabajo enlazado
sin cálculo, como la E/S de archivo, podría ser beneficioso especificar un grado de paralelismo
mayor que el número de núcleos del equipo.
Consultar en paralelo ordenadas frente a no ordenadas
En algunas consultas, un operador de consulta debe generar resultados que conservan el orden
de la secuencia de origen. PLINQ proporciona el operador de AsOrdered con este fin.
AsOrdered es distinto de AsSequential<TSource>. Una secuencia de AsOrdered todavía se
procesa en paralelo, pero se almacenan en búfer y se ordenan los resultados. Porque la
conservación del orden implica normalmente trabajo adicional, una secuencia de AsOrdered se
podría procesar más despacio que la secuencia predeterminada de AsUnordered<TSource>. El
hecho de que una operación en paralelo ordenada especial sea más rápida que una versión
secuencial de la operación depende de muchos factores.
El siguiente ejemplo de código muestra las opciones que se deben elegir para conservar el
orden.
evenNums = from num in numbers.AsParallel().AsOrdered() where num % 2 == 0
select num;

Paralelo VS. consultas secuenciales


Algunas operaciones requieren que los datos de origen se entreguen de una manera secuencial.
Los operadores de consulta ParallelEnumerable revierten automáticamente al modo secuencial
cuando es necesario. Para los operadores de consulta definidos por el usuario y los delegados de
usuario que requieren la ejecución secuencial, PLINQ proporciona el método
AsSequential<TSource>. Al usar AsSequential<TSource>, se ejecutan secuencialmente todos
los operadores subsiguientes en la consulta hasta que se llame de nuevo a AsParallel.
Opciones para combinar los resultados de la consulta
Cuando una consulta PLINQ se ejecuta en paralelo, sus resultados de cada subproceso de
trabajo deben volver a combinarse en el subproceso principal para su uso en un bucle foreach
(For Each en Visual Basic) o la inserción en una lista o matriz. En algunos casos, podría ser
beneficioso especificar un tipo determinado de operación de combinación, por ejemplo, para
empezar a generar resultados más rápidamente. Con este propósito, PLINQ admite el método de
WithMergeOptions<TSource>, y la enumeración de ParallelMergeOptions.
El operador ForAll
En las consultas secuenciales de LINQ, la ejecución se difiere hasta que la consulta se enumere
en un bucle foreach (For Each en Visual Basic) o al invocar un método como ToList<TSource>,
ToArray<TSource> o ToDictionary. En PLINQ, también se puede usar foreach para ejecutar la
consulta e iterar por los resultados. Sin embargo, el propio bucle foreach no se ejecuta en
paralelo y, por consiguiente, requiere que el resultado de todas las tareas paralelas se vuelva a

MCT: Luis Dueñas Pag 288 de 336


Manual de .NET Framework 4.5

combinar en el subproceso en el que se está ejecutando el bucle. En PLINQ, puede usar foreach
cuando deba conservar el orden final de los resultados de la consulta, y también cada vez que
procese los resultados en serie, por ejemplo cuando está llamando a Console.WriteLine para
cada elemento. Para una ejecución más rápida de la consulta cuando no se requiere la
conservación del orden y cuando el propio procesamiento de los resultados se puede ejecutar en
paralelo, use el método ForAll<TSource> para ejecutar una consulta PLINQ. ForAll<TSource>
no realiza este paso final de la combinación. En el siguiente ejemplo de código, se muestra
cómo utilizar el método ForAll<TSource>. Se usa System.Collections.Concurrent.
ConcurrentBag<T> en este caso porque está optimizado para varios subprocesos que agregan
elementos simultáneamente sin intentar quitar ningún elemento.
var nums = Enumerable.Range(10, 10000);
var query = from num in nums.AsParallel()
where num % 10 == 0
select num;
// Process the results as each thread completes
// and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
// which can safely accept concurrent add operations
query.ForAll((e) => concurrentBag.Add(Compute(e)));

En la siguiente ilustración, se muestra la diferencia entre foreach y ForAll<TSource> con


respecto a la ejecución de una consulta.

Cancelación
PLINQ se integra con los tipos de cancelación en .NET Framework 4. (Para obtener más
información, vea Cancelación en subprocesos administrados). Por consiguiente, a diferencia de
las consultas secuenciales LINQ to Objects, las consultas PLINQ pueden cancelarse. Crear una
consulta PLINQ cancelable, use el operador WithCancellation<TSource> en la consulta y
proporcione una instancia de CancellationToken como argumento. Cuando se establece en true
la propiedad IsCancellationRequested del token, PLINQ lo observará, detendrá el
procesamiento en todos los subprocesos y generará una excepción Operation
CanceledException.
Es posible que una consulta PLINQ continúe procesando algunos elementos una vez establecido
el token de cancelación.
Para lograr una sensibilidad mayor, puede responder también a las solicitudes de cancelación en
los delegados de usuario de ejecución prolongada.
Excepciones
Cuando se ejecuta una consulta PLINQ, se podrían producir simultáneamente varias
excepciones de diferentes subprocesos. Asimismo, el código para controlar la excepción podría

MCT: Luis Dueñas Pag 289 de 336


Manual de .NET Framework 4.5

encontrarse un subproceso diferente al del código que produjo la excepción. PLINQ usa el tipo
AggregateException para encapsular todas las excepciones producidas por una consulta y
vuelve a calcular las referencias de esas excepciones para el subproceso que realiza la llamada.
En el subproceso que realiza la llamada, solo se requiere un bloque try-catch. Sin embargo,
puede iterar por todas las excepciones encapsuladas en AggregateException y detectar
cualquiera de la que se pueda recuperar de forma segura. En casos raros, pueden producir
algunas excepciones que no se encapsulan en AggregateException, y s para
ThreadAbortException tampoco se ajusta.
Cuando las excepciones pueden propagarse de nuevo al subproceso de unión, es posible que una
consulta continúe procesando algunos elementos después de que se haya producido la
excepción.
Particionadores personalizados
En ciertos casos, puede mejorar el rendimiento de las consultas si escribe un particionador
personalizado que se aprovecha de alguna característica de los datos de origen. En la consulta, el
propio particionador personalizado es el objeto enumerable que se consulta.
int[] arr= ...;
Partitioner<int> partitioner = newMyArrayPartitioner<int>(arr);
var q = partitioner.AsParallel().Select(x => SomeFunction(x));

PLINQ admite un número de particiones fijo (aunque los datos se pueden reasignar
dinámicamente a esas particiones durante el tiempo de ejecución para el equilibrio de carga).
For y ForEach solo admiten la creación dinámica de particiones, lo que significa que el número
de particiones cambia en tiempo de ejecución.
Medir el rendimiento de PLINQ
En muchos casos, una consulta se puede ejecutar en paralelo, pero la sobrecarga de preparar la
consulta paralela supera a la ventaja de rendimiento obtenida. Si una consulta no realiza muchos
cálculos o si el origen de datos es pequeño, una consulta PLINQ puede ser más lenta que una
consulta secuencial LINQ to Objects. Puede usar el Analizador de rendimiento de
procesamiento paralelo en Visual Studio Team Server para comparar el rendimiento de varias
consultas, buscar los cuellos de botella del procesamiento y determinar si su consulta se está
ejecutando en paralelo o secuencialmente.
3.2.2. Introducción a la velocidad en PLINQ
El objetivo principal de PLINQ es acelerar la ejecución de LINQ to Objects mediante la
ejecución de los delegados de consulta en paralelo en equipos multiprocesador. El rendimiento
de PLINQ es óptimo cuando el procesamiento de cada elemento de una colección de origen es
independiente, y no se comparte el estado entre los delegados individuales. Esas operaciones
son comunes en LINQ to Objects y PLINQ, y se prestan con facilidad a la programación en
varios subprocesos, por lo que se conocen como "perfectamente paralelas". Sin embargo, no
todas las consultas se componen de operaciones paralelas perfectas; en la mayoría de los casos,
una consulta incluye operadores que no se pueden paralelizar o que ralentizan la ejecución en
paralelo. Incluso con consultas que son perfectamente paralelas, PLINQ debe crear particiones
del origen de datos y programar el trabajo en los subprocesos, y generalmente tiene que
combinar los resultados cuando la consulta se completa. Todas estas operaciones aumentan el
costo computacional de la paralelización; el costo de agregar paralelización se denomina
sobrecarga. Para lograr el rendimiento óptimo en una consulta PLINQ, el objetivo es maximizar
las partes que son perfectamente paralelas y minimizar las que requieren sobrecarga. En este
artículo se proporciona información que le ayudará a escribir consultas PLINQ lo más eficaces
posible y que además produzcan resultados correctos.
Factores que afectan al rendimiento de las consultas PLINQ

MCT: Luis Dueñas Pag 290 de 336


Manual de .NET Framework 4.5

En las siguientes secciones se enumeran algunos de los factores más importantes que influyen
en el rendimiento de las consultas en paralelo. Son instrucciones generales que por sí solas no
bastan para predecir el rendimiento de las consultas en todos los casos. Como siempre, es
importante medir el rendimiento real de consultas concretas en equipos con una gama de cargas
y configuraciones representativas.
1. Costo computacional del trabajo total.
Para lograr velocidad, una consulta PLINQ debe tener bastante trabajo perfectamente en
paralelo como para compensar la sobrecarga. El trabajo se puede expresar como el costo
computacional de cada delegado multiplicado por el número de elementos de la
colección de origen. Suponiendo que una operación se pueda paralelizar, cuanto más
cara sea computacionalmente, más oportunidad hay de aumentar la velocidad. Por
ejemplo, si una función tarda un milisegundo en ejecutarse, una consulta secuencial de
más de 1000 elementos tardará un segundo en realizar esa operación, mientras que una
consulta paralela en un equipo con cuatro núcleos solo tardaría 250 milisegundos. Esto
supone 750 milisegundos menos. Si la función tardara un segundo en ejecutar cada
elemento, el aumento sería de 750 segundos. Si el delegado resulta muy caro, PLINQ
podría proporcionar un aumento significativo con solo unos elementos de la colección
de origen. A la inversa, las colecciones de origen pequeñas con delegados triviales no
son, en general, buenas candidatas para PLINQ.
En el siguiente ejemplo, queryA es probablemente buena candidata para PLINQ,
suponiendo que su función Select implica mucho trabajo. queryB no es probablemente
una buena candidata porque no hay bastante trabajo en la instrucción Select y la
sobrecarga de paralelización compensará la mayoría del aumento (o todo).
var queryA = from num in numberList.AsParallel()
select ExpensiveFunction(num); //good for PLINQ
var queryB = from num in numberList.AsParallel()
where num % 2 > 0
select num; //not as good for PLINQ

2. Número de núcleos lógicos del sistema (grado de paralelismo).


Este punto es un corolario obvio de la sección anterior. Las consultas que están
perfectamente en paralelo se ejecutan más rápidamente en equipos con más núcleos
porque se puede dividir el trabajo entre más subprocesos simultáneos. La cantidad total
de aumento de velocidad depende del porcentaje del trabajo total de la consulta que se
puede paralelizar. Sin embargo, no se debe dar por supuesto que todas las consultas se
ejecutarán el doble de rápido en un equipo de ocho núcleos que en uno de cuatro. Al
refinar las consultas para lograr el rendimiento óptimo, es importante medir los
resultados reales en equipos con varios núcleos. Este punto se relaciona con el punto 1:
se necesitan conjuntos de datos mayores para aprovechar los grandes recursos
informáticos.
3. Número y tipo de operaciones.
PLINQ proporciona el operador AsOrdered para las situaciones en las que es necesario
mantener el orden de elementos de la secuencia de origen. Hay un costo asociado a la
ordenación, pero suele ser reducido. Las operaciones GroupBy y Join también incurren
en sobrecarga. PLINQ rinde mejor cuando puede procesar los elementos de la colección
de origen en cualquier orden y pasarlos al operador siguiente en cuanto están listos.
4. Forma de ejecución de la consulta.
Si se guardan los resultados de una consulta llamando a ToArray o ToList, los
resultados de todos los subprocesos paralelos deben combinarse en la estructura de
datos única. Esto implica un costo computacional inevitable. De igual modo, si se
recorren los resultados con un bucle foreach (For Each en Visual Basic), hay que
serializar los resultados de los subprocesos de trabajo en el subproceso del enumerador.

MCT: Luis Dueñas Pag 291 de 336


Manual de .NET Framework 4.5

Pero si solo desea realizar una acción basada en el resultado de cada subproceso, puede
utilizar el método ForAll para realizar este trabajo en varios subprocesos.
5. Tipo de opciones de combinación.
PLINQ se puede configurar para almacenar en búfer el resultado y producirlo en
fragmentos o todo a la vez cuando el conjunto de resultados esté completo, o transmitir
en secuencias los resultados individuales a medida que se van produciendo. El primer
resultado disminuye el tiempo de ejecución total y el segundo disminuye la latencia
entre los elementos producidos. Aunque las opciones de combinación no siempre tienen
un efecto importante en el rendimiento global de las consultas, pueden influir en el
rendimiento percibido, ya que controlan cuánto tiempo debe esperar un usuario para ver
los resultados.
6. Tipo de creación de particiones.
En algunos casos, una consulta PLINQ sobre una colección de origen indizable puede
producir una carga de trabajo desequilibrada. Cuando suceda, tal vez logre aumentar el
rendimiento de las consultas creando un particionador personalizado.
Cuándo elige PLINQ el modo secuencial
PLINQ siempre intentará ejecutar una consulta con la misma rapidez que si la consulta se
ejecutara secuencialmente. Aunque PLINQ no tenga en cuenta en el costo computacional de los
delegados de usuario ni el tamaño del origen de entrada, sí busca ciertas "formas" de consulta.
Específicamente, busca operadores de consulta o combinaciones de operadores que hacen que
normalmente una consulta se ejecute más despacio en modo paralelo. Cuando encuentra esas
formas, PLINQ vuelve de forma predeterminada al modo secuencial.
Sin embargo, después de medir el rendimiento de una consulta concreta, puede determinar que
realmente se ejecuta más rápidamente en modo paralelo. En casos así puede utilizar la marca
ParallelExecution Mode.ForceParallelism a través del método WithExecutionMode<TSource>
para indicar a PLINQ que paralelice la consulta.
La siguiente lista describe las formas de consulta que PLINQ ejecutará de forma predeterminada
en modo secuencial:
 Consultas que contienen Select, Where indizado, SelectMany indizado o una cláusula
ElementAt después de un operador de clasificación o de filtrado que ha quitado o
reorganizado los índices originales.
 Consultas que contienen un operador Take, TakeWhile, Skip, SkipWhile y donde los
índices de la secuencia de origen no están en el orden original.
 Consultas que contienen zip o SequenceEquals, a menos que uno de los orígenes de
datos tenga un índice ordenado inicialmente y el otro origen de datos sea indizable (IE..
una matriz o IList (T)).
 Consultas que contienen Concat, a menos que se apliquen a orígenes de datos
indizables.
 Consultas que contienen Reverse, a menos que se apliquen a un origen de datos
indizable.
3.2.3. Conservar el orden en PLINQ
En PLINQ, el objetivo es maximizar el rendimiento manteniendo la exactitud. Una consulta se
debería ejecutar lo más rápido que fuese posible pero con resultados correctos. La exactitud
exige que se conserve el orden de la secuencia de origen en algunos casos; sin embargo, la
ordenación puede suponer la utilización de muchos recursos de computación. Por consiguiente,
de forma predeterminada, PLINQ no conserva el orden de la secuencia de origen. En este
sentido, PLINQ se parece a LINQ to SQL, pero es diferente de LINQ to Objects, que conserva
el orden.

MCT: Luis Dueñas Pag 292 de 336


Manual de .NET Framework 4.5

Para reemplazar el comportamiento predeterminado, puede activar la capacidad de conservar el


orden utilizando el operador AsOrdered en la secuencia de origen. Después, puede desactivarla
en la consulta, utilizando el método AsUnordered<TSource>. Con ambos métodos, se procesa la
consulta basándose en la heurística que determina si la consulta se debe ejecutar de forma
paralela o secuencial.
En el siguiente ejemplo se muestra una consulta paralela no ordenada que filtra todos los
elementos que coinciden con una condición, sin intentar ordenar los resultados de forma alguna.
var cityQuery = (from city in cities.AsParallel()
where city.Population > 10000
select city)
.Take(1000);

Esta consulta no obtiene necesariamente las 1000 primeras ciudades de la secuencia de origen
que cumplen la condición, sino que algún conjunto de las 1000 ciudades que la cumplen. Los
operadores de consulta PLINQ particionan la secuencia de origen en varias secuencias
secundarias que se procesan como tareas simultáneas. Si no se especifica que se conserve el
orden, los resultados de cada partición se presentan a la siguiente etapa de la consulta con un
orden arbitrario. Por otra parte, una partición puede producir un subconjunto de los resultados
antes de continuar procesando los elementos restantes. El orden resultante puede ser diferente
cada vez. Una aplicación no puede controlar este hecho, porque depende de cómo programe los
subprocesos el sistema operativo.
En el siguiente ejemplo se reemplaza el comportamiento predeterminado utilizando al operador
AsOrdered en la secuencia de origen. De esta forma se garantiza que el método Take<TSource>
devuelve las 10 primeras ciudades de la secuencia de origen que cumplen la condición.
var orderedCities = (from city in cities.AsParallel().AsOrdered()
where city.Population > 10000
select city)
.Take(1000);

Sin embargo, esta consulta probablemente no se ejecute tan rápido como la versión no ordenada,
porque debe realizar el seguimiento del orden original en todas las particiones y, en el momento
de la combinación, garantizar que el orden es coherente. Por consiguiente, recomendamos usar
AsOrdered solo cuando sea estrictamente necesario y únicamente para las partes de la consulta
que lo requieran. Cuando ya no sea necesario conservar el orden, use AsUnordered<TSource>
para desactivarlo. En el siguiente ejemplo se consigue mediante la creación de dos consultas.
var orderedCities2 = (from city in cities.AsParallel().AsOrdered()
where city.Population > 10000
select city).Take(1000);
var finalResult = from city in orderedCities2.AsUnordered()
join p in people.AsParallel() on city.Name equals p.CityName into details
from c in details select new { Name = city.Name, Pop = city.Population, Mayor
= c.Mayor };
foreach (var city in finalResult) { /*...*/ }

Observe que PLINQ conserva el orden de una secuencia generada por operadores que imponen
el orden para el resto de la consulta. En otras palabras, los operadores de tipo OrderBy y
ThenBy se tratan como si fuesen seguidos de una llamada a AsOrdered.
Operadores de consulta y ordenación
Los siguientes operadores de consulta introducen la conservación del orden en todas las
operaciones posteriores de una consulta o hasta que se llame a AsUnordered<TSource>:
 OrderBy
 OrderByDescending
 ThenBy
 ThenByDescending

MCT: Luis Dueñas Pag 293 de 336


Manual de .NET Framework 4.5

En algunos casos, los siguientes operadores de consulta PLINQ pueden requerir secuencias de
origen ordenadas para generar resultados correctos:
 Reverse<TSource>
 SequenceEqual
 TakeWhile
 SkipWhile
 Zip
Algunos operadores de consulta PLINQ se comportan de manera diferente, dependiendo de si su
secuencia de origen está ordenada o no. En la siguiente tabla se enumeran estos operadores.
Resultado cuando la Resultado cuando la
Operador secuencia de origen está secuencia de origen no está
ordenada ordenada
Salida no determinista para Salida no determinista para
Aggregate operaciones no asociativas o no operaciones no asociativas o
conmutativas. no conmutativas.
All<TSource> No es aplicable No es aplicable
Any No es aplicable No es aplicable
AsEnumerable<TSource> No es aplicable No es aplicable
Salida no determinista para Salida no determinista para
Average operaciones no asociativas o no operaciones no asociativas o
conmutativas. no conmutativas.
Cast<TResult> Resultados ordenados Resultados no ordenados
Concat Resultados ordenados Resultados no ordenados
Count No es aplicable No es aplicable
DefaultIfEmpty No es aplicable No es aplicable
Distinct Resultados ordenados Resultados no ordenados
Se devuelve el elemento
ElementAt<TSource> Elemento arbitrario
especificado
Se devuelve el elemento
ElementAtOrDefault<TSource> Elemento arbitrario
especificado
Except Resultados no ordenados Resultados no ordenados
Se devuelve el elemento
First Elemento arbitrario
especificado
Se devuelve el elemento
FirstOrDefault Elemento arbitrario
especificado
Ejecución no determinista en Ejecución no determinista en
ForAll<TSource>
paralelo paralelo
GroupBy Resultados ordenados Resultados no ordenados
GroupJoin Resultados ordenados Resultados no ordenados
Intersect Resultados ordenados Resultados no ordenados
Join Resultados ordenados Resultados no ordenados
Se devuelve el elemento
Last Elemento arbitrario
especificado
Se devuelve el elemento
LastOrDefault Elemento arbitrario
especificado
LongCount No es aplicable No es aplicable
Min No es aplicable No es aplicable

MCT: Luis Dueñas Pag 294 de 336


Manual de .NET Framework 4.5

Inicia una nueva sección


OrderBy Reordena la secuencia
ordenada
Inicia una nueva sección
OrderByDescending Reordena la secuencia
ordenada
No aplicable (el mismo valor
Range No es aplicable
predeterminado que AsParallel)
No aplicable (mismo valor
Repeat<TResult> No es aplicable
predeterminado que AsParallel)
Reverse<TSource> Invierte el orden No hace nada
Select Resultados ordenados Resultados no ordenados
Select (indizado) Resultados ordenados Resultados no ordenados.
SelectMany Resultados ordenados. Resultados no ordenados
SelectMany (indizado) Resultados ordenados. Resultados no ordenados.
SequenceEqual Comparación ordenada Comparación no ordenada
Single No es aplicable No es aplicable
SingleOrDefault No es aplicable No es aplicable
Skip<TSource> Omite los n primeros elementos Omite cualquier elemento n
No determinista. Ejecuta
SkipWhile Resultados ordenados. SkipWhile en el orden
arbitrario actual
Salida no determinista para Salida no determinista para
Sum operaciones no asociativas o no operaciones no asociativas o
conmutativas. no conmutativas.
Take<TSource> Toma los n primeros elementos Toma cualquier elemento n
No determinista. Ejecuta
TakeWhile Resultados ordenados TakeWhile en el orden
arbitrario actual
ThenBy Complementa OrderBy Complementa OrderBy
ThenByDescending Complementa OrderBy Complementa OrderBy
ToArray<TSource> Resultados ordenados Resultados no ordenados
ToDictionary No es aplicable No es aplicable
ToList<TSource> Resultados ordenados Resultados no ordenados
ToLookup Resultados ordenados Resultados no ordenados
Union Resultados ordenados Resultados no ordenados
Where Resultados ordenados Resultados no ordenados
Where (indizado) Resultados ordenados Resultados no ordenados
Zip Resultados ordenados Resultados no ordenados
Los resultados desordenados no se ordenan aleatoriamente de forma activa; simplemente no se
les aplica ninguna lógica de ordenación especial. En algunos casos, una consulta desordenada
puede conservar el orden de la secuencia de origen. En las consultas que usan el operador Select
indizado, PLINQ garantiza que los elementos de salida aparecerán en el orden de los índices
ascendentes, pero no ofrece ninguna garantía sobre qué índices se asignarán a qué elementos.
3.2.4. Opciones de combinación en PLINQ
Cuando una consulta se ejecuta en paralelo, PLINQ crea particiones en la secuencia de origen
para que varios subprocesos puedan funcionar simultáneamente en diferentes partes, por lo
general en subprocesos independientes. Si los resultados se van a usar en un subproceso, por
ejemplo, en un bucle foreach (For Each en Visual Basic), los resultados de cada subproceso

MCT: Luis Dueñas Pag 295 de 336


Manual de .NET Framework 4.5

deben volver a combinarse en una secuencia. El tipo de combinación que PLINQ realiza
depende de los operadores que se encuentran en la consulta. Por ejemplo, los operadores que
imponen un nuevo orden en los resultados deben almacenar en búfer todos los elementos de
todos los subprocesos. Desde el punto de vista del subproceso utilizado (qué también es el del
usuario de la aplicación), una consulta totalmente almacenada en búfer podría ejecutarse durante
un período notable de tiempo antes de generar su primer resultado. Otros operadores se
almacenan parcialmente en búfer de forma predeterminada; producen sus resultados en lotes. Un
operador ForAll<TSource> no se almacena en búfer de forma predeterminada. Produce
inmediatamente todos los elementos de todos los subprocesos.
Como se muestra en el siguiente ejemplo, con el método WithMergeOptions<TSource>, puede
proporcionar a PLINQ una sugerencia que indica el tipo de combinación que se va a realizar.
var scanLines = from n in nums.AsParallel()
.WithMergeOptions(ParallelMergeOptions.NotBuffered)
where n % 2 == 0
select ExpensiveFunc(n);

Si la consulta determinada no puede admitir la opción solicitada, esta se omitirá. En la mayoría


de los casos, no tiene que especificar una opción de combinación para una consulta PLINQ. Sin
embargo, en algunos casos puede encontrar mediante pruebas y mediciones que una consulta se
ejecuta mejor en un modo no predeterminado. Un uso común de esta opción es forzar a un
operador de combinación de fragmentos a transmitir en secuencias sus resultados para
proporcionar una interfaz de usuario más sensible.
ParallelMergeOptions
La enumeración ParallelMergeOptions incluye las siguientes opciones que especifican, para las
formas de la consulta compatibles, cómo se proporciona el resultado final de la consulta cuando
se usan los resultados en un subproceso:
 Not Buffered
La opción NotBuffered hace que cada subproceso devuelva cada elemento procesado en
cuanto se genere. Este comportamiento es análogo a la "transmisión por secuencias" del
resultado. Si el operador AsOrdered se encuentra en la consulta, NotBuffered conserva
el orden de los elementos de origen. Aunque NotBuffered empieza a proporcionar los
resultados en cuanto están disponibles, el tiempo total para generar todos los resultados
podría ser aún más prolongado que al usar una de las otras opciones de combinación.
 Auto Buffered
La opción de AutoBuffered hace la consulta para obtener elementos en un búfer y
después para provocar periódicamente el contenido del búfer de una vez en el
subproceso utilizado. Esto es análogo a proporcionar los datos de origen en
"fragmentos" en lugar de usar el comportamiento de "transmisión por secuencias" de
NotBuffered. AutoBuffered puede llevar mucho más tiempo que NotBuffered para
hacer que el primer elemento esté disponible en el subproceso utilizado. El tamaño del
búfer y el comportamiento productivo exacto no se puede configurar y puede variar, en
función de varios factores relacionados con la consulta.
 FullyBuffered
La opción FullyBuffered hace que el resultado de la consulta completa se almacene en
búfer antes de que se proporcione cualquiera de los elementos. Cuando se usa esta
opción, puede llevar mucho más tiempo que el primer elemento esté disponible en el
subproceso utilizado, pero los resultados completos se podrían generar aún más
rápidamente que al usar las otras opciones.
Operadores de consulta que admiten opciones de combinación

MCT: Luis Dueñas Pag 296 de 336


Manual de .NET Framework 4.5

En la siguiente tabla se hace una lista de los operadores que admiten todos los modos de opción
de combinación, sujetos a las restricciones especificadas.
Operador Restricciones
AsEnumerable<TSource> None
Cast<TResult> None
Concat Consultas no ordenadas que solo tienen un origen de matriz o lista.
DefaultIfEmpty None
OfType<TResult> None
Reverse<TSource> Consultas no ordenadas que solo tienen un origen de matriz o lista.
Select None
SelectMany None
Skip<TSource> None
Take<TSource> None
Where None
Todos los demás operadores de consulta PLINQ podrían omitir las opciones de combinación
proporcionadas por el usuario. Algunos operadores de consulta, por ejemplo,
Reverse<TSource> y OrderBy, no pueden proporcionar ningún elemento hasta que todos se
hayan generado y reordenado. Por consiguiente, cuando se usa ParallelMergeOptions en una
consulta que también contiene a operador como Reverse<TSource>, el comportamiento de
combinación no se aplicará en la consulta hasta que ese operador haya generado sus resultados.
La capacidad de algunos operadores para controlar las opciones de combinación depende del
tipo de la secuencia de origen y de si el operador AsOrdered se usó anteriormente en la consulta.
ForAll<TSource> siempre es NotBuffered; proporciona sus elementos inmediatamente.
OrderBy siempre es FullyBuffered; debe ordenar la lista completa antes de proporcionar
resultados.
3.2.5. Posibles problemas con PLINQ
En muchos casos, PLINQ puede proporcionar importantes mejoras de rendimiento con respecto
a las consultas LINQ to Objects. Sin embargo, el trabajo de paralelizar la ejecución de las
consultas aporta una complejidad que puede conducir a problemas que, en código secuencial, no
son tan comunes o que no se producen en absoluto. En este tema se indican algunas prácticas
que se deben evitar al escribir consultas PLINQ.
No se debe suponer que la ejecución en paralelo es siempre más rápida
En ocasiones, la paralelización hace que una consulta PLINQ se ejecute con mayor lentitud que
su equivalente LINQ to Objects. La regla básica es que no es probable que las consultas con
pocos elementos de origen y delegados de usuario rápidos vayan mucho más rápido. Sin
embargo, dado que hay muchos factores que afectan al rendimiento, se recomienda medir los
resultados reales antes de decidir si se usa PLINQ.
Evitar la escritura en ubicaciones de memoria compartidas
En código secuencial, no es raro leer o escribir en variables estáticas o en campos de clase. Sin
embargo, cuando varios subprocesos tienen acceso a estas variables de forma simultánea, hay
grandes posibilidades de que se produzcan condiciones de carrera. Aunque se pueden usar
bloqueos para sincronizar el acceso a la variable, el costo de la sincronización puede afectar
negativamente al rendimiento. Por tanto, se recomienda evitar, o al menos limitar, el acceso al
estado compartido en una consulta PLINQ en la medida de lo posible.
Evitar la paralelización excesiva

MCT: Luis Dueñas Pag 297 de 336


Manual de .NET Framework 4.5

Si usa el operador AsParallel, incurre en costos de sobrecarga al crear particiones de la


colección de origen y sincronizar los subprocesos de trabajo. El número de procesadores del
equipo reduce también las ventajas de la paralelización. Si se ejecutan varios subprocesos
enlazados a cálculos en un único procesador, no se gana en velocidad. Por tanto, debe tener
cuidado para no paralelizar en exceso una consulta.
El escenario más común en el que se puede producir un exceso de paralelización son las
consultas anidadas, tal como se muestra en el siguiente fragmento de código.
var q = from cust in customers.AsParallel()
from order in cust.Orders.AsParallel()
where order.OrderDate > date
select new { cust, order };

En este caso, es mejor paralelizar únicamente el origen de datos exterior (clientes) a menos que
se cumplan una o más de las siguientes condiciones:
 Se sabe que el origen de datos interno (cust.Orders) es muy largo.
 Se realiza un cálculo caro en cada pedido. (La operación que se muestra en el ejemplo
no es cara.)
 Se sabe que el sistema de destino tiene suficientes procesadores como para controlar el
número de subprocesos que se producirán al paralelizar la consulta de cust.Orders.
En todos los casos, la mejor manera de determinar la forma óptima de la consulta es mediante la
prueba y la medición.
Evitar llamadas a métodos que no son seguros para subprocesos
La escritura en métodos de instancia que no son seguros para subprocesos de una consulta
PLINQ puede producir daños en los datos, que pueden pasar o no desapercibidos en el
programa. También puede dar lugar a excepciones. En el siguiente ejemplo, varios subprocesos
estarían intentando llamar simultáneamente al método Filestream.Write, lo que la clase no
admite.
FileStream fs = File.OpenWrite(...);
a.Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));

Limitar las llamadas a métodos seguros para subprocesos


La mayoría de los métodos estáticos de .NET Framework son seguros para subprocesos y se les
pueden llamar simultáneamente desde varios subprocesos. Sin embargo, incluso en estos casos,
la sincronización que esto supone puede conducir a una ralentización importante en la consulta.
Nota
Puede comprobarlo si inserta algunas llamadas a WriteLine en las consultas. Si bien este método
se utiliza en los ejemplos de la documentación para realizar una demostración, no debe usarlo en
las consultas PLINQ.
Evitar operaciones de ordenación innecesarias
Cuando PLINQ ejecuta una consulta en paralelo, divide la secuencia de origen en particiones
con las que se puede trabajar de forma simultánea en varios subprocesos. De forma
predeterminada, el orden en que se procesan las particiones y se entregan los resultados no es
predecible (excepto para operadores como OrderBy). Puede indicar a PLINQ que conserve la
ordenación de las secuencias de origen, pero esto tiene un impacto negativo en el rendimiento.
El procedimiento recomendado, siempre que sea posible, consiste en estructurar las consultas de
forma que no dependan del mantenimiento del orden.
Preferir ForAll a ForEach cuando sea posible
Si bien PLINQ ejecuta una consulta en varios subprocesos, si utiliza los resultados en un bucle
foreach (For Each en Visual Basic), los resultados de la consulta se deben combinar de nuevo en

MCT: Luis Dueñas Pag 298 de 336


Manual de .NET Framework 4.5

un único subproceso y el enumerador debe tener acceso a ellos en serie. En algunos casos, esto
es inevitable; sin embargo, siempre que sea posible, utilice el método ForAll para permitir que
cada subproceso genera sus propios resultados, por ejemplo, escribiendo en una colección
segura para subprocesos como System.Collections.Concurrent.ConcurrentBag<T>.
El mismo problema se aplica al Parallel.ForEach In Otros Words,
source.AsParallel().Where().ForAll(...) debe .forall (...) a
Parallel.ForEach(source.AsParallel().Where(), ...).
Ser consciente de los problemas de afinidad de los subprocesos
Algunas tecnologías, como la interoperabilidad COM para componentes STA (contenedor
uniproceso), Windows Forms y Windows Presentation Foundation (WPF), imponen
restricciones de afinidad de subprocesos que exigen que el código se ejecute en un subproceso
determinado. Por ejemplo, tanto en Windows Forms como en WPF, solo se puede tener acceso a
un control en el subproceso donde se creó. Si intenta tener acceso al estado compartido de un
control Windows Forms en una consulta PLINQ, se produce una excepción si está ejecuta en el
depurador. (Este valor se puede desactivar.) Sin embargo, si la consulta se utiliza en el
subproceso de la interfaz de usuario, puede tener acceso al control desde el bucle foreach que
enumera los resultados de la consulta, ya que este código se ejecuta en un único subproceso.
No se debe suponer que las iteraciones de ForEach, For y ForAll siempre se ejecutan en
paralelo
Es importante tener presente que las iteraciones individuales de un bucle Parallel.For,
Parallel.ForEach o ForAll<TSource> tal vez no tengan que ejecutarse en paralelo. Por
consiguiente, se debe evitar escribir código cuya exactitud dependa de la ejecución en paralelo
de las iteraciones o de la ejecución de las iteraciones en algún orden concreto.
Por ejemplo, es probable que este código lleve a un interbloqueo:
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, ProcessorCount * 100).AsParallel().ForAll((j)
=>
{
if (j == Environment.ProcessorCount)
{
Console.WriteLine("Set on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j);
mre.Set();
}
else
{
Console.WriteLine("Waiting on {0} with value of {1}",
Thread.CurrentThread.ManagedThreadId, j);
mre.Wait();
}
}); //deadlocks

En este ejemplo, una iteración establece un evento y el resto de las iteraciones esperan el evento.
Ninguna de las iteraciones que esperan puede completarse hasta que se haya completado la
iteración del valor de evento. Sin embargo, es posible que las iteraciones que esperan bloqueen
todos los subprocesos que se utilizan para ejecutar el bucle paralelo, antes de que la iteración del
valor de evento haya tenido oportunidad de ejecutarse. Esto produce un interbloqueo: la
iteración del valor de evento nunca se ejecutará y las iteraciones que esperan nunca se activarán.
En concreto, una iteración de un bucle paralelo no debe esperar nunca otra iteración del bucle
para progresar. Si el bucle paralelo decide programar las iteraciones secuencialmente pero en el
orden contrario, se producirá un interbloqueo.
3.2.6. Cómo: Crear y ejecutar una consulta PLINQ simple

MCT: Luis Dueñas Pag 299 de 336


Manual de .NET Framework 4.5

En el siguiente ejemplo se muestra cómo crear un consulta LINQ paralela simple utilizando el
método de extensión AsParallel de la secuencia de origen y cómo ejecutarla utilizando el
método ForAll<TSource>.
Nota
En esta documentación, se utilizan expresiones lambda para definir delegados en la PLINQ.
Ejemplo
var source = Enumerable.Range(100, 20000);
// Result sequence might be out of order.
var parallelQuery = from num in source.AsParallel()
where num % 10 == 0
select num;
// Process result sequence in parallel
parallelQuery.ForAll((e) => DoSomething(e));
// Or use foreach to merge results first.
foreach (var n in parallelQuery)
{
Console.WriteLine(n);
}
// You can also use ToArray, ToList, etc
// as with LINQ to Objects.
var parallelQuery2 = (from num in source.AsParallel()
where num % 10 == 0
select num).ToArray();
// Method syntax is also supported
var parallelQuery3 = source.AsParallel().Where(n => n % 10 == 0).
Select(n => n);

En este ejemplo se muestra el modelo básico para crear y ejecutar cualquier consulta LINQ
paralela cuando la clasificación de la secuencia del resultado no es importante; las consultas no
ordenadas son generalmente más rápidas que las ordenadas. La consulta crea particiones del
origen en tareas que se ejecutan de forma asincrónica en varios subprocesos. El orden en que se
completa cada tarea depende no solo de la cantidad de trabajo que se precisa para procesar los
elementos de la partición, sino también de factores externos, como la forma en que el sistema
operativo programa cada subproceso., Este ejemplo está diseñado para mostrar el uso y podría
no ejecutarse más rápidamente que la consulta secuencial equivalente de LINQ to Objects.
3.2.7. Cómo: Controlar la ordenación en una consulta PLINQ
En estos ejemplos, se muestra cómo se controla el orden en una consulta PLINQ usando el
método de extensión AsOrdered.
Precaución
Estos ejemplos sirven principalmente para mostrar el uso y podría no ejecutarse más
rápidamente que la consulta secuencial equivalente de LINQ to Objects.
Ejemplo
En el siguiente ejemplo se mantiene el orden de la secuencia de origen. A veces esto resulta
necesario, por ejemplo, cuando algunos operadores de consulta necesitan una secuencia de
origen ordenada para generar los resultados correctos.
var source = Enumerable.Range(9, 10000);
// Source is ordered; let's preserve it.
var parallelQuery = from num in source.AsParallel().AsOrdered()
where num % 3 == 0
select num;
// Use foreach to preserve order at execution time.
foreach (var v in parallelQuery)
Console.Write("{0} ", v);
// Some operators expect an ordered source sequence.
var lowValues = parallelQuery.Take(10);

MCT: Luis Dueñas Pag 300 de 336


Manual de .NET Framework 4.5

En el siguiente ejemplo se muestran algunos operadores de consulta cuya secuencia de origen es


muy probable que esté ordenada. Estos operadores pueden funcionar en secuencias no
ordenadas, aunque pueden generar resultados inesperados.
// Paste into PLINQDataSample class.
static void SimpleOrdering()
{
var customers = GetCustomers();
// Take the first 20, preserving the original order
var firstTwentyCustomers = customers
.AsParallel()
.AsOrdered()
.Take(20);
foreach (var c in firstTwentyCustomers)
Console.Write("{0} ", c.CustomerID);
// All elements in reverse order.
var reverseOrder = customers
.AsParallel()
.AsOrdered()
.Reverse();
foreach (var v in reverseOrder)
Console.Write("{0} ", v.CustomerID);
// Get the element at a specified index.
var cust = customers.AsParallel()
.AsOrdered()
.ElementAt(48);
Console.WriteLine("Element #48 is: {0}", cust.CustomerID);
}

En el ejemplo siguiente se muestra cómo se mantiene el orden en la primera parte de una


consulta, cómo se quita el orden para aumentar el rendimiento de una cláusula de combinación y
cómo se aplica de nuevo el orden a la secuencia del resultado final.
// Paste into PLINQDataSample class.
static void OrderedThenUnordered()
{
var orders = GetOrders();
var orderDetails = GetOrderDetails();
var q2 = orders.AsParallel()
.Where(o => o.OrderDate < DateTime.Parse("07/04/1997"))
.Select(o => o)
.OrderBy(o => o.CustomerID) // Preserve original ordering for Take
operation.
.Take(20)
.AsUnordered() // Remove ordering constraint to make join faster.
.Join(
orderDetails.AsParallel(),
ord => ord.OrderID,
od => od.OrderID,
(ord, od) =>
new
{
ID = ord.OrderID,
Customer = ord.CustomerID,
Product = od.ProductID
}
)
.OrderBy(i => i.Product); // Apply new ordering to final result
sequence.
foreach (var v in q2)
Console.WriteLine("{0} {1} {2}", v.ID, v.Customer, v.Product);

3.2.8. Cómo: Combinar consultas LINQ paralelas y secuenciales


En este ejemplo, se muestra cómo usar el método AsSequential<TSource> para indicar a
PLINQ que procese secuencialmente todos los operadores posteriores de la consulta. Aunque el

MCT: Luis Dueñas Pag 301 de 336


Manual de .NET Framework 4.5

procesamiento secuencial es por lo general más lento que el procesamiento en paralelo, a veces
es necesario para generar resultados correctos.
Precaución
Este ejemplo está diseñado para mostrar el uso y podría no ejecutarse más rápidamente que la
consulta secuencial equivalente de LINQ to Objects.
Ejemplo
En el siguiente ejemplo se muestra un escenario en el que se requiere AsSequential<TSource>,
a saber, para conservar el orden que se estableció en una cláusula anterior de la consulta.
// Paste into PLINQDataSample class.
static void SequentialDemo()
{
var orders = GetOrders();
var query = (from ord in orders.AsParallel()
orderby ord.CustomerID
select new
{
Details = ord.OrderID,
Date = ord.OrderDate,
Shipped = ord.ShippedDate
}).AsSequential().Take(5);
}

3.2.9. Cómo: Controlar excepciones en una consulta PLINQ


En el primer ejemplo de este tema se muestra cómo controlar la excepción
System.AggregateException que se puede iniciar desde una consulta PLINQ cuando se ejecuta.
En el segundo ejemplo se muestra cómo incluir bloques try-catch dentro de los delegados, lo
más próximos posibles al punto donde se iniciará la excepción. De esta manera, se pueden
detectar en cuanto se producen y, posiblemente, continuar con la ejecución de la consulta.
Cuando las excepciones pueden propagarse de nuevo al subproceso de unión, es posible que una
consulta continúe procesando algunos elementos después de que se haya producido la
excepción.
En algunos casos, cuando PLINQ utiliza la ejecución secuencial y se produce una excepción, es
posible que esta se propague directamente y no se encapsule en una excepción
AggregateException. Además, las excepciones ThreadAbortException siempre se propagan
directamente.
Nota
Cuando está habilitada la opción "Solo mi código", 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.
Este ejemplo está diseñado para mostrar el uso y podría no ejecutarse más rápidamente que la
consulta secuencial equivalente de LINQ to Objects.
Ejemplo
En este ejemplo se muestra cómo colocar los bloques try-catch alrededor del código que ejecuta
la consulta para detectar las excepciones System.AggregateExceptions que se produzcan.
// Paste into PLINQDataSample class.
static void PLINQExceptions_1()
{
// Using the raw string array here. See PLINQ Data Sample.
string[] customers = GetCustomersAsStrings().ToArray();

MCT: Luis Dueñas Pag 302 de 336


Manual de .NET Framework 4.5

// First, we must simulate some currupt input.


customers[54] = "###";
var parallelQuery = from cust in customers.AsParallel()
let fields = cust.Split(',')
where fields[3].StartsWith("C") //throw
indexoutofrange
select new { city = fields[3],
thread = Thread.CurrentThread.ManagedThreadId };
try
{
// We use ForAll although it doesn't really improve performance
// since all output is serialized through the Console.
parallelQuery.ForAll(e => Console.WriteLine("City: {0}, Thread:{1}",
e.city,
e.thread));
}
// In this design, we stop query processing when the exception occurs.
catch (AggregateException e)
{
foreach (var ex in e.InnerExceptions)
{
Console.WriteLine(ex.Message);
if (ex is IndexOutOfRangeException)
Console.WriteLine("The data source is corrupt. Query
stopped.");
}
}
}

En este ejemplo, la consulta no puede continuar una vez producida la excepción. Cuando el
código de aplicación detecta la excepción, PLINQ ya ha detenido la consulta en todos los
subprocesos.
En el siguiente ejemplo se muestra cómo colocar un bloque try-catch en un delegado para
permitir que se detecte una excepción y que continúe la ejecución de la consulta.
// Paste into PLINQDataSample class.
static void PLINQExceptions_2()
{
var customers = GetCustomersAsStrings().ToArray();
// Using the raw string array here.
// First, we must simulate some currupt input
customers[54] = "###";
// Create a delegate with a lambda expression.
// Assume that in this app, we expect malformed data
// occasionally and by design we just report it and continue.
Func<string[], string, bool> isTrue = (f, c) =>
{
try
{
string s = f[3];
return s.StartsWith(c);
}
catch (IndexOutOfRangeException e)
{
Console.WriteLine("Malformed cust: {0}", f);
return false;
}
};
// Using the raw string array here
var parallelQuery = from cust in customers.AsParallel()
let fields = cust.Split(',')
where isTrue(fields, "C")
//use a named delegate with a try-catch
select new { city = fields[3] };
try
{
// We use ForAll although it doesn't really improve performance

MCT: Luis Dueñas Pag 303 de 336


Manual de .NET Framework 4.5

// since all output must be serialized through the Console.


parallelQuery.ForAll(e => Console.WriteLine(e.city));
}
//IndexOutOfRangeException will not bubble up because we handle it where it
is thrown
catch (AggregateException e)
{
foreach (var ex in e.InnerExceptions)
Console.WriteLine(ex.Message);
}
}

3.2.10. Cómo: Cancelar una consulta PLINQ


En los siguientes ejemplos se muestran dos maneras de cancelar una consulta PLINQ. En el
primer ejemplo se muestra cómo cancelar una consulta que consta principalmente de recorridos
de datos. En el segundo ejemplo se muestra cómo cancelar una consulta que contiene una
función de usuario que resulta cara en los cálculos.
Nota
Cuando está habilitada la opción "Solo mi código", 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.
Este ejemplo está diseñado para mostrar el uso y podría no ejecutarse más rápidamente que la
consulta secuencial equivalente de LINQ to Objects.
Ejemplo
namespace PLINQCancellation_1
{
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static void Main(string[] args)
{
int[] source = Enumerable.Range(1, 10000000).ToArray();
CancellationTokenSource cs = new CancellationTokenSource();
// Start a new asynchronous task that will cancel the
// operation from another thread. Typically you would call
// Cancel() in response to a button click or some other
// user interface event.
Task.Factory.StartNew(() =>
{
UserClicksTheCancelButton(cs);
});
int[] results = null;
try
{
results = (from num in
source.AsParallel().WithCancellation(cs.Token)
where num % 3 == 0
orderby num descending
select num).ToArray();
}
catch (OperationCanceledException e)
{
Console.WriteLine(e.Message);
}

MCT: Luis Dueñas Pag 304 de 336


Manual de .NET Framework 4.5

catch (AggregateException ae)


{
if (ae.InnerExceptions != null)
{
foreach (Exception e in ae.InnerExceptions)
Console.WriteLine(e.Message);
}
}
if (results != null)
{
foreach (var v in results) Console.WriteLine(v);
}
Console.WriteLine();
Console.ReadKey();
}

static void UserClicksTheCancelButton(CancellationTokenSource cs)


{
// Wait between 150 and 500 ms, then cancel.
// Adjust these values if necessary to make
// cancellation fire while query is still executing.
Random rand = new Random();
Thread.Sleep(rand.Next(150, 350));
cs.Cancel();
}
}
}

El marco de PLINQ no incluye una única excepción OperationCanceledException en


System.Aggregate Exception; OperationCanceledException se debe controlar en un bloque
catch independiente. Si uno o más delegados de usuario inician una excepción
OperationCanceledException (externalCT) (mediante una estructura
System.Threading.CancellationToken externa) pero ninguna otra excepción, y la consulta se
definió como AsParallel().WithCancellation(externalCT), PLINQ emitirá una única excepción
OperationCanceledException (externalCT) en lugar de System.AggregateException. Sin
embargo, si un delegado de usuario inicia una excepción OperationCanceledException y otro
delegado inicia otro tipo de excepción, ambas excepciones se incluyen en AggregateException.
A continuación se proporciona una orientación general sobre la cancelación:
1. Si realiza una cancelación del delegado de usuario, debería informar a PLINQ sobre la
estructura CancellationToken externa e iniciar una excepción
OperationCanceledException (externalCT).
2. Si se produce la cancelación y no se inicia ninguna otra excepción, debería controlar
una clase OperationCanceledException en lugar de AggregateException.
En el siguiente ejemplo se muestra cómo controlar la cancelación cuando se cuenta con una
función cara en cálculos en el código de usuario.
namespace PLINQCancellation_2
{
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static void Main(string[] args)
{
int[] source = Enumerable.Range(1, 10000000).ToArray();
CancellationTokenSource cs = new CancellationTokenSource();
// Start a new asynchronous task that will cancel the
// operation from another thread. Typically you would call
// Cancel() in response to a button click or some other
// user interface event.

MCT: Luis Dueñas Pag 305 de 336


Manual de .NET Framework 4.5

Task.Factory.StartNew(() =>
{
UserClicksTheCancelButton(cs);
});
double[] results = null;
try
{
results = (from num in
source.AsParallel().WithCancellation(cs.Token)
where num % 3 == 0
select Function(num, cs.Token)).ToArray();
}
catch (OperationCanceledException e)
{
Console.WriteLine(e.Message);
}
catch (AggregateException ae)
{
if (ae.InnerExceptions != null)
{
foreach (Exception e in ae.InnerExceptions)
Console.WriteLine(e.Message);
}
}
if (results != null)
{
foreach (var v in results) Console.WriteLine(v);
}
Console.WriteLine();
Console.ReadKey();
}

// A toy method to simulate work.


static double Function(int n, CancellationToken ct)
{
// If work is expected to take longer than 1 ms
// then try to check cancellation status more often within that
work.
for (int i = 0; i < 5; i++)
{
// Work hard for approx 1 millisecond.
Thread.SpinWait(50000);
// Check for cancellation request.
ct.ThrowIfCancellationRequested();
}
// Anything will do for our purposes.
return Math.Sqrt(n);
}

static void UserClicksTheCancelButton(CancellationTokenSource cs)


{
// Wait between 150 and 500 ms, then cancel.
// Adjust these values if necessary to make
// cancellation fire while query is still executing.
Random rand = new Random();
Thread.Sleep(rand.Next(150, 350));
Console.WriteLine("Press 'c' to cancel");
if (Console.ReadKey().KeyChar == 'c') cs.Cancel();
}
}
}

Al controlar la cancelación en el código de usuario, no es necesario usar


WithCancellation<TSource> en la definición de la consulta. Sin embargo, recomendamos usarlo
ya que WithCancellation<TSource> no tiene ningún efecto en el rendimiento de la consulta y
permite controlar la cancelación con operadores de consulta y con el código de usuario.

MCT: Luis Dueñas Pag 306 de 336


Manual de .NET Framework 4.5

Para garantizar la capacidad de respuesta del sistema, se recomienda comprobar la cancelación


alrededor de una vez por milisegundo; también se considera aceptable, sin embargo, un período
máximo de 10 milisegundos. Esta frecuencia no tiene por qué tener un impacto negativo en el
rendimiento del código.
Cuando se elimina un enumerador, por ejemplo cuando el código sale de un bucle foreach (For
Each en Visual Basic) que está iterando en los resultados de la consulta, la consulta se cancela
pero no se inicia ninguna excepción.
3.2.11. Cómo: Escribir una función de agregado personalizada de
PLINQ
En este ejemplo, se muestra cómo utilizar el método Aggregate para aplicar una función de
agregación personalizada a una secuencia de origen.
Ejemplo
En el siguiente ejemplo se calcula la desviación estándar de una secuencia de enteros.
namespace PLINQAggregation
{
using System;
using System.Linq;

class aggregation
{
static void Main(string[] args)
{
// Create a data source for demonstration purposes.
int[] source = new int[100000];
Random rand = new Random();
for (int x = 0; x < source.Length; x++)
{
// Should result in a mean of approximately 15.0.
source[x] = rand.Next(10, 20);
}
// Standard deviation calculation requires that we first
// calculate the mean average. Average is a predefined
// aggregation operator, along with Max, Min and Count.
double mean = source.AsParallel().Average();
// We use the overload that is unique to ParallelEnumerable. The
// third Func parameter combines the results from each thread.
double standardDev = source.AsParallel().Aggregate(
// initialize subtotal. Use decimal point to tell
// the compiler this is a type double. Can also use: 0d.
0.0,
// do this on each thread
(subtotal, item) => subtotal + Math.Pow((item - mean), 2),
// aggregate results after all threads are done.
(total, thisThread) => total + thisThread,
// perform standard deviation calc on the aggregated result.
(finalSum) => Math.Sqrt((finalSum / (source.Length - 1)))
);
Console.WriteLine("Mean value is = {0}", mean);
Console.WriteLine("Standard deviation is {0}", standardDev);
Console.ReadLine();
}
}
}

En este ejemplo se utiliza una sobrecarga del operador de consulta estándar Aggregate, que solo
existe en PLINQ. Esta sobrecarga toma un delegado System.Func<T1, T2, TResult> adicional
como tercer parámetro de entrada. Este delegado combina los resultados de todos los
subprocesos antes de realizar el cálculo final en los resultados agregados. En este ejemplo
sumamos las sumas de todos los subprocesos.

MCT: Luis Dueñas Pag 307 de 336


Manual de .NET Framework 4.5

Tenga en cuenta que cuando el cuerpo de una expresión lambda está compuesto por una única
expresión, el valor devuelto del delegado System.Func<T, TResult> es el valor de la expresión.
3.2.12. Cómo: Especificar el modo de ejecución en PLINQ
En este ejemplo se muestra cómo forzar a PLINQ a omitir su heurística predeterminada y
ejecutar en paralelo una consulta sin tener en cuenta la forma de la consulta.
Ejemplo
// Paste into PLINQDataSample class.
static void ForceParallel()
{
var customers = GetCustomers();
var parallelQuery = (from cust in customers.AsParallel()

.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
where cust.City == "Berlin"
select cust.CustomerName)
.ToList();
}

PLINQ está diseñado para aprovechar las oportunidades para la ejecución en paralelo. Sin
embargo, no todas las consultas se benefician de la ejecución en paralelo. Por ejemplo, cuando
una consulta contiene un delegado de usuario único que hace muy poco trabajo, la consulta
normalmente se ejecutará más rápidamente de forma secuencial. Esto se debe a que la
sobrecarga necesaria para habilitar la ejecución en paralelo es más costosa que la velocidad que
se obtiene. Por consiguiente, PLINQ no ejecuta en paralelo cada consulta de forma automática.
Primero examina la forma de la consulta y los diversos operadores que la comprenden. En
función de este análisis, PLINQ en el modo de ejecución predeterminado puede decidir ejecutar
algunas consultas o todas ellas secuencialmente. Sin embargo, en algunos casos puede saber
sobre su consulta más de lo que PLINQ puede determinar a partir de su análisis. Por ejemplo,
puede saber que un delegado es muy costoso y que la consulta se beneficiará definitivamente de
la ejecución en paralelo. En esos casos, puede usar el método WithExecutionMode<TSource> y
especificar el valor ForceParallelism para indicar a PLINQ que ejecute siempre la consulta en
paralelo.
3.2.13. Cómo: Especificar opciones de combinación en PLINQ
En este ejemplo se muestra cómo especificar las opciones de combinación que se aplicarán a
todos los operadores subsiguientes en una consulta PLINQ. No tiene que establecer las opciones
de combinación explícitamente, pero, si lo hace, puede mejorar rendimiento.
Ejemplo
En el siguiente ejemplo se muestra el comportamiento de las opciones de combinación en un
escenario básico que tiene un origen no ordenado y aplica una función que utiliza muchos
recursos a cada elemento.
namespace MergeOptions
{
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;

class Program
{
static void Main(string[] args)
{
var nums = Enumerable.Range(1, 10000);
// Replace NotBuffered with AutoBuffered
// or FullyBuffered to compare behavior.
var scanLines = from n in nums.AsParallel()

MCT: Luis Dueñas Pag 308 de 336


Manual de .NET Framework 4.5

.WithMergeOptions(ParallelMergeOptions.NotBuffered)
where n % 2 == 0
select ExpensiveFunc(n);
Stopwatch sw = Stopwatch.StartNew();
foreach (var line in scanLines)
{
Console.WriteLine(line);
}
Console.WriteLine("Elapsed time: {0} ms. Press any key to exit.",
sw.ElapsedMilliseconds);
Console.ReadKey();
}
// A function that demonstrates what a fly sees when it watches
television
static string ExpensiveFunc(int i)
{
Thread.SpinWait(2000000);
return String.Format("{0}
*****************************************", i);
}
}
}

Cuando la opción AutoBuffered incurre en una latencia no deseable antes de proporcionar el


primer elemento, pruebe la opción NotBuffered para proporcionar elementos del resultado de
una manera más rápida y sencilla.
3.2.14. Cómo: Recorrer en iteración directorios con PLINQ
En este ejemplo se muestran dos maneras sencillas de paralelizar las operaciones en los
directorios de archivos. En la primera consulta se usa el método GetFiles para rellenar una
matriz de nombres de archivo en un directorio y todos los subdirectorios. Este método no
devuelve ningún valor hasta que se rellena la matriz completa y, por consiguiente, puede
presentar la latencia al principio de la operación. Sin embargo, una vez que se rellena la matriz,
PLINQ puede procesarla en paralelo muy rápidamente.
En la segunda consulta se usan los métodos estáticos EnumerateDirectories y EnumerateFiles,
que empiezan a devolver resultados inmediatamente. Este enfoque puede ofrecer mayor
velocidad cuando se itera en árboles de directorios grandes, aunque, en comparación con el
primer ejemplo, el tiempo de procesamiento puede depender de muchos factores.
Ejemplo
En el ejemplo siguiente se muestra cómo iterar en los directorios de archivos en escenarios
simples cuando se tiene acceso a todos los directorios en el árbol, los tamaños de archivo no son
muy grandes y los tiempos de acceso no son significativos. Este enfoque implica un período de
latencia al principio mientras la matriz de nombres de archivo se construye.
struct FileResult
{
public string Text;
public string FileName;
}

// Use Directory.GetFiles to get the source sequence of file names.


public static void FileIteration_1(string path)
{
var sw = Stopwatch.StartNew();
int count = 0;
string[] files = null;
try
{
files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);
}
catch (UnauthorizedAccessException e)

MCT: Luis Dueñas Pag 309 de 336


Manual de .NET Framework 4.5

{
Console.WriteLine("You do not have permission to access one or more
folders
in this directory tree.");
return;
}
catch (FileNotFoundException)
{
Console.WriteLine("The specified directory {0} was not found.", path);
}
var fileContents = from file in files.AsParallel()
let extension = Path.GetExtension(file)
where extension == ".txt" || extension == ".htm"
let text = File.ReadAllText(file)
select new FileResult { Text = text , FileName = file };
//Or ReadAllBytes, ReadAllLines, etc.
try
{
foreach (var item in fileContents)
{
Console.WriteLine(Path.GetFileName(item.FileName) + ":" +
item.Text.Length);
count++;
}
}
catch (AggregateException ae)
{
ae.Handle((ex) =>
{
if (ex is UnauthorizedAccessException)
{
Console.WriteLine(ex.Message);
return true;
}
return false;
});
}
Console.WriteLine("FileIteration_1 processed {0} files in {1}
milliseconds",
count, sw.ElapsedMilliseconds);
}

En el ejemplo siguiente se muestra cómo iterar en los directorios de archivos en escenarios


simples cuando se tiene acceso a todos los directorios en el árbol, los tamaños de archivo no son
muy grandes y los tiempos de acceso no son significativos. Este enfoque empieza a producir
resultados más rápidamente que el ejemplo anterior.
struct FileResult
{
public string Text;
public string FileName;
}

// Use Directory.EnumerateDirectories and EnumerateFiles to get the source


sequence of file names.
public static void FileIteration_2(string path) //225512 ms
{
var count = 0;
var sw = Stopwatch.StartNew();
var fileNames = from dir in Directory.EnumerateFiles(path, "*.*",
SearchOption.AllDirectories) select dir;
var fileContents = from file in fileNames.AsParallel()
// Use AsOrdered to preserve source ordering
let extension = Path.GetExtension(file)
where extension == ".txt" || extension == ".htm"
let Text = File.ReadAllText(file)
select new { Text, FileName = file };
//Or ReadAllBytes, ReadAllLines, etc.

MCT: Luis Dueñas Pag 310 de 336


Manual de .NET Framework 4.5

try
{
foreach (var item in fileContents)
{
Console.WriteLine(Path.GetFileName(item.FileName) + ":" +
item.Text.Length);
count++;
}
}
catch (AggregateException ae)
{
ae.Handle((ex) =>
{
if (ex is UnauthorizedAccessException)
{
Console.WriteLine(ex.Message);
return true;
}
return false;
});
}
Console.WriteLine("FileIteration_2 processed {0} files in {1}
milliseconds",
count, sw.ElapsedMilliseconds);
}

Al usar GetFiles, asegúrese de que tiene permisos suficientes en todos los directorios del árbol.
De lo contrario, se producirá una excepción y no se devolverá ningún resultado. Al usar
EnumerateDirectories en una consulta PLINQ, es problemático controlar las excepciones de E/S
de una forma correcta que permita continuar con la iteración. Si el código debe controlar las
excepciones de E/S o de acceso no autorizado, debe considerar el enfoque descrito en Cómo:
Recorrer en iteración directorios con la clase paralela.
Si la latencia de E/S es un problema, por ejemplo, porque la E/S de archivos se produce a través
de una red, considere la posibilidad de usar una de las técnicas de E/S asincrónicas descritas en
TPL y la programación asincrónica tradicional de .NET y en esta entrada de blog.
3.2.15. Cómo: Medir el rendimiento de consultas PLINQ
En este ejemplo se muestra cómo usar la clase Stopwatch para medir el tiempo que tarda en
ejecutarse una consulta PLINQ.
Ejemplo
En este ejemplo se usa un bucle foreach vacío (For Each en Visual Basic) para medir el tiempo
que tarda en ejecutarse la consulta. En el código real, normalmente, el bucle contiene pasos de
procesamiento adicionales que aumentan el tiempo de ejecución total de la consulta. Observe
que el cronómetro se inicia justo antes del bucle, porque es en ese momento cuando comienza la
ejecución de la consulta. Si necesita una medición más exacta, puede usar la propiedad
ElapsedTicks en lugar de ElapsedMilliseconds.
static void Main()
{
var source = Enumerable.Range(0, 3000000);
var queryToMeasure = from num in source
where num % 3 == 0
select Math.Sqrt(num);
Console.WriteLine("Measuring...");
// The query does not run until it is enumerated. Therefore, start the
timer here.
System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
// For pure query cost, enumerate and do nothing else.
foreach (var n in queryToMeasure) { }
long elapsed = sw.ElapsedMilliseconds; // or sw.ElapsedTicks
Console.WriteLine("Total query time: {0} ms", elapsed);
// Keep the console window open in debug mode.

MCT: Luis Dueñas Pag 311 de 336


Manual de .NET Framework 4.5

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


Console.ReadKey();
}

El tiempo de ejecución total es una medida útil cuando se prueban implementaciones de


consultas, pero no siempre lo dice todo. Para obtener una vista más detallada y completa de la
interacción de los subprocesos de consulta entre sí y con otros procesos en ejecución, use el
visualizador de simultaneidad.
3.2.16. Ejemplo de datos de PLINQ
Este ejemplo contiene datos de ejemplo en formato .csv, junto con métodos que lo transforman
en colecciones en memoria de clientes, productos, pedidos y detalles de pedidos. Si desea
realizar más pruebas con PLINQ, puede pegar ejemplos de código de otros temas en el código
de este tema e invocarlo desde el método Main. También puede usar estos datos con sus propias
consultas PLINQ.
Los datos representan un subconjunto de la base de datos Northwind. Se incluyen cincuenta (50)
registros de cliente, pero no todos los campos. Se incluye un subconjunto de las filas de pedidos
y los detalles correspondientes de Order_Detail de cada cliente. Se incluyen todos los productos.
Nota
El conjunto de datos no es lo bastante grande para demostrar que PLINQ es más rápido que
LINQ to Objects para las consultas que contienen simplemente cláusulas select y where básicas.
Para notar realmente el aumento de la velocidad con conjuntos de datos pequeños como este,
use consultas que contengan operaciones que consuman muchos recursos de computación en
cada elemento del conjunto de datos.
Para configurar este ejemplo
1. Cree un proyecto de aplicación de consola de Visual Basic o Visual C#.
2. Reemplace el contenido de Module1.vb o Program.cs utilizando el código que se
incluye después de estos pasos.
3. En el menú Proyecto, haga clic en Agregar nuevo elemento. Seleccione Archivo de
texto y, a continuación, haga clic en Aceptar. Copie los datos de este tema y, a
continuación, péguelos en el nuevo archivo de texto. En el menú Archivo, haga clic en
Guardar, asigne al archivo el nombre Plinqdata.csv y, a continuación, guárdelo en la
carpeta que contiene sus archivos de código fuente.
4. Presione F5 para comprobar que el proyecto se compila y se ejecuta correctamente. Se
debería mostrar la salida siguiente en la ventana de la consola.
Customer count: 50
Product count: 77
Order count: 190
Order Details count: 483
Press any key to exit.
// This class contains a subset of data from the Northwind database in the
form of
// string arrays. The methods such as GetCustomers, GetOrders, and so on
transform
// the strings into object arrays that you can query in an object-oriented
way.
// Many of the code examples in the PLINQ How-to topics are designed to be
pasted into
// the PLINQDataSample class and invoked from the Main method.
partial class PLINQDataSample
{
public static void Main()
{
////Call methods here.
TestDataSource();
Console.WriteLine("Press any key to exit.");
Console.ReadKey();

MCT: Luis Dueñas Pag 312 de 336


Manual de .NET Framework 4.5

static void TestDataSource()


{
Console.WriteLine("Customer count: {0}", GetCustomers().Count());
Console.WriteLine("Product count: {0}", GetProducts().Count());
Console.WriteLine("Order count: {0}", GetOrders().Count());
Console.WriteLine("Order Details count: {0}",
GetOrderDetails().Count());
}

#region DataClasses
public class Order
{
private Lazy<OrderDetail[]> _orderDetails;
public Order()
{
_orderDetails = new Lazy<OrderDetail[]>(() =>
GetOrderDetailsForOrder(OrderID));
}
public int OrderID { get; set; }
public string CustomerID { get; set; }
public DateTime OrderDate { get; set; }
public DateTime ShippedDate { get; set; }
public OrderDetail[] OrderDetails { get { return _orderDetails.Value;
} }
}

public class Customer


{
private Lazy<Order[]> _orders;
public Customer()
{
_orders = new Lazy<Order[]>(() =>
GetOrdersForCustomer(CustomerID));
}
public string CustomerID { get; set; }
public string CustomerName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string PostalCode { get; set; }
public Order[] Orders
{
get
{
return _orders.Value;
}
}
}

public class Product


{
public string ProductName { get; set; }
public int ProductID { get; set; }
public double UnitPrice { get; set; }
}

public class OrderDetail


{
public int OrderID { get; set; }
public int ProductID { get; set; }
public double UnitPrice { get; set; }
public double Quantity { get; set; }
public double Discount { get; set; }
}
#endregion

public static IEnumerable<string> GetCustomersAsStrings()

MCT: Luis Dueñas Pag 313 de 336


Manual de .NET Framework 4.5

{
return System.IO.File.ReadAllLines(@"..\..\plinqdata.csv")
.SkipWhile((line) =>
line.StartsWith("CUSTOMERS") == false)
.Skip(1)
.TakeWhile((line) => line.StartsWith("END CUSTOMERS") ==
false);
}

public static IEnumerable<Customer> GetCustomers()


{
var customers = System.IO.File.ReadAllLines(@"..\..\plinqdata.csv")
.SkipWhile((line) => line.StartsWith("CUSTOMERS") ==
false)
.Skip(1)
.TakeWhile((line) => line.StartsWith("END CUSTOMERS") ==
false);
return (from line in customers
let fields = line.Split(',')
let custID = fields[0].Trim()
select new Customer()
{
CustomerID = custID,
CustomerName = fields[1].Trim(),
Address = fields[2].Trim(),
City = fields[3].Trim(),
PostalCode = fields[4].Trim()
});
}

public static Order[] GetOrdersForCustomer(string id)


{
// Assumes we copied the file correctly!
var orders = System.IO.File.ReadAllLines(@"..\..\plinqdata.csv")
.SkipWhile((line) => line.StartsWith("ORDERS") == false)
.Skip(1)
.TakeWhile((line) => line.StartsWith("END ORDERS") ==
false);
var orderStrings = from line in orders
let fields = line.Split(',')
where fields[1].CompareTo(id) == 0
select new Order()
{
OrderID = Convert.ToInt32(fields[0]),
CustomerID = fields[1].Trim(),
OrderDate = DateTime.Parse(fields[2]),
ShippedDate = DateTime.Parse(fields[3])
};
return orderStrings.ToArray();
}

// "10248, VINET, 7/4/1996 12:00:00 AM, 7/16/1996 12:00:00 AM


public static IEnumerable<Order> GetOrders()
{
// Assumes we copied the file correctly!
var orders = System.IO.File.ReadAllLines(@"..\..\plinqdata.csv")
.SkipWhile((line) => line.StartsWith("ORDERS") == false)
.Skip(1)
.TakeWhile((line) => line.StartsWith("END ORDERS") ==
false);
return from line in orders
let fields = line.Split(',')
select new Order()
{
OrderID = Convert.ToInt32(fields[0]),
CustomerID = fields[1].Trim(),
OrderDate = DateTime.Parse(fields[2]),
ShippedDate = DateTime.Parse(fields[3])

MCT: Luis Dueñas Pag 314 de 336


Manual de .NET Framework 4.5

};
}

public static IEnumerable<Product> GetProducts()


{
// Assumes we copied the file correctly!
var products = System.IO.File.ReadAllLines(@"..\..\plinqdata.csv")
.SkipWhile((line) => line.StartsWith("PRODUCTS") ==
false)
.Skip(1)
.TakeWhile((line) => line.StartsWith("END PRODUCTS") ==
false);
return from line in products
let fields = line.Split(',')
select new Product()
{
ProductID = Convert.ToInt32(fields[0]),
ProductName = fields[1].Trim(),
UnitPrice = Convert.ToDouble(fields[2])
};
}

public static IEnumerable<OrderDetail> GetOrderDetails()


{
// Assumes we copied the file correctly!
var orderDetails = System.IO.File.ReadAllLines(@"..\..\plinqdata.csv")
.SkipWhile((line) => line.StartsWith("ORDER DETAILS") ==
false)
.Skip(1)
.TakeWhile((line) => line.StartsWith("END ORDER DETAILS")==
false);

return from line in orderDetails


let fields = line.Split(',')
select new OrderDetail()
{
OrderID = Convert.ToInt32(fields[0]),
ProductID = Convert.ToInt32(fields[1]),
UnitPrice = Convert.ToDouble(fields[2]),
Quantity = Convert.ToDouble(fields[3]),
Discount = Convert.ToDouble(fields[4])
};
}

public static OrderDetail[] GetOrderDetailsForOrder(int id)


{
// Assumes we copied the file correctly!
var orderDetails = System.IO.File.ReadAllLines(@"..\..\plinqdata.csv")
.SkipWhile((line) => line.StartsWith("ORDER DETAILS") ==
false)
.Skip(1)
.TakeWhile((line) => line.StartsWith("END ORDER
DETAILS")==false);

var orderDetailStrings = from line in orderDetails


let fields = line.Split(',')
let ordID = Convert.ToInt32(fields[0])
where ordID == id
select new OrderDetail()
{
OrderID = ordID,
ProductID = Convert.ToInt32(fields[1]),
UnitPrice = Convert.ToDouble(fields[2]),
Quantity = Convert.ToDouble(fields[3]),
Discount = Convert.ToDouble(fields[4])
};

return orderDetailStrings.ToArray();

MCT: Luis Dueñas Pag 315 de 336


Manual de .NET Framework 4.5

}
}

3.3. Estructuras de datos para la programación paralela


.NET Framework versión 4 incorpora varios tipos nuevos que resultan útiles para la
programación en paralelo, entre los que se incluye un conjunto de clases de colección
simultáneas, primitivas de sincronización ligeras y tipos de inicialización diferida. Puede usar
estos tipos con cualquier código de aplicación multiproceso, incluso con la biblioteca TPL (Task
Parallel Library, biblioteca de procesamiento paralelo basado en tareas) y PLINQ.
Clases de colección simultáneas
Las clases de colección del espacio de nombres System.Collections.Concurrent proporcionan
operaciones Add y Remove seguras para subprocesos que evitan los bloqueos en la medida de lo
posible y, cuando es necesario, usan bloqueos específicos. A diferencia de las colecciones que
se incorporaron en las versiones 1.0 y 2.0 de .NET Framework, una clase de colección
simultánea no requiere que el código de usuario tome ningún bloqueo cuando obtiene acceso a
los elementos. Las clases de colección simultáneas pueden mejorar significativamente el
rendimiento frente a tipos como System.Collections.ArrayList y
System.Collections.Generic.List<T> (con bloqueo implementado por el usuario) en escenarios
en lo que varios subprocesos agregan y quitan elementos de una colección.
En la tabla siguiente se muestran las nuevas clases de colección simultáneas:
Tipo Descripción
Proporciona capacidades de bloqueo y establecimiento de límites
en colecciones seguras para subprocesos que implementan
System.Collections.Concurrent.IProducerConsumerCollection<
T>. Los subprocesos de productor se bloquean si no hay ranuras
disponibles o si la colección está completa. Los subprocesos de
System.Collections.Concurren
consumidor se bloquean si la colección está vacía. Este tipo
t. BlockingCollection<T>
también permite el acceso sin bloqueo de los subprocesos de
consumidor y productor. Blocking Collection<T> puede usarse
como clase base o memoria auxiliar para proporcionar el
bloqueo y el establecimiento de límites de cualquier clase de
colección que admita IEnumerable<T>.
System.Collections.Concurren Implementación de un contenedor seguro para subprocesos que
t. ConcurrentBag<T> proporciona operaciones Add y Get escalables.
System.Collections.Concurren
t.
Tipo de diccionario simultáneo y escalable.
ConcurrentDictionary<TKey,
TValue>
System.Collections.Concurren
Cola FIFO simultánea y escalable.
t. ConcurrentQueue<T>
System.Collections.Concurren
Pila LIFO simultánea y escalable.
t. ConcurrentStack<T>
Primitivas de sincronización
Las nuevas primitivas de sincronización del espacio de nombres System.Threading
proporcionan una simultaneidad específica y un mayor rendimiento ya que evitan los costosos
mecanismos de bloqueo que se encuentran en el código multithreading heredado. Algunos de
los nuevos tipos, como System.Threading.Barrier y System.Threading.CountdownEvent, no
tienen equivalente en versiones anteriores de .NET Framework.
En la tabla siguiente, se muestran los nuevos tipos de sincronización:

MCT: Luis Dueñas Pag 316 de 336


Manual de .NET Framework 4.5

Tipo Descripción
Permite que varios subprocesos trabajen en un
algoritmo en paralelo proporcionando un punto en el
System.Threading.Barrier
que cada tarea puede señalar su llegada y bloquearse
hasta que algunas o todas las tareas hayan llegado.
Simplifica los escenarios de bifurcación y unión
System.Threading.CountdownEvent proporcionando un mecanismo de encuentro
sencillo.
Primitiva de sincronización similar a
System.Threading. ManualResetEvent.
System.Threading.ManualResetEventSlim ManualResetEventSlim es un objeto ligero, pero solo
puede usarse en la comunicación que tiene lugar
dentro de un proceso.
Primitiva de sincronización que limita el número de
System.Threading.SemaphoreSlim subprocesos que pueden obtener acceso a la vez a un
recurso o grupo de recursos.
Primitiva de bloqueo de exclusión mutua que hace
que el subproceso que está intentando adquirir el
bloqueo espere en un bucle o ciclo durante un
System.Threading.SpinLock período de tiempo antes de que se produzca su
cuanto. En escenarios en los que se prevé que la
espera del bloqueo será breve, SpinLock proporciona
mayor rendimiento que otras formas de bloqueo.
Tipo pequeño y ligero que iterará en ciclos durante
System.Threading.SpinWait un período especificado y situará el subproceso en
estado de espera si el recuento de ciclos se supera.
Clases de inicialización diferida
Con la inicialización diferida, la memoria de un objeto no se asigna hasta que es necesario. La
inicialización diferida puede mejorar el rendimiento al extender las asignaciones de objetos
uniformemente a lo largo de la duración de un programa. Puede habilitar la inicialización
diferida en cualquier tipo personalizado encapsulando el tipo Lazy<T>.
En la tabla siguiente, se muestran los tipos de inicialización diferida:
Tipo Descripción
Proporciona una inicialización diferida ligera y segura para
System.Lazy<T>
subprocesos.
Proporciona un valor de inicialización diferida por cada
System.Threading.ThreadLocal<T> subproceso, donde cada subproceso invoca de forma
diferida la función de inicialización.
Proporciona métodos estáticos que evitan tener que asignar
una instancia de inicialización diferida dedicada. En su
System.Threading.LazyInitializer lugar, usan referencias para garantizar que los destinos se
han inicializado a medida que se va obteniendo acceso a
ellos.
Excepciones agregadas
El tipo System.AggregateException se puede utilizar para capturar varias excepciones que se
producen simultáneamente en diferentes subprocesos y devolverlas al subproceso de unión
como una sola excepción. Los tipos System.Threading.Tasks.Task y
System.Threading.Tasks.Parallel así como la PLINQ usan AggregateException en gran medida
con este propósito.

MCT: Luis Dueñas Pag 317 de 336


Manual de .NET Framework 4.5

3.4. Herramientas de diagnóstico paralelo


Microsoft Visual Studio 2010 proporciona amplia compatibilidad con la depuración y la
generación de perfiles de las aplicaciones multiproceso.
Depuración
El depurador de Visual Studio agrega nuevas ventanas para depurar aplicaciones paralelas. Para
obtener más información, vea los temas siguientes:
 Uso de la ventana Tareas paralelas
 Uso de la ventaja Tareas paralelas
 Tutorial: Depurar una aplicación paralela.
Generación de perfiles
En las vistas de informe del visualizador de simultaneidad se puede ver cómo los subprocesos
de un programa paralelo interactúan entre sí y con los subprocesos de otros procesos del
sistema.
3.5. Particionadores personalizados para PLINQ y TPL
Para paralelizar una operación en un origen de datos, uno de los pasos esenciales es crear
particiones del origen en varias secciones a las que se tenga acceso simultáneamente a través de
varios subprocesos. PLINQ y Task Parallel Library (TPL) proporcionan particionadores
predeterminados que funcionan de forma transparente al escribir una consulta o bucle ForEach
en paralelo. En escenarios más avanzados, puede conectar su propio particionador.
Creación de particiones diferentes
Hay muchas maneras de crear particiones de un origen de datos. En los enfoques más eficaces,
varios subprocesos cooperan para procesar la secuencia original, en lugar de separar físicamente
el origen en varias subsecuencias. Para matrices y otros orígenes indizados como colecciones
IList donde de antemano se conoce la longitud, la creación de particiones por intervalos es la
forma más simple de crear particiones. Cada subproceso recibe índices iniciales y finales únicos,
para poder procesar el intervalo del origen sin sobrescribir ni ser sobrescrito por otro
subproceso. La única sobrecarga implicada en la creación de particiones por intervalos es el
trabajo inicial de crear los intervalos; ninguna sincronización adicional se requiere después de
eso. Por consiguiente, puede proporcionar buen rendimiento siempre que la carga de trabajo se
divida uniformemente. Una desventaja de la creación de particiones por intervalos es que si un
subproceso finaliza pronto, no puede ayudar a los demás a finalizar el trabajo.
Con listas vinculadas u otras colecciones cuya longitud no se conoce, puede utilizar la creación
de particiones por fragmentos. En la creación de particiones por fragmentos, cada subproceso o
tarea de un bucle o consulta en paralelo utiliza un número de elementos de origen de un
fragmento, los procesa y vuelve a recuperar más elementos. Los particionadores se aseguran de
que se distribuyen todos los elementos y no hay ningún duplicado. Un fragmento puede tener
cualquier tamaño. Por ejemplo, el particionador que se muestra en Cómo: Implementar las
particiones dinámicas crea fragmentos que contienen un solo elemento. Con tal de que los
fragmentos no sean demasiado grandes, esta forma de crear particiones mantiene
inherentemente el equilibrio de carga porque la asignación de elementos a subprocesos no está
predeterminada. Sin embargo, el particionador incurre en la sobrecarga de sincronización cada
vez que el subproceso necesita obtener otro fragmento. La cantidad de sincronización en que se
incurre en estos casos es inversamente proporcional al tamaño de los fragmentos.
En general, la creación de particiones por intervalos solo es más rápida cuando el tiempo de
ejecución del delegado es de poco a moderado, el origen tiene un número grande de elementos y
el trabajo total de cada partición es aproximadamente equivalente. La creación de particiones
por fragmentos es, por consiguiente, más rápida en la mayoría de los casos. En orígenes con un

MCT: Luis Dueñas Pag 318 de 336


Manual de .NET Framework 4.5

número pequeño de elementos o tiempos de ejecución más largos para el delegado, el


rendimiento de la creación de particiones por fragmentos e intervalos es casi igual.
Los particionadores de TPL también admiten un número de particiones dinámicas. Esto
significa que pueden crear particiones sobre la marcha, por ejemplo, cuando el bucle ForEach
genera una nueva tarea. Esta característica permite al particionador escalar junto con el propio
bucle. Los particionadores dinámicos también mantienen inherentemente el equilibrio de carga.
Cuando se crea un particionador personalizado, se debe admitir que la creación de particiones
dinámicas se use desde un bucle ForEach.
Configurar particionadores de equilibrio de carga para PLINQ
Algunas sobrecargas del método Partitioner.Create permitían crear particionadores para una
matriz u origen IList y especificar si debía intentar equilibrar la carga de trabajo entre los
subprocesos. Cuando se configura el particionador para equilibrar la carga, se utiliza la creación
de particiones por fragmentos y los elementos se presentan fuera de cada partición en pequeños
fragmentos a medida que se solicitan. Este enfoque ayuda a asegurar que todas las particiones
tienen elementos para procesar hasta que todo el bucle o la consulta se completa. Se puede
utilizar una sobrecarga adicional para proporcionar la creación de particiones de equilibrio de
carga de cualquier origen IEnumerable.
En general, el equilibrio de carga exige que las particiones soliciten elementos con relativa
frecuencia de los particionadores. Por contraste, el particionador que crea particiones estáticas
puede asignar todos los elementos a la vez mediante la creación de particiones por intervalos o
por fragmentos. Esto requiere menos sobrecarga que el equilibrio de carga, pero podría llevar
más mucho tiempo ejecutarse si un subproceso termina significativamente con más trabajo que
los demás. De forma predeterminada cuando se pasa IList o una matriz, PLINQ siempre utiliza
la creación de particiones por intervalos sin equilibrio de carga. Para habilitar el equilibrio de
carga para PLINQ, use el método Partitioner.Create, como se muestra en el siguiente ejemplo.
// Static partitioning requires indexable source. Load balancing
// can use any IEnumerable.
var nums = Enumerable.Range(0, 100000000).ToArray();
// Create a load-balancing partitioner. Or specify false for
static
// partitioning.
Partitioner<int> customPartitioner = Partitioner.Create(nums,
true);
// The partitioner is the query's data source.
var q = from x in customPartitioner.AsParallel()
select x * Math.PI;
q.ForAll((x) =>
{
ProcessData(x);
});

La mejor manera de determinar si utilizar el equilibrio de carga en un escenario determinado es


experimentar y medir cuánto tiempo tardan las operaciones en completarse con cargas y
configuraciones de equipo representativas. Por ejemplo, la creación de particiones estáticas
podría proporcionar un aumento de velocidad significativo en un equipo multiproceso con pocos
núcleos, pero podría ralentizar los equipos que tienen relativamente más núcleos.
En la tabla siguiente se muestran las sobrecargas disponibles del método Create. Estos
particionadores no están limitados a su uso con PLINQ o Task. También se pueden utilizar con
cualquier construcción paralela personalizada.
Sobrecarga Utiliza el equilibrio de carga
Create<TSource>(IEnumerable<TSource>) Siempre
Cuando el argumento booleano se especifica como
Create<TSource>(TSource[], Boolean)
verdadero
Create<TSource>(IList<TSource>, Cuando el argumento booleano se especifica como

MCT: Luis Dueñas Pag 319 de 336


Manual de .NET Framework 4.5

Boolean) verdadero
Create(Int32, Int32) Nunca
Create(Int32, Int32, Int32) Nunca
Create(Int64, Int64) Nunca
Create(Int64, Int64, Int64) Nunca
Configurar particionadores por intervalos estáticos para Parallel.ForEach
En un bucle For, el cuerpo del bucle se proporciona al método como un delegado. El costo de
invocar ese delegado es más o menos similar a una llamada al método virtual. En algunos
escenarios, el cuerpo de un bucle paralelo podría ser lo bastante pequeño como para que el costo
de la invocación del delegado en cada iteración del bucle fuera significativa. En tales
situaciones, puede utilizar una de las sobrecargas Create para crear una IEnumerable<T> de
particiones por intervalos de los elementos de origen. Después puede pasar esta colección de
intervalos a un método ForEach cuyo cuerpo está compuesto de un bucle for normal. La ventaja
de este enfoque es que solo se incurre en el costo de invocación de delegados una vez por
intervalo, en lugar de una vez por elemento. En el siguiente ejemplo se muestra el modelo
básico.
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;
if (input == 'y' || input == 'Y')
{
foreach(double d in results)
{
Console.Write("{0} ", d);
}
}
}
}

Cada subproceso del bucle recibe su propio Tuple<T1, T2> que contiene los valores de índice
de inicio y de fin del subintervalo especificado. El bucle for interno utiliza los valores
toExclusive y fromInclusive para recorrer directamente la matriz o IList.
Una de las sobrecargas Create permite especificar el tamaño y el número de las particiones. Esta
sobrecarga se puede utilizar en escenarios donde el trabajo por elemento es tan bajo que incluso
una llamada al método virtual por elemento tiene un impacto notable en el rendimiento.

MCT: Luis Dueñas Pag 320 de 336


Manual de .NET Framework 4.5

Particionadores personalizados
En algunos escenarios, valdría la pena o incluso podría ser preciso implementar un particionador
propio. Por ejemplo, podría tener una clase de colección personalizada que puede crear
particiones más eficazmente que los particionadores predeterminados, basándose en su
conocimiento de la estructura interna de la clase. O tal vez desee crear particiones por intervalos
de tamaños diferentes basándose en su conocimiento de cuánto tiempo tardará en procesar los
elementos en ubicaciones diferentes de la colección de origen.
Para crear un particionador personalizado básico, derive una clase de
System.Collections.Concurrent. Partitioner<TSource> e invalide los métodos virtuales, tal y
como se describe en la siguiente tabla.
El subproceso principal llama a este método una vez y devuelve
IList(IEnumerator(TSource)). Cada subproceso de trabajo del bucle
GetPartitions
o la consulta puede llamar a GetEnumerator en la lista para
recuperar IEnumerator<T> de una partición distinta.
Devuelve true si implementa GetDynamicPartitions, de lo contrario,
SupportsDynamicPartitions
false.
Si SupportsDynamicPartitions es true, se puede llamar a este
GetDynamicPartitions
método opcionalmente en lugar de a GetPartitions.
Si los resultados deben ser ordenables o si necesita acceso indizado a los elementos, derive de
System.Collections.Concurrent.OrderablePartitioner<TSource> e invalide sus métodos virtuales
tal y como se describe en la siguiente tabla.
El subproceso principal llama a este método una vez y devuelve
IList(IEnumerator(TSource)). Cada subproceso de trabajo del bucle o
GetPartitions
la consulta puede llamar a GetEnumerator en la lista para recuperar
IEnumerator<T> de una partición distinta.
SupportsDynamic Devuelve true si implementa GetDynamicPartitions; de lo contrario,
Partitions falso.
GetDynamicPartitions Normalmente, solo llama a GetOrderableDynamicPartitions.
GetOrderable Si SupportsDynamicPartitions es true, se puede llamar a este método
DynamicPartitions opcionalmente en lugar de a GetPartitions.
En la siguiente tabla se proporcionan los detalles adicionales sobre cómo los tres tipos de
particionadores del equilibrio de carga implementan la clase OrderablePartitioner<TSource>.
IList / matriz
IList / matriz con
Propiedad o método sin equilibrio IEnumerable
equilibrio de carga
de carga
Utiliza la creación de
Utiliza la creación de
Utiliza la particiones por
particiones por
creación de fragmentos
GetOrderablePartitions fragmentos y crea un
particiones optimizada para la
número de
por intervalos partitionCount
particiones estáticas.
especificada
Utiliza la creación de
Utiliza la creación de
particiones por
Produce una particiones por
OrderablePartitioner<TSource>. fragmentos
excepción no fragmentos creando
GetOrderableDynamicPartitions optimizada para las
admitida un número de
listas y las particiones
particiones dinámico.
dinámicas
KeysOrderedInEachPartition Devuelve true Devuelve true Devuelve true

MCT: Luis Dueñas Pag 321 de 336


Manual de .NET Framework 4.5

KeysOrderedAcrossPartitions Devuelve true Devuelve false Devuelve false


KeysNormalized Devuelve true Devuelve true Devuelve true
Devuelve
SupportsDynamicPartitions Devuelve true Devuelve true
false
Particiones dinámicas
Si piensa utilizar el particionador en un método ForEach, debe poder devolver un número de
particiones dinámico. Esto significa que el particionador pueden proporcionar un enumerador
para una nueva partición a petición en cualquier momento durante la ejecución del bucle.
Básicamente, cada vez que el bucle agrega una nueva tarea en paralelo, solicita una nueva
partición para esa tarea. Si exige que los datos se puedan ordenar, derive de
System.Collections.Concurrent.OrderablePartitioner<TSource> para que cada elemento de cada
partición tenga asignado un índice único.
Contrato para particionadores
Cuando implemente un particionador personalizado, siga estas instrucciones para asegurarse de
que la interacción es correcta con PLINQ y ForEach en TPL:
 Si se llama a GetPartitions con un argumento de cero o menos para partitionsCount, se
produce una ArgumentOutOfRangeException. Aunque PLINQ y TPL nunca pasarán en
una partitionCount igual a 0, recomendamos, no obstante, que se proteja ante esa
posibilidad.
 GetPartitions y GetOrderablePartitions siempre deberían devolver el número de
particiones partitionsCount. Si particionador se ejecuta fuera de los datos y no puede
crear tantas particiones como se solicitan, el método debería devolver un enumerador
vacío para cada una de las particiones restantes. De lo contrario, PLINQ y TPL
producirán una InvalidOperationException.
 GetPartitions , GetOrderablePartitions, GetDynamicPartitions y
GetOrderableDynamicPartitions nunca deberían devolver null (Nothing en Visual
Basic). Si lo hacen, PLINQ / TPL producirán una excepción
InvalidOperationException.
 Los métodos que devuelven particiones siempre deberían devolver particiones que
puedan enumerar completamente y de forma única el origen de datos. No debería haber
ninguna duplicación en el origen de datos ni elementos omitidos a menos que lo
requiera específicamente el particionador. Si no se sigue esta regla, se puede alterar el
orden del resultado.
 Los siguientes captadores get booleanos siempre deben devolver con precisión los
siguientes valores para que no se altere el orden de salida:
o KeysOrderedInEachPartition : cada partición devuelve los elementos con
índices de clave en aumento.
o KeysOrderedAcrossPartitions : para todas las particiones que se devuelven, los
índices de la clave de la partición i son más altos que los índices de la clave en
la partición i-1.
o KeysNormalized : todos los índices de clave aumentan consecutivamente,
comenzando por cero.
 Todos los índices deben ser únicos. No puede haber índices duplicados. Si no se sigue
esta regla, se puede alterar el orden del resultado.
 Todos los índices deben ser no negativos. Si no se sigue esta regla, PLINQ/TPL pueden
producir excepciones.
3.5.1. Cómo: Implementar las particiones dinámicas
En el siguiente ejemplo se muestra cómo implementar un System.Collections.Concurrent.
OrderablePartitioner<TSource> personalizado que implementa la creación de particiones
dinámicas y que se puede utilizar desde algunas sobrecargas de ForEach y de PLINQ.

MCT: Luis Dueñas Pag 322 de 336


Manual de .NET Framework 4.5

Ejemplo
Cada vez que una partición llama a MoveNext en el enumerador, éste proporciona un elemento
de lista a la partición. En el caso de PLINQ y ForEach, la partición es una instancia de Task.
Dado que las solicitudes se producen simultáneamente en varios subprocesos, se sincroniza el
acceso al índice actual.
// An orderable dynamic partitioner for lists
class OrderableListPartitioner<TSource> : OrderablePartitioner<TSource>
{
private readonly IList<TSource> m_input;

public OrderableListPartitioner(IList<TSource> input)


: base(true, false, true)
{
m_input = input;
}

// Must override to return true.


public override bool SupportsDynamicPartitions
{
get
{
return true;
}
}

public override IList<IEnumerator<KeyValuePair<long, TSource>>>


GetOrderablePartitions(int partitionCount)
{
var dynamicPartitions = GetOrderableDynamicPartitions();
var partitions =
new IEnumerator<KeyValuePair<long, TSource>>[partitionCount];
for (int i = 0; i < partitionCount; i++)
{
partitions[i] = dynamicPartitions.GetEnumerator();
}
return partitions;
}

public override IEnumerable<KeyValuePair<long, TSource>>


GetOrderableDynamicPartitions()
{
return new ListDynamicPartitions(m_input);
}

private class ListDynamicPartitions


: IEnumerable<KeyValuePair<long, TSource>>
{
private IList<TSource> m_input;
private int m_pos = 0;

internal ListDynamicPartitions(IList<TSource> input)


{
m_input = input;
}

public IEnumerator<KeyValuePair<long, TSource>> GetEnumerator()


{
while (true)
{
// Each task gets the next item in the list. The index is
// incremented in a thread-safe manner to avoid races.
int elemIndex = Interlocked.Increment(ref m_pos) - 1;
if (elemIndex >= m_input.Count)
{
yield break;
}

MCT: Luis Dueñas Pag 323 de 336


Manual de .NET Framework 4.5

yield return new KeyValuePair<long, TSource>(


elemIndex, m_input[elemIndex]);
}
}

IEnumerator IEnumerable.GetEnumerator()
{
return
((IEnumerable<KeyValuePair<long, TSource>>)this)
.GetEnumerator();
}
}
}

class ConsumerClass
{
static void Main()
{
var nums = Enumerable.Range(0, 10000).ToArray();
OrderableListPartitioner<int> partitioner =
new OrderableListPartitioner<int>(nums);
// Use with Parallel.ForEach
Parallel.ForEach(partitioner, (i) => Console.WriteLine(i));
// Use with PLINQ
var query = from num in partitioner.AsParallel()
where num % 2 == 0
select num;
foreach (var v in query)
Console.WriteLine(v);
}
}

Éste es un ejemplo de creación de particiones de fragmentos, y cada fragmento se compone de


un elemento. Proporcionando más elementos a la vez, podría reducir la contención sobre el
bloqueo y teóricamente lograr un rendimiento más rápido. Sin embargo, en algún punto, los
fragmentos mayores podrían requerir lógica de equilibrio de carga adicional para mantener
todos los subprocesos ocupados hasta que se finalice todo el trabajo.
3.5.2. Cómo: Implementar un particionador con un número estático de
particiones
En el siguiente ejemplo se muestra una manera de implementar un particionador personalizado
simple para PLINQ que realiza la creación de particiones estáticas. Dado que el particionador no
admite las particiones dinámicas, no se puede usar de Parallel.ForEach. Este particionador
determinado podría proporcionar más velocidad que el particionador del intervalo
predeterminado para los orígenes de datos en los que cada elemento requiere una cantidad
creciente de tiempo de proceso.
Ejemplo
// A static range partitioner for sources that require
// a linear increase in processing time for each succeeding element.
// The range sizes are calculated based on the rate of increase
// with the first partition getting the most elements and the
// last partition getting the least.
class MyPartitioner : Partitioner<int>
{
int[] source;
double rateOfIncrease = 0;

public MyPartitioner(int[] source, double rate)


{
this.source = source;
rateOfIncrease = rate;
}

MCT: Luis Dueñas Pag 324 de 336


Manual de .NET Framework 4.5

public override IEnumerable<int> GetDynamicPartitions()


{
throw new NotImplementedException();
}

// Not consumable from Parallel.ForEach.


public override bool SupportsDynamicPartitions
{
get
{
return false;
}
}

public override IList<IEnumerator<int>> GetPartitions(int partitionCount)


{
List<IEnumerator<int>> _list = new List<IEnumerator<int>>();
int end = 0;
int start = 0;
int[] nums = CalculatePartitions(partitionCount, source.Length);
for (int i = 0; i < nums.Length; i++)
{
start = nums[i];
if (i < nums.Length - 1)
end = nums[i + 1];
else
end = source.Length;
_list.Add(GetItemsForPartition(start, end));
// For demonstratation.
Console.WriteLine("start = {0} b (end) = {1}", start, end);
}
return (IList<IEnumerator<int>>)_list;
}
/*
* B
// Model increasing workloads as a right triangle / |
divided into equal areas along vertical lines. / | |
Each partition is taller and skinnier / | |
than the last. / | | |
/ | | |
/ | | |
/ | | | |
/ | | | |
A /______|____|___|__| C
*/
private int[] CalculatePartitions(int partitionCount, int sourceLength)
{
// Corresponds to the opposite side of angle A, which corresponds
// to an index into the source array.
int[] partitionLimits = new int[partitionCount];
partitionLimits[0] = 0;
// Represent total work as rectangle of source length times "most
expensive
// element"
// Note: RateOfIncrease can be factored out of equation.
double totalWork = sourceLength * (sourceLength * rateOfIncrease);
// Divide by two to get the triangle whose slope goes from zero on the
left to
// "most" on the right. Then divide by number of partitions to get
area of
// each partition.
totalWork /= 2;
double partitionArea = totalWork / partitionCount;
// Draw the next partitionLimit on the vertical coordinate that gives
// an area of partitionArea * currentPartition.
for (int i = 1; i < partitionLimits.Length; i++)
{
double area = partitionArea * i;

MCT: Luis Dueñas Pag 325 de 336


Manual de .NET Framework 4.5

// Solve for base given the area and the slope of the hypotenuse.

partitionLimits[i]=(int)Math.Floor(Math.Sqrt((2*area)/rateOfIncrease));
}
return partitionLimits;
}

IEnumerator<int> GetItemsForPartition(int start, int end)


{
// For demonstration purpsoes. Each thread receives its own
enumerator.
Console.WriteLine("called on thread
{0}",Thread.CurrentThread.ManagedThreadId);
for (int i = start; i < end; i++) yield return source[i];
}
}

class Consumer
{
public static void Main2()
{
var source = Enumerable.Range(0, 10000).ToArray();
Stopwatch sw = Stopwatch.StartNew();
MyPartitioner partitioner = new MyPartitioner(source, .5);
var query = from n in partitioner.AsParallel()
select ProcessData(n);
foreach (var v in query) { }
Console.WriteLine("Processing time with custom partitioner {0}",
sw.ElapsedMilliseconds);
var source2 = Enumerable.Range(0, 10000).ToArray();
sw = Stopwatch.StartNew();
var query2 = from n in source2.AsParallel()
select ProcessData(n);
foreach (var v in query2) { }
Console.WriteLine("Processing time with default partitioner {0}",
sw.ElapsedMilliseconds);
}

// Consistent processing time for measurement purposes.


static int ProcessData(int i)
{
Thread.SpinWait(i * 1000);
return i;
}
}

Las particiones de este ejemplo están basadas en la hipótesis de un aumento lineal del tiempo de
proceso por cada elemento. En la práctica, podría ser difícil predecir los tiempos de proceso de
esta manera. Si está utilizando un particionador estático con un origen de datos concreto, puede
optimizar la fórmula de creación de particiones del origen, agregar lógica de equilibrio de carga
o emplear un enfoque de creación de particiones de los fragmentos, como se muestra en Cómo:
Implementar las particiones dinámicas.
3.6. Generadores de tareas
Un generador de tareas se representa mediante la clase System.Threading.Tasks.TaskFactory,
que crea objetos Task, o la clase System.Threading.Tasks.TaskFactory<TResult>, que crea
objetos Task<TResult>. Ambas clases contienen métodos que puede utilizar para:
 Crear tareas e iniciarlas inmediatamente.
 Crear continuaciones de tareas que se inicien cuando alguna o toda una matriz de tareas
se complete.
 Crear tareas que representen pares de métodos de comienzo/fin que siguen el Modelo de
programación asincrónica.

MCT: Luis Dueñas Pag 326 de 336


Manual de .NET Framework 4.5

La clase Task tiene una propiedad estática que representa TaskFactory predeterminada.
Normalmente, los métodos TaskFactory se invocan utilizando la propiedad Factory, como se
muestra en el siguiente ejemplo.
Task taskA = Task.Factory.StartNew( () => ...);

En la mayoría de los casos, no tiene que derivar una nueva clase de TaskFactory. Sin embargo, a
veces es útil configurar una nueva TaskFactory y utilizarla para especificar algunas opciones o
asociar tareas a un programador personalizado. En el siguiente ejemplo se muestra cómo
configurar una nueva TaskFactory que crea tareas que usan el TaskScheduler especificado y
tiene las opciones TaskCreationOptions especificadas.
class Program
{
static CancellationTokenSource cts = new CancellationTokenSource();

static TaskFactory _factory = new TaskFactory(cts.Token,


TaskCreationOptions.PreferFairness,
TaskContinuationOptions.ExecuteSynchronously,
new MyScheduler());

static void Main(string[] args)


{
var t2 = _factory.StartNew(() => DoWork());
}

static void DoWork() {/*...*/ }


}

3.7. Programadores de tareas


Los programadores de tarea se representan mediante la clase
System.Threading.Tasks.TaskScheduler. Un programador de tareas se asegura de que se ejecuta
el trabajo de una tarea. El programador de tareas predeterminado está basado en .NET
Framework 4 ThreadPool, que proporciona robo de trabajo para el equilibrio de carga,
inyección/retirada de subprocesos para obtener el máximo resultado y un buen rendimiento en
general. Debería ser suficiente para la mayoría de los escenarios. Sin embargo, si necesita
funcionalidad especial, puede crear un programador personalizado y habilitarlo para tareas o
consultas concretas.
Programador de tareas predeterminado y ThreadPool
El programador predeterminado para Task Parallel Library y PLINQ utiliza .NET Framework
ThreadPool para poner en cola y ejecutar el trabajo. En .NET Framework 4, ThreadPool utiliza
la información que proporciona el tipo System.Threading.Tasks.Task para admitir el
paralelismo específico (unidades efímeras de trabajo) que las tareas y consultas paralelas
representan a menudo.
Cola global ThreadPool VS. las colas locales
Como en versiones anteriores de .NET Framework, ThreadPool mantiene una cola de trabajo
FIFO (primero en llegar, primero en salir) global para los subprocesos en cada dominio de
aplicación. Cuando un programa llama a QueueUserWorkItem (o UnsafeQueueUserWorkItem),
el trabajo se coloca en esta cola compartida y finalmente sale de la cola hacia el subproceso
siguiente que está disponible. En .NET Framework 4, esta cola se ha mejorado para usar un
algoritmo sin bloqueo que se parece a la clase ConcurrentQueue<T>. Mediante esta
implementación sin bloqueo, ThreadPool emplea menos tiempo en poner y sacar de las colas los
elementos de trabajo. Esta ventaja de rendimiento está disponible para todos los programas que
utilizan ThreadPool.
Las tareas de nivel superior, que son tareas que no se crean en el contexto de otra tarea, se
colocan en la cola global igual que cualquier otro elemento de trabajo. Sin embargo, las tareas
anidadas o secundarias, que se crean en el contexto de otra tarea, se controlan de forma bastante

MCT: Luis Dueñas Pag 327 de 336


Manual de .NET Framework 4.5

distinta. Una tarea secundaria o anidada se coloca en una cola local que es específica del
subproceso en el que la tarea primaria se está ejecutando. La tarea primaria puede ser una tarea
de nivel superior o también puede ser el elemento secundario de otra tarea. Cuando este
subproceso está listo para más trabajo, primero busca en la cola local. Si hay elementos de
trabajo esperando, se puede tener acceso a ellos rápidamente. Se tiene acceso a las colas locales
en el orden último en entrar (LIFO), primero en salir con el fin de conservar la situación de la
memoria caché y reducir la contención.
En el siguiente ejemplo se muestran algunas tareas que se programan en la cola global y otras
que se programan en la cola local.
void QueueTasks()
{
// TaskA is a top level task.
Task taskA = Task.Factory.StartNew( () =>
{
Console.WriteLine("I was enqueued on the thread pool's global
queue.");
// TaskB is a nested task and TaskC is a child task. Both go to local
queue.
Task taskB = new Task( ()=> Console.WriteLine
("I was enqueued on the local queue."));
Task taskC = new Task(() => Console.WriteLine
("I was enqueued on the local queue, too."),
TaskCreationOptions.AttachedToParent);
taskB.Start();
taskC.Start();
});
}

El uso de colas locales reduce no solo la presión en la cola global, también aprovecha la
situación de los datos. Los elementos de trabajo de la cola local con frecuencia hacen referencia
a estructuras de datos que están físicamente cerca unos de otros en memoria. En estos casos, los
datos ya están en la memoria caché después de que la primera tarea se haya ejecutado, y se
puede obtener acceso rápidamente. LINQ Paralelo (PLINQ) y la clase Parallel usa tareas
anidadas y tareas secundarias extensivamente y logran aumentos significativos de velocidad
utilizando las colas de trabajo locales.
Robo de trabajo
.NET Framework 4 ThreadPool también representa un algoritmo de robo de trabajo para ayudar
a asegurar que ningún subproceso esté inactivo mientras otros todavía tienen trabajo en sus
colas. Cuando un subproceso ThreadPool está listo para más trabajo, examina primero el
encabezado de la cola local, a continuación, en la cola global y después en las colas locales de
otros subprocesos. Si encuentra un elemento de trabajo en la cola local de otro subproceso,
aplica primero heurística para asegurarse de que puede ejecutar el trabajo eficazmente. Si puede,
quita el elemento de trabajo de la cola (en orden FIFO). Esto reduce la contención en cada cola
local y mantiene la situación de los datos. Esta arquitectura ayuda a que el equilibrio de carga de
ThreadPool en .NET Framework 4 trabaje más eficazmente que las versiones anteriores.
Tareas de ejecución prolongada
Tal vez le interese evitar explícitamente que una tarea se coloque en una cola local. Por ejemplo,
puede saber que un elemento de trabajo determinado se ejecutará durante un tiempo
relativamente largo y es probable que bloquee el resto de los elementos de trabajo de la cola
local. En este caso, puede especificar la opción LongRunning, que proporciona una sugerencia
al programador que le indica que tal vez es necesario un subproceso adicional para que la tarea
no bloquee el progreso de otros subprocesos o elementos de trabajo de la cola local. Utilizando
esta opción, se evita ThreadPool completamente, incluidas las colas global y locales.

MCT: Luis Dueñas Pag 328 de 336


Manual de .NET Framework 4.5

Inclusión de tareas
En algunos casos, cuando se espera un tarea, se puede ejecutar sincrónicamente en el
subproceso que está realizando la operación de espera. Esto mejora el rendimiento, porque evita
la necesidad de un subproceso adicional mediante el uso del subproceso existente que, de otro
modo, se habría bloqueado. Para evitar errores después de volver a entrar, la inclusión de tareas
solo tiene lugar cuando el destino de la espera se encuentra en la cola local del subproceso
pertinente.
Especificar un contexto de sincronización
Puede utilizar el método TaskScheduler.FromCurrentSynchronizationContext para especificar
que una tarea se debería programar para ejecutarse en un subproceso determinado. Esto es útil
en marcos como Windows Forms y Windows Presentation Foundation, donde el acceso a los
objetos de interfaz de usuario está restringido a menudo para el código que se está ejecutando en
el mismo subproceso en el que se creó el objeto UI.
3.7.1. Cómo: Crear un programador de tareas que limita el grado de
simultaneidad
En algunos casos no muy usuales, podría lograr aumentar el rendimiento creando un
programador de tareas personalizado que se derive de la clase
System.Threading.Tasks.TaskScheduler. Después podría especificar este programador en un
método For o ForEach utilizando la enumeración System.Threading.Tasks.ParallelOptions. Al
utilizar los objetos Task directamente, puede especificar el programador personalizado mediante
el constructor TaskFactory que toma TaskScheduler como un parámetro de entrada o por algún
otro medio como TaskFactory.StartNew.
También puede utilizar un programador personalizado para lograr la funcionalidad que el
programador predeterminado no proporciona, como es el orden de ejecución estricto de primero
en entrar, primero en salir (FIFO). En el ejemplo siguiente se muestra cómo crear un
programador de tareas personalizado. Este programador permite especificar el grado de
simultaneidad.
Ejemplo
El siguiente ejemplo procede de Parallel Extensions Samples del sitio web Galería de código de
MSDN.
namespace System.Threading.Tasks.Schedulers
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

class Program
{
static void Main()
{
LimitedConcurrencyLevelTaskScheduler lcts =
new LimitedConcurrencyLevelTaskScheduler(1);
TaskFactory factory = new TaskFactory(lcts);
factory.StartNew(()=>
{
for (int i = 0; i < 500; i++)
{
Console.Write("{0} on thread {1}", i,
Thread.CurrentThread.ManagedThreadId);
}
}
);
Console.ReadKey();

MCT: Luis Dueñas Pag 329 de 336


Manual de .NET Framework 4.5

}
}

/// <summary>
/// Provides a task scheduler that ensures a maximum concurrency level
while
/// running on top of the ThreadPool.
/// </summary>
public class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
{
/// <summary>Whether the current thread is processing work
items.</summary>
[ThreadStatic]
private static bool _currentThreadIsProcessingItems;
/// <summary>The list of tasks to be executed.</summary>
private readonly LinkedList<Task> _tasks = new LinkedList<Task>();
// protected by lock(_tasks)
/// <summary>The maximum concurrency level allowed by this scheduler
private readonly int _maxDegreeOfParallelism;
/// <summary>Whether the scheduler is currently processing work items.
private int _delegatesQueuedOrRunning = 0; // protected by
lock(_tasks)
/// <summary>
/// Initializes an instance of the
LimitedConcurrencyLevelTaskScheduler class
/// with the specified degree of parallelism.
/// </summary>
/// <param name="maxDegreeOfParallelism">The maximum degree of
parallelism
/// provided by this scheduler.</param>
public LimitedConcurrencyLevelTaskScheduler(int
maxDegreeOfParallelism)
{
if (maxDegreeOfParallelism < 1) throw new
ArgumentOutOfRangeException("maxDegreeOfParallelism");
_maxDegreeOfParallelism = maxDegreeOfParallelism;
}

/// <summary>Queues a task to the scheduler.</summary>


/// <param name="task">The task to be queued.</param>
protected sealed override void QueueTask(Task task)
{
// Add the task to the list of tasks to be processed. If there
aren't enough
// delegates currently queued or running to process tasks, schedule
another.
lock (_tasks)
{
_tasks.AddLast(task);
if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism)
{
++_delegatesQueuedOrRunning;
NotifyThreadPoolOfPendingWork();
}
}
}

/// <summary>
/// Informs the ThreadPool that there's work to be executed for this
scheduler
/// </summary>
private void NotifyThreadPoolOfPendingWork()
{
ThreadPool.UnsafeQueueUserWorkItem(_ =>
{
// Note that the current thread is now processing work items.
// This is necessary to enable inlining of tasks into this
thread.

MCT: Luis Dueñas Pag 330 de 336


Manual de .NET Framework 4.5

_currentThreadIsProcessingItems = true;
try
{
// Process all available items in the queue.
while (true)
{
Task item;
lock (_tasks)
{
// When there are no more items to be processed,
// note that we're done processing, and get out.
if (_tasks.Count == 0)
{
--_delegatesQueuedOrRunning;
break;
}
// Get the next item from the queue
item = _tasks.First.Value;
_tasks.RemoveFirst();
}
// Execute the task we pulled out of the queue
base.TryExecuteTask(item);
}
}
// We're done processing items on the current thread
finally { _currentThreadIsProcessingItems = false; }
}, null);
}

/// <summary>Attempts to execute the specified task on the current


thread.
/// <param name="task">The task to be executed.</param>
/// <param name="taskWasPreviouslyQueued"></param>
/// <returns>Whether the task could be executed on the current thread.
protected sealed override bool TryExecuteTaskInline(Task task,
bool taskWasPreviouslyQueued)
{
// If this thread isn't already processing a task, we don't support
inlining
if (!_currentThreadIsProcessingItems) return false;
// If the task was previously queued, remove it from the queue
if (taskWasPreviouslyQueued) TryDequeue(task);
// Try to run the task.
return base.TryExecuteTask(task);
}

/// <summary>Attempts to remove a previously scheduled task from the


scheduler
/// <param name="task">The task to be removed.</param>
/// <returns>Whether the task could be found and removed.</returns>
protected sealed override bool TryDequeue(Task task)
{
lock (_tasks) return _tasks.Remove(task);
}

/// <summary>Gets the maximum concurrency level supported by this


scheduler
public sealed override int MaximumConcurrencyLevel { get
{ return _maxDegreeOfParallelism; } }
///<summary>Gets an enumerable of the tasks currently scheduled on this
scheduler
/// <returns>An enumerable of the tasks currently scheduled.</returns>
protected sealed override IEnumerable<Task> GetScheduledTasks()
{
bool lockTaken = false;
try
{
Monitor.TryEnter(_tasks, ref lockTaken);

MCT: Luis Dueñas Pag 331 de 336


Manual de .NET Framework 4.5

if (lockTaken) return _tasks.ToArray();


else throw new NotSupportedException();
}
finally
{
if (lockTaken) Monitor.Exit(_tasks);
}
}
}
}

3.7.2. Cómo: Programar trabajo en el subproceso de la interfaz de


usuario
En este ejemplo se muestra cómo utilizar el método
TaskScheduler.FromCurrentSynchronizationContext en una aplicación Windows Presentation
Foundation (WPF) para programar una tarea en el mismo subproceso en que se creó el control
de interfaz de usuario.
Procedimientos
Para crear el proyecto de WPF
1. En Visual Studio, cree un proyecto de aplicación WPF y asígnele un nombre.
2. En la vista de diseño, arrastre un control Image del Cuadro de herramientas a la
superficie de diseño. En la vista de XAML, especifique la alineación horizontal como
"Left". El tamaño no importa porque el tamaño del control se cambiará dinámicamente
en tiempo de ejecución. Acepte el nombre predeterminado "image1".
3. Arrastre un botón del Cuadro de herramientas hacia la parte inferior izquierda de la
ventana de la aplicación. Haga doble clic en el botón para agregar el controlador
predeterminado del evento Click. En la vista XAML, especifique la propiedad Content
del botón como "Make a Mosaic" y especifique la alineación horizontal en "Left".
4. En el archivo MainWindow.xaml.cs, utilice el siguiente código para reemplazar el
contenido completo del archivo. Asegúrese de que el nombre del espacio de nombres
coincide con el nombre del proyecto.
5. Presione F5 para ejecutar la aplicación. Cada vez que se hace clic en el botón, se
debería mostrar una nueva organización de mosaicos.
Ejemplo
Descripción
En el siguiente ejemplo se crea un mosaico de imágenes que se seleccionan de forma aleatoria
de un directorio especificado. Los objetos WPF se utilizan para cargar y cambiar el tamaño de
las imágenes. A continuación, los píxeles sin formato se pasan a una tarea que utiliza un bucle
For para escribir los datos de píxeles en una matriz grande de un solo byte. No se requiere
ninguna sincronización porque ninguno de los dos mosaicos ocupa los mismos elementos de la
matriz. Los mosaicos también se pueden escribir en cualquier orden porque su posición se
calcula independientemente de cualquier otro mosaico. A continuación, la matriz grande se pasa
a una tarea que se ejecuta en el subproceso de la interfaz de usuario, donde los datos de píxeles
se cargan en un control Image.
Código
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

MCT: Luis Dueñas Pag 332 de 336


Manual de .NET Framework 4.5

using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace wpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private int fileCount;
int colCount;
int rowCount;
private int tilePixelHeight;
private int tilePixelWidth;
private int largeImagePixelHeight;
private int largeImagePixelWidth;
private int largeImageStride;
PixelFormat format;
BitmapPalette palette;

public MainWindow()
{
InitializeComponent();
// For this example, values are hard-coded to a mosaic of 8x8
tiles.
// Each tile is 50 pixels high and 66 pixels wide and 32 bits per
pixel.
colCount = 12;
rowCount = 8;
tilePixelHeight = 50;
tilePixelWidth = 66;
largeImagePixelHeight = tilePixelHeight * rowCount;
largeImagePixelWidth = tilePixelWidth * colCount;
largeImageStride = largeImagePixelWidth * (32 / 8);
this.Width = largeImagePixelWidth + 40;
image1.Width = largeImagePixelWidth;
image1.Height = largeImagePixelHeight;
}

private void button1_Click(object sender, RoutedEventArgs e)


{
// For best results use 1024 x 768 jpg files at 32bpp.
string[] files = System.IO.Directory.GetFiles
(@"C:\Users\Public\Pictures\Sample Pictures\", "*.jpg");
fileCount = files.Length;
Task<byte[]>[] images = new Task<byte[]>[fileCount];
for (int i = 0; i < fileCount; i++)
{
int x = i;
images[x] = Task.Factory.StartNew(() => LoadImage(files[x]));
}
// When theyve all been loaded, tile them into a single byte
array.
var tiledImage =
Task.Factory.ContinueWhenAll(images,(i)=>TileImages(i));
// We are currently on the UI thread. Save the sync context and
pass it to
// the next task so that it can access the UI control "image1".
var UISyncContext =
TaskScheduler.FromCurrentSynchronizationContext();
// On the UI thread, put the bytes into a bitmap and
// and display it in the Image control.

MCT: Luis Dueñas Pag 333 de 336


Manual de .NET Framework 4.5

var t3 = tiledImage.ContinueWith((antedecent) =>


{
// Get System DPI.
Matrix m =PresentationSource.FromVisual(Application.Current.
MainWindow).CompositionTarget.TransformToDevice;
double dpiX = m.M11;
double dpiY = m.M22;
BitmapSource bms = BitmapSource.Create( largeImagePixelWidth,
largeImagePixelHeight, dpiX, dpiY, format, palette,
antedecent.Result, largeImageStride);
image1.Source = bms;
}, UISyncContext);
}

byte[] LoadImage(string filename)


{
// Use the WPF BitmapImage class to load and
// resize the bitmap. NOTE: Only 32bpp formats are supported
correctly.
// Support for additional color formats is left as an exercise for
the
// reader. For more information see documentation
ColorConvertedBitmap.
BitmapImage myBitmapImage = new BitmapImage();
myBitmapImage.BeginInit();
myBitmapImage.UriSource = new Uri(filename);
tilePixelHeight = myBitmapImage.DecodePixelHeight =
tilePixelHeight;
tilePixelWidth = myBitmapImage.DecodePixelWidth = tilePixelWidth;
myBitmapImage.EndInit();
format = myBitmapImage.Format;
int size = (int)(myBitmapImage.Height * myBitmapImage.Width);
int stride = (int)myBitmapImage.Width * 4;
byte[] dest = new byte[stride * tilePixelHeight];
myBitmapImage.CopyPixels(dest, stride, 0);
return dest;
}

int Stride(int pixelWidth, int bitsPerPixel)


{
return (((pixelWidth * bitsPerPixel + 31) / 32) * 4);
}

// Map the individual image tiles to the large image


// in parallel. Any kind of raw image manipulation can be
// done here because we are not attempting to access any
// WPF controls from multiple threads.
byte[] TileImages(Task<byte[]>[] sourceImages)
{
byte[] largeImage = new byte[largeImagePixelHeight *
largeImageStride];
int tileImageStride = tilePixelWidth * 4; // hard coded to 32bpp
Random rand = new Random();
Parallel.For(0, rowCount * colCount, (i) =>
{
// Pick one of the images at random for this tile.
int cur = rand.Next(0, sourceImages.Length);
byte[] pixels = sourceImages[cur].Result;
// Get the starting index for this tile.
int row = i / colCount;
int col = (int)(i % colCount);
int
idx=((row*(largeImageStride*tilePixelHeight))+(col*tileImageStride));
// Write the pixels for the current tile. The pixels are not
contiguous
// in the array, therefore we have to advance the index by the
image stride

MCT: Luis Dueñas Pag 334 de 336


Manual de .NET Framework 4.5

// (minus the stride of the tile) for each scanline of the


tile.
int tileImageIndex = 0;
for (int j = 0; j < tilePixelHeight; j++)
{
// Write the next scanline for this tile.
for (int k = 0; k < tileImageStride; k++)
{
largeImage[idx++] = pixels[tileImageIndex++];
}
// Advance to the beginning of the next scanline.
idx += largeImageStride - tileImageStride;
}
});
return largeImage;
}
}
}

Comentarios
En este ejemplo se muestra cómo mover los datos del subproceso de la interfaz de usuario,
modificarlos usando bucles paralelos y objetos Task y, a continuación, devolverlos a una tarea
que se ejecuta en el subproceso de la interfaz de usuario. Este enfoque es útil cuando se tiene
que utilizar Task Parallel Library para realizar operaciones admitidas o no admitidas por la API
de WPF, o que no son suficientemente rápidas. Otra manera de crear un mosaico de la imagen
en WPF es utilizar un objeto WrapPanel y agregarle las imágenes. WrapPanel controlará el
trabajo de colocar los mosaicos. Sin embargo, este trabajo solo se puede realizar en el
subproceso de la interfaz de usuario.
Este ejemplo tiene algunas limitaciones. Solo se admiten imágenes de 32 bits por píxel, por
ejemplo; el objeto BitmapImage daña las imágenes de otros formatos durante la operación de
cambio de tamaño. Además las imágenes de origen deben ser mayores que el tamaño del
mosaico. Como un ejercicio más extenso, puede agregar la funcionalidad para controlar varios
formatos de píxel y tamaños de archivo.
3.8. Expresiones lambda en PLINQ y TPL
La biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas)
contiene muchos métodos que toman una de las familias de delegados System.Func<TResult> o
System.Action como parámetros de entrada. Estos delegados se usan para pasar la lógica del
programa personalizado al bucle, tarea o consulta paralelo. Los ejemplos de código de TPL, así
como PLINQ, usan expresiones lambda para crear instancias de los delegados como bloques de
código alineado. En este tema se proporciona una breve introducción a Func y Action, y se
muestra cómo usar las expresiones lambda en la biblioteca TPL y PLINQ.
Delegado Func
Un delegado Func encapsula un método que devuelve un valor. En una signatura de Func, el
último parámetro de tipo, o el situado en el extremo derecho, siempre especifica el tipo de valor
devuelto. Una causa común de los errores del compilador es el intento de pasar dos parámetros
de entrada a System.Func<T, TResult>; de hecho, este tipo toma un único parámetro de entrada.
La biblioteca de clases de .NET Framework define 17 versiones de Func:
System.Func<TResult>, System.Func<T, TResult>, System.Func<T1, T2, TResult>, y así
sucesivamente hasta System.Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
T14, T15, T16, TResult>.
Delegado Action
Un delegado System.Action encapsula un método (Sub en Visual Basic) que no devuelve
ningún valor o que devuelve void. En una signatura de Action, los parámetros de tipo solamente
representan parámetros de entrada. Al igual que sucede con Func, la biblioteca de clases de

MCT: Luis Dueñas Pag 335 de 336


Manual de .NET Framework 4.5

.NET Framework define 17 versiones de Action, desde una versión que no tiene ningún
parámetro de tipo hasta una versión con 16 parámetros de tipo.
Ejemplo
En el ejemplo siguiente del método Parallel.ForEach<TSource,
TLocal>(IEnumerable<TSource>, Func<TLocal>, Func<TSource, ParallelLoopState, TLocal,
TLocal>, Action<TLocal>) se muestra cómo expresar los delegados Func y Action mediante
expresiones lambda.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

class ForEachWithThreadLocal
{
// Demonstrated features:
// Parallel.ForEach()
// Thread-local state
// Expected results:
// This example sums up the elements of an int[] in parallel.
// Each thread maintains a local sum. When a thread is initialized,
that
// local sum is set to 0.
// On every iteration the current element is added to the local sum.
// When a thread is done, it safely adds its local sum to the global
sum.
// After the loop is complete, the global sum is printed out.
// Documentation:
// http://msdn.microsoft.com/en-us/library/dd990270(VS.100).aspx
static void Main()
{
// The sum of these elements is 40.
int[] input = { 4, 1, 6, 2, 9, 5, 10, 3 };
int sum = 0;
try
{
Parallel.ForEach(
input, // source collection
() => 0, // thread local initializer
(n, loopState, localSum) => // body
{
localSum += n;
Console.WriteLine("Thread={0}, n={1}, localSum={2}",
Thread.CurrentThread.ManagedThreadId, n, localSum);
return localSum;
},
(localSum) => Interlocked.Add(ref sum, localSum)
);

Console.WriteLine("\nSum={0}", sum);
}
// No exception is expected in this example, but if one is still thrown from
a task,
// it will be wrapped in AggregateException and propagated to the main
thread.
catch (AggregateException e)
{
Console.WriteLine("Parallel.ForEach has thrown an exception.
THIS WAS NOT EXPECTED.\n{0}", e);
}
}
}

MCT: Luis Dueñas Pag 336 de 336

Das könnte Ihnen auch gefallen