Documentos de Académico
Documentos de Profesional
Documentos de Cultura
subprocesos múltiples con
Libro de cocina C#
Segunda edicion
Más de 70 recetas para que pueda escribir
programas paralelos, asincrónicos y
multiproceso potentes y eficientes en C# 6.0
Eugene Agáfonov
BIRMINGHAM BOMBAY
Machine Translated by Google
Multihilo con C# Cookbook
Segunda edicion
Copyright © 2016 Packt Publishing
Reservados todos los derechos. Ninguna parte de este libro puede reproducirse, almacenarse en un sistema
de recuperación o transmitirse de ninguna forma ni por ningún medio sin el permiso previo por escrito del
editor, excepto en el caso de citas breves incrustadas en artículos críticos o reseñas.
Se ha hecho todo lo posible en la preparación de este libro para garantizar la exactitud de la información
presentada. Sin embargo, la información contenida en este libro se vende sin garantía, ya sea expresa o
implícita. Ni el autor, ni Packt Publishing, ni sus comerciantes y distribuidores serán responsables de los
daños causados o presuntamente causados directa o indirectamente por este libro.
Packt Publishing se ha esforzado por proporcionar información de marcas registradas sobre todas las
empresas y productos mencionados en este libro mediante el uso apropiado de capitales.
Sin embargo, Packt Publishing no puede garantizar la exactitud de esta información.
Primera publicación: noviembre de 2013
Segunda Edición: Abril 2016
Referencia de producción: 1150416
Publicado por Packt Publishing Ltd.
Livery Place
35 Livery Street
Birmingham B3 2PB, Reino Unido.
ISBN 9781785881251
www.packtpub.com
Machine Translated by Google
Créditos
Autor Editor de copia
Eugene Agáfonov Neha Vyas
revisores Coordinador del proyecto
Chad McCallum Francina Pinto
Felipe Pierce
Corrector de pruebas
Editor de puesta en marcha Edición Safis
edward gordon
indexador
Rekha Nair
Redactor de adquisiciones
kirk d'costa
Coordinador de produccion
Editor de desarrollo de contenido manu jose
Nikhil Borkar
trabajo de portada
Redactor técnico manu jose
Vivek Pala
Machine Translated by Google
Sobre el Autor
Eugene Agafonov dirige el departamento de desarrollo de ABBYY y vive en Moscú.
Tiene más de 15 años de experiencia profesional en desarrollo de software y comenzó a trabajar
con C# cuando estaba en versión beta. Es un MVP de Microsoft en ASP.NET desde 2006 y, a
menudo, habla en conferencias locales de desarrollo de software, como DevCon Russia, sobre
tecnologías de vanguardia en el desarrollo de aplicaciones web y del lado del servidor modernas. Sus
principales intereses profesionales son la arquitectura de software basada en la nube, la escalabilidad y la confiabilidad.
Eugene es un gran aficionado al fútbol y toca la guitarra en una banda de rock local. Puede comunicarse
con él en su blog personal, eugeneagafonov.com, o encuéntralo en Twitter en @eugene_agafonov.
ABBYY es un líder mundial en el desarrollo de tecnologías y soluciones de reconocimiento de
documentos, captura de contenido y lenguaje que se integran en todo el ciclo de vida de la información.
Es autor de Multithreading in C# 5.0 Cookbook y Mastering C# Concurrency de Packt Publishing.
Me gustaría dedicar este libro a mi amada esposa, Helen, y a mi hijo, Nikita.
Machine Translated by Google
Acerca de los revisores
Chad McCallum es un experto en informática de Saskatchewan apasionado por el desarrollo
de software. Tiene más de 10 años de experiencia en .NET (y 2 años en PHP, pero no hablaremos
de eso). Después de graduarse de SIAST Kelsey Campus, tomó un trabajo de contratación de PHP
independiente hasta que pudo molestar a iQmetrix para que le diera un trabajo, al que se ha aferrado
durante los últimos 10 años. Regresó a sus raíces en Regina y comenzó HackREGINA, una
organización local de hackathon destinada a fortalecer la comunidad de desarrolladores mientras
programa y bebe cerveza. Su enfoque actual es dominar el arte del comercio electrónico multiusuario con .NET.
Entre su obsesión por los juegos de mesa y las ideas aleatorias de aplicaciones, intenta aprender
una nueva tecnología cada semana. Puede ver los resultados en www.rtigger.com.
Philip Pierce es un desarrollador de software con 20 años de experiencia en desarrollo móvil, web, de
escritorio y servidor, diseño y gestión de bases de datos y desarrollo de juegos. Su experiencia incluye
la creación de inteligencia artificial para juegos y software empresarial, la conversión de juegos AAA
entre varias plataformas, el desarrollo de aplicaciones de subprocesos múltiples y la creación de
tecnologías de comunicación cliente/servidor patentadas.
Philip ha ganado varios hackatones, incluido el de Mejor aplicación móvil en la Cumbre de desarrolladores de
AT&T 2013, y finalista en la categoría de Mejor aplicación de Windows 8 en el Battlethon Miami de PayPal.
Su proyecto más reciente fue convertir Rail Rush y Temple Run 2 de la plataforma Android a las plataformas
Arcade.
Los portafolios de Philip se pueden encontrar en los siguientes sitios web:
f http://www.rocketgamesmobile.com f http://
www.philippiercedeveloper.com
Machine Translated by Google
www.PacktPub.com
Libros electrónicos, ofertas de descuento y más
¿Sabía que Packt ofrece versiones de libros electrónicos de cada libro publicado, con archivos PDF y ePub
disponibles? Puede actualizar a la versión de libro electrónico en www.PacktPub.com y como cliente de un libro
impreso, tiene derecho a un descuento en la copia del libro electrónico. Póngase en contacto con nosotros
en customercare@packtpub.com para obtener más detalles.
En www.PacktPub.com, también puede leer una colección de artículos técnicos gratuitos, suscribirse
a una variedad de boletines gratuitos y recibir descuentos y ofertas exclusivos en libros y libros
electrónicos de Packt.
TM
https://www2.packtpub.com/books/subscription/packtlib
¿Necesita soluciones instantáneas a sus preguntas de TI? PacktLib es la biblioteca de libros digitales en
línea de Packt. Aquí puede buscar, acceder y leer toda la biblioteca de libros de Packt.
¿Por qué suscribirse? f Búsqueda
completa en todos los libros publicados por Packt
f Copiar y pegar, imprimir y marcar contenido
f Bajo demanda y accesible a través de un navegador web
Machine Translated by Google
Tabla de contenido
Prefacio v
Capítulo 1: Conceptos básicos de enhebrado 1
Introducción 2
Crear un hilo en C# 2
Pausar un hilo 6
Hacer esperar un hilo 7
Abortar un hilo
Determinar el estado de un hilo 8 10
Tarea prioritaria 12
Hilos de primer plano y de fondo 14
Pasar parámetros a un hilo 16
Bloqueo con una palabra clave de bloqueo de C# 19
Bloqueo con una construcción de monitor 22
Manejo de excepciones 24
Capítulo 2: Sincronización de subprocesos 27
Introducción 27
Realización de operaciones atómicas básicas. 28
Usando la construcción Mutex 31
Uso de la construcción SemaphoreSlim 32
Uso de la construcción AutoResetEvent 34
Uso de la construcción ManualResetEventSlim 36
Uso de la construcción CountDownEvent 38
Uso de la construcción Barrera 39
Uso de la construcción ReaderWriterLockSlim 41
Usando la construcción SpinWait 44
i
Machine Translated by Google
Tabla de contenido
Capítulo 3: Uso de un grupo de subprocesos 47
Introducción 47
Invocar a un delegado en un grupo de subprocesos 49
Publicar una operación asíncrona en un grupo de subprocesos 52
Un grupo de subprocesos y el grado de paralelismo 54
Implementar una opción de cancelación 56
Usar un identificador de espera y un tiempo de espera con un grupo de subprocesos 59
usando un temporizador 61
Uso del componente BackgroundWorker 63
Capítulo 4: Uso de la biblioteca paralela de tareas 67
Introducción 67
Creando una tarea 69
Realizar operaciones básicas con una tarea 70
Combinando tareas 72
Convertir el patrón APM en tareas 75
Convertir el patrón EAP en tareas 79
Implementación de una opción de cancelación 81
Manejo de excepciones en tareas 83
Ejecutar tareas en paralelo 85
Ajustar la ejecución de tareas con TaskScheduler 87
Capítulo 5: Uso de C# 6.0 93
Introducción 93
Uso del operador await para obtener resultados de tareas 96
asincrónicas Uso del operador await en una expresión 98
lambda Uso del operador await con tareas asincrónicas consiguientes 100 Uso del operador
await para la ejecución de tareas asincrónicas paralelas 102 Manejo de excepciones en
operaciones asincrónicas 104 Evitar el uso del contexto de sincronización capturado Evitar
el método de vacío asíncrono Diseñar un tipo awaitable 107
personalizado Usar el tipo dinámico con 111
await 114
118
Capítulo 6: Uso de colecciones simultáneas 123
Introducción 123
Uso de diccionario concurrente 125
Implementación de procesamiento asíncrono usando ConcurrentQueue 127
Cambiar el orden de procesamiento asíncrono con ConcurrentStack 130
Creación de un rastreador escalable con ConcurrentBag 132
Generalizando el procesamiento asíncrono con BlockingCollection 136
yo
Machine Translated by Google
Tabla de contenido
Capítulo 7: Uso de PLINQ 141
Introducción 141
Usando la clase Parallel 143
Paralelizar una consulta LINQ 145
Ajustando los parámetros de una consulta PLINQ 148
Manejo de excepciones en una consulta PLINQ 151
Administrar la partición de datos en una consulta PLINQ 153
Creación de un agregador personalizado para una consulta PLINQ 157
Capítulo 8: Extensiones reactivas 161
Introducción 161
Convertir una colección en un Observable asíncrono 162
Escribiendo un Observable personalizado 165
Usando el tipo de Materias 168
Crear un objeto observable 172
Uso de consultas LINQ en una colección observable 174
Creación de operaciones asíncronas con Rx 177
Capítulo 9: Uso de E/S asíncrona 181
Introducción 181
Trabajar con archivos de forma asíncrona 183
Escribir un servidor y un cliente HTTP asíncronos 187
Trabajar con una base de datos de forma asíncrona 190
Llamar a un servicio WCF de forma asíncrona 194
Capítulo 10: Patrones de programación en paralelo 199
Introducción 199
Implementación de estados compartidos evaluados por Lazy 200
Implementación de canalización paralela con BlockingCollection 205
Implementación de canalización paralela con TPL DataFlow 210
Implementando Map/Reduce con PLINQ 215
Capítulo 11: Hay más Introducción 221
Uso de un 221
temporizador en una aplicación de la Plataforma universal de Windows 223
Uso de WinRT desde aplicaciones habituales 227
Uso de BackgroundTask en aplicaciones de la Plataforma universal de Windows 230
Ejecución de una aplicación .NET Core en OS X 237
Ejecución de una aplicación .NET Core en Ubuntu Linux 240
Índice 243
iii
Machine Translated by Google
Machine Translated by Google
Prefacio
No hace mucho tiempo, la CPU típica de una computadora personal tenía solo un núcleo de cómputo, y el consumo
de energía era suficiente para cocinar huevos fritos en él. En 2005, Intel presentó su primera CPU de múltiples núcleos
y, desde entonces, las computadoras comenzaron a desarrollarse en una dirección diferente.
El bajo consumo de energía y una cantidad de núcleos de computación se volvieron más importantes que el
rendimiento de un núcleo de computación en fila. Esto también condujo a cambios en el paradigma de la programación.
Ahora, debemos aprender a usar todos los núcleos de la CPU de manera efectiva para lograr el mejor rendimiento y, al
mismo tiempo, debemos ahorrar energía de la batería ejecutando solo los programas que necesitamos en un momento
determinado. Además de eso, necesitamos programar aplicaciones de servidor de manera que usen múltiples núcleos
de CPU o incluso múltiples computadoras de la manera más eficiente posible para admitir a tantos usuarios como
podamos.
Para poder crear tales aplicaciones, debe aprender a usar múltiples núcleos de CPU en sus programas de manera
efectiva. Si utiliza la plataforma de desarrollo Microsoft .NET y C#, este libro será un punto de partida perfecto para
programar aplicaciones rápidas y receptivas.
El propósito de este libro es brindarle una guía paso a paso para la programación paralela y multiproceso en C#.
Comenzaremos con los conceptos básicos, pasando por temas cada vez más avanzados basados en la información de
los capítulos anteriores, y terminaremos con patrones de programación paralela del mundo real, aplicaciones universales de
Windows y muestras de aplicaciones multiplataforma.
Lo que cubre este libro
El Capítulo 1, Conceptos básicos de creación de subprocesos, presenta las operaciones básicas con subprocesos en C#.
Explica qué es un subproceso, los pros y los contras de usar subprocesos y otros aspectos importantes del subproceso.
El Capítulo 2, Sincronización de subprocesos, describe los detalles de interacción de subprocesos. Aprenderá por qué
necesitamos coordinar hilos juntos y las diferentes formas de organizar la coordinación de hilos.
El Capítulo 3, Uso de un grupo de subprocesos, explica el concepto de grupo de subprocesos. Muestra cómo usar
un grupo de subprocesos, cómo trabajar con operaciones asincrónicas y las buenas y malas prácticas de usar un
grupo de subprocesos.
v
Machine Translated by Google
Prefacio
El Capítulo 4, Uso de la biblioteca paralela de tareas, es una inmersión profunda en el marco de la biblioteca paralela
de tareas (TPL). Este capítulo describe todos los aspectos importantes de TPL, incluida la combinación de tareas, la
gestión de excepciones y la cancelación de operaciones.
El Capítulo 5, Uso de C# 6.0, explica en detalle la característica de C# recientemente introducida: los métodos
asincrónicos. Descubrirá qué significan las palabras clave async y await , cómo usarlas en diferentes escenarios y
cómo funciona await bajo el capó.
El Capítulo 6, Uso de colecciones simultáneas, describe las estructuras de datos estándar para algoritmos paralelos
incluidos en .NET Framework. Pasa por escenarios de programación de muestra para cada estructura de datos.
El Capítulo 7, Uso de PLINQ, es una inmersión profunda en la infraestructura de Parallel LINQ. El capítulo
describe el paralelismo de tareas y datos, la paralelización de una consulta LINQ, el ajuste de las opciones de
paralelismo, la partición de una consulta y la agregación del resultado de la consulta paralela.
El Capítulo 8, Extensiones reactivas, explica cómo y cuándo usar el marco de Extensiones reactivas. Aprenderá
cómo componer eventos y cómo realizar una consulta LINQ en una secuencia de eventos.
El Capítulo 9, Uso de E/S asíncrona, cubre en detalle el proceso de E/S asíncrona, incluidos los escenarios de
archivos, redes y bases de datos.
El Capítulo 10, Patrones de programación en paralelo, describe las soluciones a problemas comunes de
programación en paralelo.
El Capítulo 11, Hay más, cubre los aspectos de la programación de aplicaciones asincrónicas para Windows 10, OS X
y Linux. Aprenderá cómo trabajar con las API asincrónicas de Windows 10 y cómo realizar el trabajo en segundo plano
en las aplicaciones universales de Windows. Además, se familiarizará con las herramientas y los componentes de
desarrollo .NET multiplataforma.
Lo que necesitas para este libro.
Para la mayoría de las recetas, necesitará Microsoft Visual Studio Community 2015. Las recetas del Capítulo 11, Hay
más, para OS X y Linux requerirán opcionalmente el editor de Visual Studio Code. Sin embargo, puede utilizar
cualquier editor específico con el que esté familiarizado.
para quien es este libro
Este libro está escrito para desarrolladores existentes de C# con poca o ninguna experiencia en subprocesos múltiples
y programación asíncrona y paralela. El libro cubre estos temas, desde conceptos básicos hasta patrones y algoritmos
de programación complicados que utilizan el ecosistema C# y .NET.
vi
Machine Translated by Google
Prefacio
Convenciones
En este libro, encontrará varios estilos de texto que distinguen entre diferentes tipos de información. Aquí hay algunos
ejemplos de estos estilos y una explicación de su significado.
Las palabras de código en el texto se muestran de la siguiente manera: "Cuando se ejecuta el programa, crea un
hilo que ejecutará un código en el método PrintNumbersWithDelay ".
Un bloque de código se establece de la siguiente manera:
vacío estático LockTooMuch (objeto lock1, objeto lock2) {
bloquear (bloquear1)
{
Dormir (1000);
cerradura (cerradura2);
}
}
Cualquier entrada o salida de la línea de comandos se escribe de la siguiente manera:
restauración de dotnet
ejecutar dotnet
Los términos nuevos y las palabras importantes se muestran en negrita. Las palabras que ve en la pantalla, en
menús o cuadros de diálogo, por ejemplo, aparecen en el texto de esta manera: "Haga clic con el botón derecho en la
carpeta Referencias en el proyecto y seleccione la opción de menú Administrar paquetes NuGet... ".
Las advertencias o notas importantes aparecen en un cuadro como este.
Los consejos y trucos aparecen así.
viii
Machine Translated by Google
Prefacio
Comentarios del lector
Los comentarios de nuestros lectores es siempre bienvenido. Háganos saber lo que piensa acerca de este libro, lo que
le gustó o no le gustó. Los comentarios de los lectores son importantes para que podamos desarrollar títulos que
realmente aproveches al máximo.
Para enviarnos comentarios generales, simplemente envíe un correo electrónico a feedback@packtpub.com y
mencione el título del libro en el asunto de su mensaje.
Si hay un tema en el que tiene experiencia y está interesado en escribir o contribuir a un libro, consulte
nuestra guía para autores en www.packtpub.com/authors.
Atención al cliente
Ahora que es el orgulloso propietario de un libro de Packt, tenemos una serie de cosas para ayudarlo a aprovechar al
máximo su compra.
Descargando el código de ejemplo
Puede descargar los archivos de código de ejemplo para este libro desde su cuenta en http://
www.packtpub.com . Si compró este libro en otro lugar, puede visitar http://www. packtpub.com/soporte y regístrese
para recibir los archivos directamente por correo electrónico.
Puede descargar los archivos de código siguiendo estos pasos:
1. Inicie sesión o regístrese en nuestro sitio web utilizando su dirección de correo electrónico y contraseña.
2. Pase el puntero del mouse sobre la pestaña SOPORTE en la parte superior.
3. Haga clic en Descargas de códigos y erratas.
4. Introduzca el nombre del libro en el cuadro de búsqueda .
5. Seleccione el libro que está buscando para descargar los archivos de código.
6. Elija del menú desplegable donde compró este libro.
7. Haga clic en Descargar código.
Una vez descargado el archivo, asegúrese de descomprimir o extraer la carpeta con la última versión de:
f WinRAR / 7Zip para Windows
f Zipeg / iZip / UnRarX para Mac f 7Zip /
PeaZip para Linux
viii
Machine Translated by Google
Prefacio
Fe de erratas
Aunque hemos tomado todas las precauciones para garantizar la precisión de nuestro contenido, los
errores ocurren. Si encuentra un error en uno de nuestros libros, tal vez un error en el texto o en el código, le
agradeceríamos que nos lo informara. Al hacerlo, puede salvar a otros lectores de la frustración y ayudarnos a
mejorar las versiones posteriores de este libro. Si encuentra alguna errata, infórmela visitando http://
www.packtpub.com/submiterrata, seleccionando su libro, haciendo clic en el enlace del formulario de envío de
erratas e ingresando los detalles de su errata. Una vez que se verifique su errata, se aceptará su envío y
la errata se cargará en nuestro sitio web, o se agregará a cualquier lista de errata existente, en la sección
Errata de ese título. Cualquier fe de erratas existente se puede ver seleccionando su título en http://www.
packtpub.com/support.
Piratería
La piratería de material protegido por derechos de autor en Internet es un problema constante en todos los
medios. En Packt, nos tomamos muy en serio la protección de nuestros derechos de autor y licencias. Si
encuentra copias ilegales de nuestros trabajos, en cualquier forma, en Internet, indíquenos la dirección de la
ubicación o el nombre del sitio web de inmediato para que podamos buscar una solución.
Póngase en contacto con nosotros en copyright@packtpub.com con un enlace al material sospechoso de piratería.
Agradecemos su ayuda para proteger a nuestros autores y nuestra capacidad para brindarle contenido valioso.
Preguntas
Puede ponerse en contacto con nosotros en question@packtpub.com si tiene algún problema con algún
aspecto del libro, y haremos todo lo posible para solucionarlo.
ix
Machine Translated by Google
Machine Translated by Google
Conceptos básicos de enhebrado
1
En este capítulo, cubriremos las tareas básicas para trabajar con subprocesos en C#. Aprenderás las siguientes
recetas:
f Creando un hilo en C#
f Pausar un hilo f Hacer
que un hilo espere
f Cancelar un hilo
f Determinar el estado de un subproceso
f Prioridad del subproceso
f Subprocesos de primer plano y de fondo
f Pasar parámetros a un subproceso f
Bloquear con una palabra clave de bloqueo de C#
f Bloqueo con una construcción de monitor
f Manejo de excepciones
1
Machine Translated by Google
Conceptos básicos de enhebrado
Introducción
En algún momento del pasado, la computadora común tenía solo una unidad de cómputo y no podía
ejecutar varias tareas de cómputo simultáneamente. Sin embargo, los sistemas operativos ya podían
trabajar con múltiples programas simultáneamente, implementando el concepto de multitarea.
Para evitar la posibilidad de que un programa tome el control de la CPU para siempre, causando
que otras aplicaciones y el propio sistema operativo se cuelguen, los sistemas operativos tenían que
dividir una unidad de computación física en algunos procesadores virtualizados de alguna manera y
dar una cierta cantidad de computación energía a cada programa en ejecución. Además, un sistema
operativo siempre debe tener acceso prioritario a la CPU y debe poder priorizar el acceso de la CPU a
diferentes programas. Un hilo es una implementación de este concepto. Podría considerarse como un
procesador virtual que se asigna a un programa específico y lo ejecuta de forma independiente.
Recuerde que un subproceso consume una cantidad significativa de recursos del sistema
operativo. Intentar compartir un procesador físico entre muchos subprocesos conducirá a una
situación en la que un sistema operativo está ocupado simplemente administrando subprocesos
en lugar de ejecutar programas.
Por lo tanto, si bien era posible mejorar los procesadores de las computadoras, haciéndolos ejecutar más y
más comandos por segundo, trabajar con subprocesos solía ser una tarea del sistema operativo.
No tenía sentido tratar de calcular algunas tareas en paralelo en una CPU de un solo núcleo porque llevaría
más tiempo que ejecutar esos cálculos secuencialmente. Sin embargo, cuando los procesadores
comenzaron a tener más núcleos de cómputo, los programas más antiguos no pudieron aprovechar esto
porque solo usaban un núcleo de procesador.
Para usar el poder de cómputo de un procesador moderno de manera efectiva, es muy importante poder
componer un programa de manera que pueda usar más de un núcleo de cómputo, lo que lleva a
organizarlo como varios hilos que se comunican y sincronizan entre sí.
Las recetas de este capítulo se centran en realizar algunas operaciones muy básicas con subprocesos
en el lenguaje C#. Cubriremos el ciclo de vida de un subproceso, que incluye crear, suspender, hacer
que un subproceso espere y abortar un subproceso, y luego, veremos las técnicas básicas de
sincronización.
Crear un hilo en C#
A lo largo de las siguientes recetas, utilizaremos Visual Studio 2015 como la herramienta principal para
escribir programas multiproceso en C#. Esta receta le mostrará cómo crear un nuevo programa C# y usar
subprocesos en él.
Se puede descargar un IDE gratuito de Visual Studio Community 2015 desde el sitio
web de Microsoft y se puede utilizar para ejecutar los ejemplos de código.
2
Machine Translated by Google
Capítulo 1
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos
previos. El código fuente de esta receta se puede encontrar en el directorio BookSamples\Chapter1\Receta1 .
Descarga del código de ejemplo Puede
descargar los archivos de código de ejemplo para este libro desde su cuenta en http://
www.packtpub.com. Si compró este libro en otro lugar, puede visitar http://www.packtpub.com/
support y registrarse para recibir los archivos por correo electrónico directamente.
Puede descargar los archivos de código siguiendo estos pasos:
f Inicie sesión o regístrese en nuestro sitio web utilizando su dirección de correo electrónico y
contraseña.
f Pase el puntero del mouse sobre la pestaña SOPORTE en la parte superior.
f Haga clic en Descargas de códigos y erratas.
f Introduzca el nombre del libro en el cuadro de búsqueda .
f Seleccione el libro que está buscando para descargar los archivos de código. f Elija del
menú desplegable dónde compró este libro.
f Haga clic en Descargar código.
Una vez descargado el archivo, asegúrese de descomprimir o extraer la carpeta con la última
versión de:
f WinRAR/7Zip para Windows f Zipeg/
iZip / UnRarX para Mac f 7Zip/PeaZip
para Linux
Cómo hacerlo...
Para entender cómo crear un nuevo programa C# y usar subprocesos en él, realice los
siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
3
Machine Translated by Google
Conceptos básicos de enhebrado
2. Asegúrese de que el proyecto utilice .NET Framework 4.6 o superior; sin embargo, el código en
este capítulo funcionará con versiones anteriores.
3. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading; usando
System.Console estático;
4. Agregue el siguiente fragmento de código debajo del método principal :
static void PrintNumbers() {
WriteLine("Iniciando..."); para (int i = 1; i
< 10; i++) {
WriteLine(i);
}
}
4
Machine Translated by Google
Capítulo 1
5. Agregue el siguiente fragmento de código dentro del método principal :
Subproceso t = nuevo subproceso (PrintNumbers); t.Inicio();
ImprimirNúmeros();
6. Ejecute el programa. La salida será algo como la siguiente captura de pantalla:
Cómo funciona...
En los pasos 1 y 2, creamos una aplicación de consola simple en C# usando .Net Framework versión 4.0.
Luego, en el paso 3, incluimos el espacio de nombres System.Threading , que contiene todos los tipos
necesarios para el programa. Luego, usamos la función estática de uso de C# 6.0, que nos permite usar los
métodos estáticos del tipo System.Console sin especificar el nombre del tipo.
Una instancia de un programa que se está ejecutando puede denominarse
proceso. Un proceso consta de uno o más subprocesos. Esto significa que
cuando ejecutamos un programa, siempre tenemos un hilo principal que ejecuta
el código del programa.
En el paso 4, definimos el método PrintNumbers , que se usará tanto en el hilo principal como en el nuevo.
Luego, en el paso 5, creamos un subproceso que ejecuta PrintNumbers. Cuando construimos un subproceso,
se pasa una instancia del delegado ThreadStart o ParameterizedThreadStart al constructor. El compilador de
C# crea este objeto detrás de escena cuando solo escribimos el nombre del método que queremos ejecutar en
un subproceso diferente. Luego, comenzamos un subproceso y ejecutamos PrintNumbers de la manera habitual
en el subproceso principal.
5
Machine Translated by Google
Conceptos básicos de enhebrado
Como resultado, habrá dos rangos de números del 1 al 10 que se cruzarán aleatoriamente.
Esto ilustra que el método PrintNumbers se ejecuta simultáneamente en el subproceso principal y en el otro
subproceso.
Pausar un hilo
Esta receta le mostrará cómo hacer que un subproceso espere un tiempo sin desperdiciar recursos del sistema
operativo.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos. El
código fuente de esta receta se puede encontrar en BookSamples\Chapter1\ Recipe2.
Cómo hacerlo...
Para comprender cómo hacer que un subproceso espere sin desperdiciar recursos del sistema operativo,
realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static void PrintNumbers() {
WriteLine("Iniciando..."); para (int i = 1; i
< 10; i++) {
WriteLine(i);
}
} vacío estático PrintNumbersWithDelay() {
WriteLine("Iniciando..."); para (int i = 1; i
< 10; i++) {
Dormir (TimeSpan.FromSeconds (2));
6
Machine Translated by Google
Capítulo 1
WriteLine(i);
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
Subproceso t = nuevo subproceso (PrintNumbersWithDelay); t.Inicio();
ImprimirNúmeros();
5. Ejecute el programa.
Cómo funciona...
Cuando se ejecuta el programa, crea un hilo que ejecutará un código en el método
PrintNumbersWithDelay . Inmediatamente después, ejecuta el método PrintNumbers . La característica
clave aquí es agregar la llamada al método Thread.Sleep a un método PrintNumbersWithDelay .
Hace que el subproceso que ejecuta este código espere una cantidad de tiempo específica (2 segundos
en nuestro caso) antes de imprimir cada número. Mientras un subproceso duerme, utiliza el menor tiempo de CPU
posible. Como resultado, veremos que el código del método PrintNumbers , que generalmente se ejecuta
más tarde, se ejecutará antes que el código del método PrintNumbersWithDelay en un subproceso separado.
Hacer esperar un hilo
Esta receta le mostrará cómo un programa puede esperar a que se complete algún cálculo en otro subproceso
para usar su resultado más adelante en el código. No es suficiente usar el método Thread.Sleep porque no
sabemos el tiempo exacto que tomará el cálculo.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter1\Recipe3.
Cómo hacerlo...
Para comprender cómo un programa espera que se complete algún cálculo en otro subproceso para usar su
resultado más tarde, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading;
7
Machine Translated by Google
Conceptos básicos de enhebrado
usando System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
vacío estático PrintNumbersWithDelay() {
WriteLine("Iniciando..."); para (int i =
1; i < 10; i++) {
Dormir (TimeSpan.FromSeconds (2));
WriteLine(i);
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
WriteLine("Iniciando..."); Subproceso
t = nuevo subproceso (PrintNumbersWithDelay); t.Inicio(); t.Unirse();
WriteLine("Subproceso completado");
5. Ejecute el programa.
Cómo funciona...
Cuando se ejecuta el programa, ejecuta un subproceso de ejecución prolongada que imprime números y
espera dos segundos antes de imprimir cada número. Pero, en el programa principal, llamamos al método
t.Join , que nos permite esperar a que el subproceso t termine de funcionar. Cuando se completa, el programa
principal continúa ejecutándose. Con la ayuda de esta técnica, es posible sincronizar los pasos de ejecución
entre dos hilos. El primero espera hasta que el otro esté completo y luego continúa trabajando. Mientras el
primer subproceso espera, está en un estado bloqueado (como en la receta anterior cuando llama a
Thread.Sleep).
Abortar un hilo
En esta receta, describiremos cómo abortar la ejecución de otro subproceso.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter1\Recipe4.
8
Machine Translated by Google
Capítulo 1
Cómo hacerlo...
Para comprender cómo abortar la ejecución de otro subproceso, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading; usando
System.Console estático;
3. Usando el System.Threading.Thread estático, agregue el siguiente fragmento de código debajo del método
Main :
vacío estático PrintNumbersWithDelay() {
WriteLine("Iniciando..."); para (int i = 1; i
< 10; i++) {
Dormir (TimeSpan.FromSeconds (2)); WriteLine(i);
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
WriteLine("Iniciando programa..."); Subproceso t =
nuevo subproceso (PrintNumbersWithDelay); t.Inicio();
Thread.Sleep(TimeSpan.FromSeconds(6)); t.Abort();
WriteLine("Un hilo ha sido abortado"); Subproceso t = nuevo
subproceso (PrintNumbers); t.Inicio(); ImprimirNúmeros();
5. Ejecute el programa.
9
Machine Translated by Google
Conceptos básicos de enhebrado
Cómo funciona...
Cuando se ejecutan el programa principal y un subproceso de impresión de números separado, esperamos seis
segundos y luego llamamos al método t.Abort en un subproceso. Esto inyecta un método ThreadAbortException
en un subproceso, lo que hace que finalice. Es muy peligroso, generalmente porque esta excepción puede
ocurrir en cualquier momento y puede destruir totalmente la aplicación. Además, no siempre es posible terminar
un hilo con esta técnica. El subproceso de destino puede negarse a abortar manejando esta excepción llamando al
método Thread.ResetAbort . Por lo tanto, no se recomienda que utilice el método Abort para cerrar un hilo. Hay
diferentes métodos preferidos, como proporcionar un objeto CancellationToken para cancelar la ejecución de un
subproceso. Este enfoque se describirá en el Capítulo 3, Uso de un grupo de subprocesos.
Determinar el estado de un hilo
Esta receta describirá los posibles estados que podría tener un hilo. Es útil para obtener información sobre si un
subproceso ya se inició o si se encuentra en un estado bloqueado. Tenga en cuenta que debido a que un subproceso
se ejecuta de forma independiente, su estado se puede cambiar en cualquier momento.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter1\Recipe5.
Cómo hacerlo...
Para comprender cómo determinar el estado de un subproceso y adquirir información útil sobre él, realice los
siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
vacío estático hacer nada () {
Dormir (TimeSpan.FromSeconds (2));
}
vacío estático PrintNumbersWithStatus()
10
Machine Translated by Google
Capítulo 1
{
WriteLine("Iniciando...");
WriteLine(CurrentThread.ThreadState.ToString()); para (int i = 1; i < 10;
i++) {
Dormir (TimeSpan.FromSeconds (2));
WriteLine(i);
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
WriteLine("Iniciando programa..."); Subproceso t
= nuevo subproceso (PrintNumbersWithStatus); Subproceso t2 =
nuevo Subproceso (Hacer Nada);
WriteLine(t.ThreadState.ToString()); t2.Inicio();
t.Inicio(); para
(int i = 1; i < 30; i++) {
WriteLine(t.ThreadState.ToString());
}
Dormir (TimeSpan.FromSeconds (6)); t.Abort();
WriteLine("Un
hilo ha sido abortado"); WriteLine(t.ThreadState.ToString());
WriteLine(t2.ThreadState.ToString());
5. Ejecute el programa.
Cómo funciona...
Cuando se inicia el programa principal, define dos subprocesos diferentes; uno de ellos será abortado
y el otro se ejecutará con éxito. El estado del subproceso se encuentra en la propiedad ThreadState de un
objeto Thread , que es una enumeración de C#. Al principio, el subproceso tiene un estado
ThreadState.Unstarted . Luego, lo ejecutamos y asumimos que durante las 30 iteraciones de un ciclo, el
subproceso cambiará su estado de ThreadState.Running a ThreadState.WaitSleepJoin.
Tenga en cuenta que siempre se puede acceder al objeto Thread actual a
través de la propiedad estática Thread.CurrentThread.
11
Machine Translated by Google
Conceptos básicos de enhebrado
Si esto no sucede, simplemente aumente el número de iteraciones. Luego, abortamos el primer hilo y vemos
que ahora tiene un estado ThreadState.Aborted . También es posible que el programa imprima el estado
ThreadState.AbortRequested . Esto ilustra muy bien la complejidad de sincronizar dos hilos. Tenga en cuenta que no
debe usar el aborto de subprocesos en sus programas. Lo he cubierto aquí solo para mostrar el estado del hilo
correspondiente.
Finalmente, podemos ver que nuestro segundo subproceso t2 se completó con éxito y ahora tiene un estado
ThreadState.Stopped . Hay varios otros estados, pero están en parte obsoletos y no son tan útiles como los que
examinamos.
Tarea prioritaria
Esta receta describirá las diferentes opciones para la prioridad de subprocesos. Establecer una prioridad de hilo
determina cuánto tiempo de CPU se le dará a un hilo.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter1\Recipe6.
Cómo hacerlo...
Para comprender el funcionamiento de la prioridad de subprocesos, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
System.Threading; usando System.Console
estático; usando System.Threading.Thread estático;
utilizando System.Diagnostics.Process estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
vacío estático Ejecutar subprocesos ()
{
var muestra = nuevo ThreadSample();
var threadOne = new Thread(sample.CountNumbers);
subprocesoUno.Nombre = "SubprocesoUno";
var threadTwo = new Thread(sample.CountNumbers);
subprocesoDos.Nombre = "SubprocesoDos";
threadOne.Priority = ThreadPriority.Highest;
12
Machine Translated by Google
Capítulo 1
threadTwo.Priority = ThreadPriority.Lowest; hiloUno.Inicio();
hiloDos.Inicio();
Dormir (TimeSpan.FromSeconds (2)); muestra.Stop();
muestra de hilo de clase {
privado bool _isStopped = falso;
vacío público Detener() {
_isDetenido = verdadero;
}
public void ContarNúmeros() {
contador largo = 0;
mientras (!_está detenido) {
contador++;
}
WriteLine($"{CurrentThread.Name} con " +
$"{CurrentThread.Priority,11} prioridad " $"tiene un recuento = +
{contador,13:N0}");
}
}
4. Agregue el siguiente fragmento de código dentro del método
Main : WriteLine($"Prioridad actual del subproceso: {CurrentThread.Priority}");
WriteLine("Ejecutándose en todos los núcleos disponibles");
Ejecutar
subprocesos(); Dormir
(TimeSpan.FromSeconds (2)); WriteLine("Ejecutándose
en un solo núcleo"); GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
Ejecutar subprocesos();
5. Ejecute el programa.
13
Machine Translated by Google
Conceptos básicos de enhebrado
Cómo funciona...
Cuando se inicia el programa principal, define dos subprocesos diferentes. El primero, threadOne, tiene la prioridad de
subproceso más alta ThreadPriority.Highest, mientras que el segundo, threadTwo, tiene la prioridad
ThreadPriority.Lowest más baja. Imprimimos el valor de prioridad del subproceso principal y luego comenzamos estos
dos subprocesos en todos los núcleos disponibles. Si tenemos más de un núcleo de cómputo, deberíamos obtener un
resultado inicial en dos segundos. El subproceso de mayor prioridad debería calcular más iteraciones normalmente,
pero ambos valores deberían estar cerca.
Sin embargo, si hay otros programas ejecutándose que cargan todos los núcleos de la CPU, la situación podría ser
bastante diferente.
Para simular esta situación, configuramos la opción ProcessorAffinity , instruyendo al sistema operativo para
ejecutar todos nuestros subprocesos en un solo núcleo de CPU (número 1). Ahora, los resultados deberían ser muy
diferentes y los cálculos tardarán más de dos segundos. Esto sucede porque el núcleo de la CPU ejecuta principalmente el
subproceso de alta prioridad, dando al resto de los subprocesos muy poco tiempo.
Tenga en cuenta que esta es una ilustración de cómo funciona un sistema operativo con la priorización de subprocesos.
Por lo general, no debe escribir programas que se basen en este comportamiento.
Hilos de primer plano y de fondo
Esta receta describirá qué son los subprocesos en primer plano y en segundo plano y cómo la configuración de esta
opción afecta el comportamiento del programa.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter1\Recipe7.
Cómo hacerlo...
Para comprender el efecto de los subprocesos en primer plano y en segundo plano en un programa, realice los siguientes
pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
System.Threading; usando System.Console
estático; usando System.Threading.Thread estático;
14
Machine Translated by Google
Capítulo 1
3. Agregue el siguiente fragmento de código debajo del método principal :
muestra de hilo de clase {
iteraciones privadas de solo lectura int;
ThreadSample pública (iteraciones int) {
_iteraciones = iteraciones;
} public void CountNumbers() {
for (int i = 0; i < _iteraciones; i++) {
Dormir (TimeSpan.FromSeconds (0.5));
WriteLine($"{CurrentThread.Name} imprime {i}");
}
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
var sampleForeground = new ThreadSample(10); var sampleBackground
= new ThreadSample(20);
var threadOne = new Thread(sampleForeground.CountNumbers); subprocesoUno.Nombre =
"Subproceso en primer plano"; var threadTwo = new
Thread(sampleBackground.CountNumbers); subprocesoDos.Nombre = "Subproceso de fondo";
threadTwo.IsBackground = true;
hiloUno.Inicio(); hiloDos.Inicio();
5. Ejecute el programa.
Cómo funciona...
Cuando se inicia el programa principal, define dos subprocesos diferentes. De forma predeterminada, un
subproceso que creamos explícitamente es un subproceso en primer plano. Para crear un subproceso de fondo,
establecemos manualmente la propiedad IsBackground del objeto threadTwo en verdadero. Configuramos estos
hilos de manera que el primero se complete más rápido y luego ejecutamos el programa.
15
Machine Translated by Google
Conceptos básicos de enhebrado
Una vez que se completa el primer subproceso, el programa se cierra y el subproceso en segundo plano
finaliza. Esta es la principal diferencia entre los dos: un proceso espera a que se completen todos los subprocesos
en primer plano antes de finalizar el trabajo, pero si tiene subprocesos en segundo plano, simplemente se
cierran.
También es importante mencionar que si un programa define un subproceso en primer plano que no se completa;
el programa principal no finaliza correctamente.
Pasar parámetros a un hilo
Esta receta describirá cómo proporcionar el código que ejecutamos en otro hilo con los datos requeridos.
Revisaremos las diferentes formas de cumplir con esta tarea y revisaremos los errores comunes.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter1\Recipe8.
Cómo hacerlo...
Para comprender cómo pasar parámetros a un subproceso, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
Recuento de vacíos estáticos (iteraciones de objetos) {
CountNumbers((int)iteraciones);
}
static void CountNumbers(int iteraciones) {
for (int i = 1; i <= iteraciones; i++) {
Dormir (TimeSpan.FromSeconds (0.5));
WriteLine($"{CurrentThread.Name} imprime {i}");
}
dieciséis
Machine Translated by Google
Capítulo 1
static void PrintNumber(int número) {
WriteLine(número);
}
muestra de hilo de clase {
iteraciones privadas de solo lectura int;
ThreadSample pública (iteraciones int) {
_iteraciones = iteraciones;
} public void CountNumbers() {
for (int i = 1; i <= _iteraciones; i++) {
Dormir (TimeSpan.FromSeconds (0.5));
WriteLine($"{CurrentThread.Name} imprime {i}");
}
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
var muestra = nuevo ThreadSample(10);
var threadOne = new Thread(sample.CountNumbers); subprocesoUno.Nombre
= "SubprocesoUno"; hiloUno.Inicio();
hiloUno.Unirse();
Línea de escritura("");
var threadTwo = nuevo Thread(Count);
subprocesoDos.Nombre = "SubprocesoDos";
hiloDos.Inicio(8); hiloDos.Unirse();
Línea de escritura("");
var threadThree = new Thread(() => CountNumbers(12)); subprocesoTres.Nombre =
"SubprocesoTres";
17
Machine Translated by Google
Conceptos básicos de enhebrado
hiloTres.Inicio();
subprocesoTres.Unirse();
Línea de escritura("");
int i = 10; var
threadFour = new Thread(() => PrintNumber(i)); yo = 20; var threadFive = new
Thread(()
=> PrintNumber(i));
hiloCuatro.Inicio();
hiloCinco.Inicio();
5. Ejecute el programa.
Cómo funciona...
Cuando se inicia el programa principal, primero crea un objeto de la clase ThreadSample ,
proporcionándole una serie de iteraciones. Luego, comenzamos un hilo con el método
CountNumbers del objeto . Este método se ejecuta en otro hilo, pero usa el número 10, que es el
valor que le pasamos al constructor del objeto. Por lo tanto, simplemente pasamos este número
de iteraciones a otro subproceso de la misma manera indirecta.
Hay más…
Otra forma de pasar datos es usar el método Thread.Start aceptando un objeto que se puede pasar a
otro hilo. Para que funcione de esta manera, un método que comenzamos en otro hilo debe aceptar
un solo parámetro del tipo objeto. Esta opción se ilustra mediante la creación de un subproceso
threadTwo . Pasamos 8 como un objeto al método Count , donde se convierte en un tipo entero .
La siguiente opción implica el uso de expresiones lambda. Una expresión lambda define un método
que no pertenece a ninguna clase. Creamos un método de este tipo que invoca otro método con los
argumentos necesarios y lo iniciamos en otro hilo. Cuando comenzamos el subproceso tres ,
imprime 12 números, que son exactamente los números que le pasamos a través de la expresión
lambda.
El uso de expresiones lambda implica otra construcción de C# llamada closure. Cuando usamos
cualquier variable local en una expresión lambda, C# genera una clase y convierte esta variable en una
propiedad de esta clase. Entonces, en realidad, hacemos lo mismo que en el subproceso threadOne ,
pero no definimos la clase nosotros mismos; el compilador de C# hace esto automáticamente.
Esto podría conducir a varios problemas; por ejemplo, si usamos la misma variable de varias lambdas,
en realidad compartirán el valor de esta variable. Esto se ilustra en el ejemplo anterior donde,
cuando iniciamos hiloCuatro e hiloCinco, ambos imprimen 20 porque la variable se cambió para
contener el valor 20 antes de que se iniciaran ambos hilos.
18
Machine Translated by Google
Capítulo 1
Bloqueo con una palabra clave de bloqueo de C#
Esta receta describirá cómo garantizar que cuando un subproceso usa algún recurso, otro no lo usa
simultáneamente. Veremos por qué es necesario esto y de qué se trata el concepto de seguridad de
subprocesos.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter1\Recipe9.
Cómo hacerlo...
Para entender cómo usar la palabra clave de bloqueo de C#, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading; usando
System.Console estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
TestCounter vacío estático (CounterBase c)
{
para (int i = 0; i < 100000; i++) {
c.Incremento();
c.Decremento();
}
}
Contador de clase: CounterBase
{
público int Contar { obtener; conjunto privado; }
public override void Increment() {
Contar++;
}
invalidación pública decremento()
19
Machine Translated by Google
Conceptos básicos de enhebrado
{
Contar;
}
}
clase CounterWithLock : CounterBase
{
objeto privado de solo lectura _syncRoot = new Object();
público int Contar { obtener; conjunto privado; }
public override void Increment() {
bloquear (_syncRoot) {
Contar++;
}
}
public override void Decrement() {
bloquear (_syncRoot) {
Contar;
}
}
}
clase abstracta CounterBase
{
Incremento vacío abstracto público ();
public abstract void Decrement();
}
4. Agregue el siguiente fragmento de código dentro del método principal :
WriteLine("Contador incorrecto");
var c = nuevo Contador();
var t1 = nuevo hilo (() => TestCounter (c));
var t2 = nuevo hilo (() => TestCounter (c)); var t3 = nuevo hilo (() =>
TestCounter (c)); t1.Inicio();
20
Machine Translated by Google
Capítulo 1
t2.Inicio();
t3.Inicio();
t1.Unirse();
t2.Unirse();
t3.Unirse();
WriteLine($"Cuenta total: {c.Cuenta}"); Línea de
escritura("");
WriteLine("Contador correcto");
var c1 = nuevo ContadorConBloqueo();
t1 = hilo nuevo(() => TestCounter(c1)); t2 = hilo nuevo(() =>
TestCounter(c1)); t3 = hilo nuevo(() => TestCounter(c1));
t1.Inicio();
t2.Inicio();
t3.Inicio();
t1.Unirse();
t2.Unirse();
t3.Unirse();
WriteLine($"Recuento total: {c1.Count}");
5. Ejecute el programa.
Cómo funciona...
Cuando se inicia el programa principal, primero crea un objeto de la clase Contador . Esta clase
define un contador simple que se puede incrementar y decrementar. Luego, comenzamos tres
subprocesos que comparten la misma instancia de contador y realizamos un incremento y una
disminución en un ciclo. Esto conduce a resultados no deterministas. Si ejecutamos el programa
varias veces, imprimirá varios valores de contador diferentes. Podría ser 0, pero en su mayoría no lo será.
Esto sucede porque la clase Counter no es segura para subprocesos. Cuando varios subprocesos
acceden al contador al mismo tiempo, el primer subproceso obtiene el valor del contador 10 y lo
incrementa a 11. Luego, un segundo subproceso obtiene el valor 11 y lo incrementa a 12. El primer
subproceso obtiene el valor del contador 12, pero antes de que tenga lugar un decremento, un segundo
subproceso también obtiene el valor de contador 12 . Luego, el primer hilo decrementa de 12 a 11 y lo
guarda en el contador, y el segundo hilo simultáneamente hace lo mismo. Como resultado, tenemos dos
incrementos y solo un decremento, lo que obviamente no es correcto. Este tipo de situación se denomina
condición de carrera y es una causa muy común de errores en un entorno de subprocesos múltiples.
21
Machine Translated by Google
Conceptos básicos de enhebrado
Para asegurarnos de que esto no suceda, debemos asegurarnos de que mientras un subproceso trabaja con el
contador, todos los demás subprocesos esperan hasta que el primero finaliza el trabajo. Podemos usar la palabra clave
lock para lograr este tipo de comportamiento. Si bloqueamos un objeto, todos los demás subprocesos que requieren
acceso a este objeto esperarán en un estado bloqueado hasta que se desbloquee. Esto podría ser un problema grave de
rendimiento y más adelante, en el Capítulo 2, Sincronización de subprocesos, obtendrá más información al respecto.
Bloqueo con una construcción de monitor
Esta receta ilustra otro error común de subprocesos múltiples llamado interbloqueo. Dado que un interbloqueo
hará que un programa deje de funcionar, la primera pieza de este ejemplo es una nueva construcción de
Monitor que nos permite evitar un interbloqueo. Luego, la palabra clave de bloqueo descrita anteriormente se
usa para obtener un interbloqueo.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter1\Recipe10.
Cómo hacerlo...
Para comprender el interbloqueo del error multiproceso, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
vacío estático LockTooMuch (objeto lock1, objeto lock2) {
cerradura (cerradura1)
{
Dormir (1000);
cerradura (cerradura2);
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
objeto lock1 = nuevo objeto();
22
Machine Translated by Google
Capítulo 1
objeto lock2 = nuevo objeto();
hilo nuevo(() => LockTooMuch(lock1, lock2)).Start();
cerradura (cerradura2)
{
Subproceso.Sueño(1000);
WriteLine("Monitor.TryEnter permite no quedarse atascado, devolviendo falso después de que haya
transcurrido un tiempo de espera especificado");
if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5))) {
WriteLine("Adquirió un recurso protegido con éxito");
}
demás
{
WriteLine("¡Tiempo de espera para adquirir un recurso!");
}
}
hilo nuevo(() => LockTooMuch(lock1, lock2)).Start();
Línea de escritura("");
cerradura (cerradura2)
{
WriteLine("¡Esto será un punto muerto!"); Dormir (1000);
bloquear (bloquear1)
{
WriteLine("Adquirió un recurso protegido con éxito");
}
}
5. Ejecute el programa.
Cómo funciona...
Comencemos con el método LockTooMuch . En este método, simplemente bloqueamos el primer objeto,
esperamos un segundo y luego bloqueamos el segundo objeto. Luego, comenzamos este método en otro
hilo e intentamos bloquear el segundo objeto y luego el primer objeto del hilo principal.
Si usamos la palabra clave lock como en la segunda parte de esta demostración, habrá un interbloqueo.
El primer subproceso mantiene un bloqueo en el objeto lock1 y espera mientras el objeto lock2 se libera;
el subproceso principal mantiene un bloqueo en el objeto lock2 y espera a que el objeto lock1 se libere,
lo que nunca sucederá en esta situación.
23
Machine Translated by Google
Conceptos básicos de enhebrado
En realidad, la palabra clave lock es azúcar sintáctica para el uso de la clase Monitor . Si tuviéramos
que desensamblar el código con bloqueo, veríamos que se convierte en el siguiente fragmento de código:
bool adquiridoBloqueo = falso; intentar {
Monitor.Enter(bloquearObjeto, ref adquiridoBloquear);
// Código que accede a los recursos que están protegidos por el candado.
} finalmente
{
si (bloqueo adquirido) {
Monitor.Salir(bloquearObjeto);
}
}
Por lo tanto, podemos usar la clase Monitor directamente; tiene el método TryEnter , que acepta un
parámetro de tiempo de espera y devuelve falso si este parámetro de tiempo de espera caduca antes
de que podamos adquirir el recurso protegido por bloqueo.
Manejo de excepciones
Esta receta describirá cómo manejar las excepciones en otros subprocesos correctamente. Es muy
importante colocar siempre un bloque try/catch dentro del hilo porque no es posible capturar una
excepción fuera del código de un hilo.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter1\Recipe11.
Cómo hacerlo...
Para comprender el manejo de excepciones en otros subprocesos, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading;
24
Machine Translated by Google
Capítulo 1
usando System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
vacío estático subproceso defectuoso() {
WriteLine("Iniciando un hilo defectuoso..."); Dormir
(TimeSpan.FromSeconds (2)); lanzar una nueva
excepción ("¡Boom!");
}
vacío estático subproceso defectuoso ()
{
intentar
{
WriteLine("Iniciando un hilo defectuoso..."); Dormir
(TimeSpan.FromSeconds (1)); lanzar una nueva
excepción ("¡Boom!");
} catch (excepción ex) {
WriteLine($"Excepción manejada: {ex.Message}");
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
var t = hilo nuevo (hilo defectuoso); t.Inicio();
t.Unirse();
intentar
t = hilo nuevo (hilo defectuoso); t.Inicio();
} catch (excepción ex) {
WriteLine("¡No llegaremos aquí!");
}
5. Ejecute el programa.
25
Machine Translated by Google
Conceptos básicos de enhebrado
Cómo funciona...
Cuando se inicia el programa principal, define dos subprocesos que generarán una excepción. Uno de estos
subprocesos maneja una excepción, mientras que el otro no. Puede ver que la segunda excepción no es capturada
por un bloque try/catch alrededor del código que inicia el hilo. Por lo tanto, si trabaja con subprocesos directamente, la
regla general es no lanzar una excepción desde un subproceso, sino usar un bloque try/catch dentro de un código de
subproceso.
En las versiones anteriores de .NET Framework (1.0 y 1.1), este comportamiento era diferente y las excepciones
no detectadas no forzaban el cierre de la aplicación. Es posible usar esta política agregando un archivo de
configuración de la aplicación (como app.config) que contiene el siguiente fragmento de código:
<configuración>
<tiempo de ejecución>
<legacyUnhandledExceptionPolicy habilitado="1" /> </runtime> </
configuration>
26
Machine Translated by Google
Sincronización de subprocesos
2
En este capítulo, describiremos algunas de las técnicas comunes para trabajar con recursos compartidos de
varios subprocesos. Aprenderás las siguientes recetas:
f Realización de operaciones atómicas básicas
f Uso de la construcción Mutex f Uso
de la construcción SemaphoreSlim
f Uso de la construcción AutoResetEvent
f Usando la construcción ManualResetEventSlim f Usando la
construcción CountDownEvent
f Uso de la construcción Barrera
f Usando la construcción ReaderWriterLockSlim f Usando la
construcción SpinWait
Introducción
Como vimos en el Capítulo 1, Conceptos básicos de creación de subprocesos, es problemático
utilizar un objeto compartido simultáneamente desde varios subprocesos. Sin embargo, es muy importante
sincronizar esos subprocesos para que realicen operaciones en ese objeto compartido en una secuencia adecuada.
En la fórmula Bloqueo con una palabra clave de bloqueo de C# , nos enfrentamos a un problema llamado
condición de carrera. El problema ocurrió porque la ejecución de esos subprocesos múltiples no se sincronizó
correctamente. Cuando un subproceso realiza operaciones de incremento y decremento, los otros subprocesos
deben esperar su turno. Organizar subprocesos de tal manera a menudo se denomina sincronización de subprocesos.
Hay varias formas de lograr la sincronización de subprocesos. Primero, si no hay un objeto compartido, no hay
necesidad de sincronización en absoluto. Sorprendentemente, es muy frecuente que podamos deshacernos de
construcciones de sincronización complejas simplemente rediseñando nuestro programa y eliminando un estado
compartido. Si es posible, simplemente evite usar un solo objeto de varios subprocesos.
27
Machine Translated by Google
Sincronización de subprocesos
Si debemos tener un estado compartido, el segundo enfoque es usar solo operaciones atómicas . Esto significa que una
operación toma una sola cantidad de tiempo y se completa de una vez, por lo que ningún otro subproceso puede realizar
otra operación hasta que se complete la primera operación. Por lo tanto, no hay necesidad de hacer que otros subprocesos
esperen a que se complete esta operación y no hay necesidad de usar bloqueos; esto a su vez, excluye la situación de
interbloqueo.
Si esto no es posible y la lógica del programa es más complicada, entonces tenemos que usar diferentes
construcciones para coordinar hilos. Un grupo de estas construcciones pone un subproceso en espera en un estado
bloqueado . En un estado bloqueado , un subproceso utiliza el menor tiempo de CPU posible.
Sin embargo, esto significa que incluirá al menos uno de los llamados cambios de contexto: el programador de
subprocesos de un sistema operativo guardará el estado del subproceso en espera y cambiará a otro subproceso,
restaurando su estado por turno. Esto requiere una cantidad considerable de recursos; sin embargo, si el hilo va a estar
suspendido por mucho tiempo, es bueno. Este tipo de construcciones también se denominan construcciones en modo
kernel porque solo el kernel de un sistema operativo puede evitar que un subproceso use el tiempo de la CPU.
En caso de que tengamos que esperar por un corto período de tiempo, es mejor simplemente esperar que cambiar
el hilo a un estado bloqueado . Esto nos ahorrará el cambio de contexto a costa de un poco de tiempo de CPU
desperdiciado mientras el hilo está esperando. Tales construcciones se conocen como construcciones de modo de
usuario . Son muy livianos y rápidos, pero desperdician mucho tiempo de CPU en caso de que un subproceso tenga
que esperar mucho tiempo.
Para utilizar lo mejor de ambos mundos, existen construcciones híbridas ; estos intentan usar el modo de espera
de usuario primero y luego, si un subproceso espera lo suficiente, cambia al estado bloqueado , ahorrando
recursos de CPU.
En este capítulo, veremos los aspectos de la sincronización de subprocesos. Cubriremos cómo realizar operaciones
atómicas y cómo usar las construcciones de sincronización existentes incluidas en .NET Framework.
Realización de operaciones atómicas básicas.
Esta receta le mostrará cómo realizar operaciones atómicas básicas en un objeto para evitar la condición de carrera sin
bloquear subprocesos.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter2\Recipe1.
Cómo hacerlo...
Para comprender las operaciones atómicas básicas, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
28
Machine Translated by Google
Capitulo 2
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading; usando
System.Console estático;
3. Debajo del método principal , agregue el siguiente fragmento de código:
TestCounter vacío estático (CounterBase c) {
para (int i = 0; i < 100000; i++) {
c.Incremento();
c.Decremento();
}
}
Contador de clase: CounterBase
{
privado int _recuento;
public int Cuenta => _cuenta;
public override void Increment() {
_contar++;
}
public override void Decrement() {
_contar;
}
}
clase CounterNoLock : CounterBase
{
privado int _recuento;
public int Cuenta => _cuenta;
public override void Increment() {
Interlocked.Increment(ref _count);
}
public override void Decrement() {
29
Machine Translated by Google
Sincronización de subprocesos
Interlocked.Decrement(ref _count);
}
}
clase abstracta CounterBase
{
Incremento vacío abstracto público ();
public abstract void Decrement();
}
4. Dentro del método Main , agregue el siguiente fragmento de código:
WriteLine("Contador incorrecto");
var c = nuevo Contador();
var t1 = nuevo hilo (() => TestCounter (c)); var t2 = nuevo hilo (() =>
TestCounter (c)); var t3 = nuevo hilo (() => TestCounter (c));
t1.Inicio();
t2.Inicio();
t3.Inicio();
t1.Unirse();
t2.Unirse();
t3.Unirse();
WriteLine($"Cuenta total: {c.Cuenta}"); Línea de
escritura("");
WriteLine("Contador correcto");
var c1 = nuevo CounterNoLock();
t1 = hilo nuevo(() => TestCounter(c1)); t2 = hilo nuevo(() =>
TestCounter(c1)); t3 = hilo nuevo(() => TestCounter(c1));
t1.Inicio(); t2.Inicio(); t3.Inicio(); t1.Unirse(); t2.Unirse();
t3.Unirse();
WriteLine($"Recuento total: {c1.Count}");
5. Ejecute el programa.
30
Machine Translated by Google
Capitulo 2
Cómo funciona...
Cuando el programa se ejecuta, crea tres hilos que ejecutarán un código en el método TestCounter . Este método
ejecuta una secuencia de operaciones de incremento/decremento en un objeto.
Inicialmente, el objeto Counter no es seguro para subprocesos y aquí obtenemos una condición de carrera.
Entonces, en el primer caso, un valor de contador no es determinista. Podríamos obtener un valor cero; sin
embargo, si ejecuta el programa varias veces, eventualmente obtendrá algún resultado incorrecto distinto de cero.
En el Capítulo 1, Principios básicos de creación de subprocesos, resolvimos este problema bloqueando
nuestro objeto, lo que provocó que otros subprocesos se bloquearan mientras un subproceso obtiene el valor del
contador anterior y luego calcula y asigna un nuevo valor al contador. Sin embargo, si ejecutamos esta operación
de tal manera que no se puede detener a la mitad, lograríamos el resultado adecuado sin ningún tipo de
bloqueo, y esto es posible con la ayuda de la construcción Interlocked . Proporciona los métodos atómicos
Increment, Decrement y Add para matemáticas básicas, y nos ayuda a escribir la clase Counter sin el uso de
bloqueo.
Usando la construcción Mutex
Esta receta describirá cómo sincronizar dos programas separados usando la construcción Mutex .
Una construcción Mutex es una primitiva de sincronización que otorga acceso exclusivo del recurso compartido
a un solo subproceso.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter2\Recipe2.
Cómo hacerlo...
Para entender la sincronización de dos programas separados usando la construcción Mutex , realice los
siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
System.Threading; usando System.Console estático;
3. Dentro del método Main , agregue el siguiente fragmento de código:
const string MutexName = "CSharpThreadingCookbook";
usando (var m = new Mutex (falso, MutexName))
31
Machine Translated by Google
Sincronización de subprocesos
{
if (!m.WaitOne(TimeSpan.FromSeconds(5), false)) {
WriteLine("¡Se está ejecutando la segunda instancia!");
} demás
{
WriteLine("¡Corriendo!");
LeerLínea();
m.ReleaseMutex();
}
}
4. Ejecute el programa.
Cómo funciona...
Cuando se inicia el programa principal, define un mutex con un nombre específico, proporcionando el indicador
initialOwner como falso. Esto permite que el programa adquiera un mutex si ya se ha creado. Luego, si no se adquiere
ningún mutex, el programa simplemente muestra En ejecución y espera a que se presione cualquier tecla para liberar
el mutex y salir.
Si iniciamos una segunda copia del programa, esperará 5 segundos, intentando adquirir el mutex.
Si pulsamos cualquier tecla en la primera copia de un programa, la segunda comenzará la ejecución.
Sin embargo, si seguimos esperando 5 segundos, la segunda copia del programa no podrá adquirir el mutex.
¡Tenga en cuenta que un mutex es un objeto de sistema operativo global! Cierre
siempre el mutex correctamente; la mejor opción es envolver un objeto mutex en
un bloque de uso.
Esto hace posible sincronizar hilos en diferentes programas, lo que podría ser útil en una gran cantidad de escenarios.
Uso de la construcción SemaphoreSlim
Esta receta le mostrará cómo limitar el acceso multiproceso a algunos recursos con la ayuda de la construcción
SemaphoreSlim . SemaphoreSlim es una versión ligera de Semaphore; limita la cantidad de subprocesos que pueden
acceder a un recurso al mismo tiempo.
32
Machine Translated by Google
Capitulo 2
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter2\Recipe3.
Cómo hacerlo...
Para comprender cómo limitar un acceso multiproceso a un recurso con la ayuda de la
SemaphoreSlim , realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Debajo del método principal , agregue el siguiente fragmento de código:
static SemaphoreSlim _semaphore = new SemaphoreSlim(4);
static void AccessDatabase(nombre de cadena, int segundos) {
WriteLine($"{name} espera para acceder a una base de datos");
_semáforo.Espera();
WriteLine($"{nombre} obtuvo acceso a una base de datos"); Dormir (TimeSpan.FromSeconds
(segundos)); WriteLine($"{name} is complete");
_semáforo.Release();
4. Dentro del método Main , agregue el siguiente fragmento de código:
para (int i = 1; i <= 6; i++) {
string threadName = "Subproceso" int segundos + yo;
en espera = 2 + 2 * i; var t = new Thread(() =>
AccessDatabase(threadName,
segundos de espera));
t.Inicio();
}
5. Ejecute el programa.
33
Machine Translated by Google
Sincronización de subprocesos
Cómo funciona...
Cuando se inicia el programa principal, crea una instancia de SemaphoreSlim , especificando la cantidad de subprocesos
simultáneos permitidos en su constructor. Luego, inicia seis subprocesos con diferentes nombres y horas de inicio
para ejecutarse.
Cada subproceso intenta adquirir acceso a una base de datos, pero restringimos el número de accesos simultáneos a
una base de datos a cuatro subprocesos con la ayuda de un semáforo. Cuando cuatro subprocesos obtienen acceso a
una base de datos, los otros dos subprocesos esperan hasta que uno de los subprocesos anteriores finaliza su trabajo y
señala a otros subprocesos llamando al método _semaphore.Release .
Hay más…
Aquí, usamos una construcción híbrida, que nos permite guardar un cambio de contexto en los casos en que el tiempo de
espera es muy corto. Sin embargo, existe una versión anterior de esta construcción llamada Semaphore.
Esta versión es una construcción pura en tiempo de kernel. No tiene sentido usarlo, excepto en un escenario muy
importante; podemos crear un semáforo con nombre como un mutex con nombre y usarlo para sincronizar
subprocesos en diferentes programas. SemaphoreSlim no usa semáforos del kernel de Windows y no admite la
sincronización entre procesos, así que use Semaphore en este caso.
Uso de la construcción AutoResetEvent
En esta receta, hay un ejemplo de cómo enviar notificaciones de un hilo a otro con la ayuda de una construcción
AutoResetEvent . AutoResetEvent notifica a un subproceso en espera que se ha producido un evento.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter2\Recipe4.
Cómo hacerlo...
Para entender cómo enviar notificaciones de un hilo a otro con la ayuda del
construcción AutoResetEvent , realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando System.Threading;
34
Machine Translated by Google
Capitulo 2
usando System.Console estático; usando
System.Threading.Thread estático;
3. Debajo del método principal , agregue el siguiente fragmento de código:
AutoResetEvent estático privado _workerEvent = nuevo
AutoResetEvent (falso);
AutoResetEvent estático privado _mainEvent = nuevo
AutoResetEvent (falso);
Proceso de vacío estático (int segundos) {
WriteLine("Comenzando un trabajo de larga duración..."); Dormir
(TimeSpan.FromSeconds (segundos)); WriteLine("¡El trabajo
está hecho!"); _workerEvent.Set();
WriteLine("Esperando que un
subproceso principal complete su trabajo"); _mainEvent.WaitOne(); WriteLine("Iniciando la segunda
operación..."); Dormir
(TimeSpan.FromSeconds (segundos)); WriteLine("¡El trabajo está
hecho!");
_workerEvent.Set();
}
4. Dentro del método Main , agregue el siguiente fragmento de código:
var t = nuevo hilo (() => Proceso (10)); t.Inicio();
WriteLine("Esperando que otro subproceso complete el trabajo"); _workerEvent.WaitOne();
WriteLine("¡Se completó la primera
operación!"); WriteLine("Realizando una operación en un subproceso
principal"); Dormir (TimeSpan.FromSeconds (5)); _mainEvent.Set(); WriteLine("Ahora
ejecutando la segunda operación en un segundo
subproceso");
_workerEvent.WaitOne(); WriteLine("Se completó la segunda operación!");
5. Ejecute el programa.
35
Machine Translated by Google
Sincronización de subprocesos
Cómo funciona...
Cuando se inicia el programa principal, define dos instancias de AutoResetEvent . Uno de ellos es para la señalización
desde el segundo subproceso al subproceso principal, y el segundo es para la señalización desde el subproceso principal
al segundo subproceso. Proporcionamos false al constructor AutoResetEvent , especificando el estado inicial de
ambas instancias como sin señalizar. Esto significa que cualquier subproceso que llame al método WaitOne de uno
de estos objetos se bloqueará hasta que llamemos al método Set . Si inicializamos el estado del evento en verdadero,
se señala y el primer subproceso que llama a WaitOne procederá de inmediato. Luego, el estado del evento deja de
estar señalado automáticamente, por lo que debemos llamar al método Set una vez más para permitir que los otros
subprocesos llamen al método WaitOne en esta instancia para continuar.
Luego, creamos un segundo hilo, que ejecuta la primera operación durante 10 segundos y espera la señal del segundo hilo.
La señal notifica que se completó la primera operación.
Ahora, el segundo hilo espera una señal del hilo principal. Realizamos un trabajo adicional en el hilo principal y enviamos
una señal llamando al método _mainEvent.Set . Luego, esperamos otra señal del segundo hilo.
AutoResetEvent es una construcción en tiempo de kernel, por lo que si el tiempo de espera no es significativo, es mejor
usar la siguiente receta con ManualResetEventslim, que es una construcción híbrida.
Uso de la construcción ManualResetEventSlim
Esta receta describirá cómo hacer que la señalización entre subprocesos sea más flexible con la construcción
ManualResetEventSlim .
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter2\Recipe5.
Cómo hacerlo...
Para comprender el uso de la construcción ManualResetEventSlim , realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
System.Threading; usando System.Console
estático; usando System.Threading.Thread estático;
36
Machine Translated by Google
Capitulo 2
3. Debajo del método Main , agregue el siguiente código:
static void TravelThroughGates(string threadName, int segundos) {
WriteLine($"{threadName} se duerme"); Dormir
(TimeSpan.FromSeconds (segundos));
WriteLine($"{threadName} espera a que se abran las puertas!"); _mainEvent.Esperar();
WriteLine($"{threadName}
entra por las puertas!");
}
estático ManualResetEventSlim _mainEvent = nuevo
ManualResetEventSlim(falso);
4. Dentro del método Main , agregue el siguiente código:
var t1 = new Thread(() => TravelThroughGates("Thread 1", 5)); var t2 = new Thread(() =>
TravelThroughGates("Thread 2", 6)); var t3 = new Thread(() => TravelThroughGates("Thread 3",
12)); t1.Inicio(); t2.Inicio(); t3.Inicio(); Dormir (TimeSpan.FromSeconds (6)); WriteLine("¡Las puertas
ya están
abiertas!");
_mainEvent.Set();
Dormir (TimeSpan.FromSeconds (2));
_mainEvent.Reset(); WriteLine("¡Las puertas han sido
cerradas!"); Dormir
(TimeSpan.FromSeconds (10)); WriteLine("¡Las
puertas ahora están abiertas
por segunda vez!"); _mainEvent.Set(); Dormir
(TimeSpan.FromSeconds (2)); WriteLine("¡Las
puertas han sido cerradas!"); _mainEvent.Reset();
5. Ejecute el programa.
Cómo funciona...
Cuando se inicia el programa principal, primero crea una instancia de la construcción
ManualResetEventSlim . Luego, iniciamos tres subprocesos que esperan este evento para
indicarles que continúen la ejecución.
37
Machine Translated by Google
Sincronización de subprocesos
Todo el proceso de trabajar con esta construcción es como dejar pasar a la gente por una puerta.
El evento AutoResetEvent que vimos en la receta anterior funciona como un torniquete, lo que permite que solo pase
una persona a la vez. ManualResetEventSlim, que es una versión híbrida de ManualResetEvent, permanece abierto
hasta que llamamos manualmente al método Reset . Volviendo al código, cuando llamamos a _mainEvent.Set, lo abrimos
y permitimos que los hilos que están listos para aceptar esta señal continúen funcionando. Sin embargo, el hilo número
tres todavía está dormido y no llega a tiempo. Llamamos a _mainEvent.Reset y así lo cerramos. El último subproceso ahora
está listo para continuar, pero debe esperar la siguiente señal, que sucederá unos segundos más tarde.
Hay más…
Como en una de las recetas anteriores, usamos una construcción híbrida que carece de la posibilidad de
funcionar a nivel de sistema operativo. Si necesitamos tener un evento global, debemos usar la construcción
EventWaitHandle , que es la clase base para AutoResetEvent y
Evento de reinicio manual.
Uso de la construcción CountDownEvent
Esta receta describirá cómo usar la construcción de señalización CountdownEvent para esperar hasta que se complete
una cierta cantidad de operaciones.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter2\Recipe6.
Cómo hacerlo...
Para comprender el uso de la construcción CountDownEvent , realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
System.Threading; usando System.Console
estático; usando System.Threading.Thread estático;
3. Debajo del método Main , agregue el siguiente código:
static CountdownEvent _countdown = new CountdownEvent(2);
static void PerformOperation(mensaje de cadena, int segundos)
38
Machine Translated by Google
Capitulo 2
{
Dormir (TimeSpan.FromSeconds (segundos));
WriteLine(mensaje); _cuenta
atrás.Señal();
}
4. Dentro del método Main , agregue el siguiente código:
WriteLine("Iniciando dos operaciones"); var t1 = new
Thread(() => PerformOperation("Operación 1 completada", 4)); var t2 = new Thread(() =>
PerformOperation("Operación
2 completada", 8)); t1.Inicio(); t2.Inicio(); _cuenta atrás.Esperar(); WriteLine("Ambas
operaciones han sido
completadas.");
_countdown.Dispose();
5. Ejecute el programa.
Cómo funciona...
Cuando se inicia el programa principal, creamos una nueva instancia de CountdownEvent , especificando
que queremos que señale cuando se completan dos operaciones en su constructor. Luego, comenzamos
dos subprocesos que señalan al evento cuando están completos. Tan pronto como se completa el
segundo subproceso, el subproceso principal regresa de esperar en CountdownEvent y continúa. Con
esta construcción, es muy conveniente esperar a que se completen varias operaciones asincrónicas.
Sin embargo, hay una desventaja significativa; _countdown.Wait() esperará para siempre si no
llamamos a _countdown.Signal() la cantidad de veces requerida. Asegúrese de que todos sus
subprocesos se completen con la llamada al método Signal cuando use CountdownEvent.
Uso de la construcción Barrera
Esta receta ilustra otra construcción de sincronización interesante llamada Barrier. La construcción
Barrier ayuda a organizar varios subprocesos para que se encuentren en algún momento, proporcionando
una devolución de llamada que se ejecutará cada vez que los subprocesos llamen al método
SignalAndWait .
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter2\Recipe7.
39
Machine Translated by Google
Sincronización de subprocesos
Cómo hacerlo...
Para comprender el uso de la construcción Barrera , realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Debajo del método Main , agregue el siguiente código:
Barrera estática _barrier = nueva barrera (2,
b => WriteLine($"Fin de fase {b.CurrentPhaseNumber + 1}"));
static void PlayMusic (nombre de cadena, mensaje de cadena, segundos int) {
para (int i = 1; i < 3; i++) {
Línea de escritura("" ); Dormir (TimeSpan.FromSeconds
(segundos)); WriteLine($"{nombre} comienza a {mensaje}");
Dormir (TimeSpan.FromSeconds (segundos)); WriteLine($"{nombre}
termina en {mensaje}"); _barrier.SignalAndWait();
}
}
4. Dentro del método Main , agregue el siguiente código:
var t1 = new Thread(() => PlayMusic("el guitarrista", "toca un solo increíble", 5)); var t2 = new
Thread(() => PlayMusic("el
cantante", "canta su canción", 2));
t1.Inicio();
t2.Inicio();
5. Ejecute el programa.
Cómo funciona...
Creamos una construcción Barrier , especificando que queremos sincronizar dos subprocesos, y después de que cada uno
de esos dos subprocesos llame al método _barrier.SignalAndWait , debemos ejecutar una devolución de llamada que
imprimirá la cantidad de fases completadas.
40
Machine Translated by Google
Capitulo 2
Cada subproceso enviará una señal a Barrier dos veces, por lo que tendremos dos fases. Cada vez
que ambos subprocesos llamen al método SignalAndWait , Barrier ejecutará la devolución de llamada.
Es útil para trabajar con algoritmos de iteración de subprocesos múltiples, para ejecutar algunos cálculos
en cada final de iteración. El final de la iteración se alcanza cuando el último subproceso llama al
método SignalAndWait .
Uso de la construcción ReaderWriterLockSlim
Esta receta describirá cómo crear un mecanismo seguro para subprocesos para leer y escribir
en una colección desde varios subprocesos mediante una construcción ReaderWriterLockSlim .
ReaderWriterLockSlim representa un candado que se utiliza para administrar el acceso a un recurso, lo
que permite múltiples subprocesos para lectura o acceso exclusivo para escritura.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter2\Recipe8.
Cómo hacerlo...
Para comprender cómo crear un mecanismo seguro para subprocesos para leer y escribir en una colección
desde varios subprocesos mediante la construcción ReaderWriterLockSlim , realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
usando System.Collections.Generic; utilizando
System.Threading; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Debajo del método Main , agregue el siguiente código:
estático ReaderWriterLockSlim _rw = nuevo ReaderWriterLockSlim(); Diccionario estático<int,
int> _items = new Diccionario<int, int>();
vacío estático Leer () {
WriteLine("Leyendo el contenido de un diccionario");
mientras (verdadero)
{
intentar
41
Machine Translated by Google
Sincronización de subprocesos
_rw.EnterReadLock(); foreach
(clave var en _items.Keys) {
Dormir (Intervalo de tiempo. FromSeconds (0.1));
}
} finalmente
{
_rw.ExitReadLock();
}
}
}
static void Write(cadena threadName) {
mientras (verdadero)
{
intentar
{
int newKey = new Random().Next(250);
_rw.EnterUpgradeableReadLock(); if (!
_items.ContainsKey(nuevaClave)) {
intentar
_rw.EnterWriteLock();
_items[nuevaClave] = 1;
WriteLine($"Nueva clave {nuevaClave} se agrega a un diccionario por
a {threadName}"); }
finalmente {
_rw.ExitWriteLock();
}
}
Dormir (Intervalo de tiempo. FromSeconds (0.1));
} finalmente
{
_rw.ExitUpgradeableReadLock();
}
}
}
42
Machine Translated by Google
Capitulo 2
4. Dentro del método Main , agregue el siguiente código:
nuevo hilo (leer) { IsBackground = true }. Start (); nuevo hilo (leer) { IsBackground
= true }. Start (); nuevo hilo (leer) { IsBackground = true }. Start ();
hilo nuevo(() => Escribir("Hilo 1"))
{ EsFondo = verdadero }.Start(); hilo nuevo(() =>
Escribir("Hilo 2"))
{ EsFondo = verdadero }.Start();
Dormir (TimeSpan.FromSeconds (30));
5. Ejecute el programa.
Cómo funciona...
Cuando se inicia el programa principal, ejecuta simultáneamente tres subprocesos que leen datos de un
diccionario y dos subprocesos que escriben algunos datos en este diccionario. Para lograr la seguridad de
subprocesos, usamos la construcción ReaderWriterLockSlim , que fue diseñada especialmente para tales
escenarios.
Tiene dos tipos de bloqueos: un bloqueo de lectura que permite leer varios subprocesos y un bloqueo de
escritura que bloquea todas las operaciones de otros subprocesos hasta que se libera este bloqueo de escritura.
También hay un escenario interesante cuando obtenemos un bloqueo de lectura, leemos algunos datos de la
colección y, dependiendo de esos datos, decidimos obtener un bloqueo de escritura y cambiar la colección. Si
obtenemos los bloqueos de escritura a la vez, se gasta demasiado tiempo, lo que no permite que nuestros lectores
lean los datos porque la colección se bloquea cuando obtenemos un bloqueo de escritura. Para minimizar este
tiempo, existen métodos EnterUpgradeableReadLock/ExitUpgradeableReadLock . Obtenemos un bloqueo de lectura
y leemos los datos; si encontramos que tenemos que cambiar la colección subyacente, simplemente actualizamos
nuestro bloqueo usando el método EnterWriteLock , luego realizamos una operación de escritura rápidamente y
liberamos un bloqueo de escritura usando ExitWriteLock.
En nuestro caso, obtenemos un número aleatorio; luego obtenemos un bloqueo de lectura y verificamos si este número
existe en la colección de claves del diccionario. Si no, actualizamos nuestro bloqueo a un bloqueo de escritura y
luego agregamos esta nueva clave a un diccionario. Es una buena práctica usar bloques try/finally para
asegurarnos de que siempre liberemos los bloqueos después de adquirirlos.
Todos nuestros subprocesos se han creado como subprocesos en segundo plano y, después de esperar 30
segundos, se completan el subproceso principal y todos los subprocesos en segundo plano.
43
Machine Translated by Google
Sincronización de subprocesos
Usando la construcción SpinWait
Esta receta describirá cómo esperar en un subproceso sin involucrar construcciones en modo kernel.
Además, presentamos SpinWait, una construcción de sincronización híbrida diseñada para esperar en el modo de
usuario durante algún tiempo y luego cambiar al modo kernel para ahorrar tiempo de CPU.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter2\Recipe9.
Cómo hacerlo...
Para comprender cómo esperar en un subproceso sin involucrar construcciones en modo kernel, realice los siguientes
pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Debajo del método Main , agregue el siguiente código:
bool volátil estático _isCompleted = falso;
vacío estático UserModeWait()
{
while (!_isCompleted) {
Escribir(".");
}
Línea de escritura();
WriteLine("Se completó la espera");
}
vacío estático HybridSpinWait() {
var w = new SpinWait(); while (!
_isCompleted) {
w.SpinOnce();
44
Machine Translated by Google
Capitulo 2
WriteLine(w.NextSpinWillYield);
}
WriteLine("Se completó la espera");
}
4. Dentro del método Main , agregue el siguiente código:
var t1 = nuevo hilo (UserModeWait); var t2 = nuevo
hilo (HybridSpinWait);
WriteLine("Modo de usuario en ejecución esperando");
t1.Inicio();
Dormir (20);
_isCompleted = verdadero;
Dormir (TimeSpan.FromSeconds (1));
_isCompleted = falso;
WriteLine("Ejecutando la construcción híbrida SpinWait esperando"); t2.Inicio();
Dormir (5);
_isCompleted
= verdadero;
5. Ejecute el programa.
Cómo funciona...
Cuando se inicia el programa principal, define un subproceso que ejecutará un ciclo sin fin durante 20
milisegundos hasta que el subproceso principal establezca la variable _isCompleted en verdadero.
Podríamos experimentar y ejecutar este ciclo durante 2030 segundos, midiendo la carga de la CPU con
el administrador de tareas de Windows. Mostrará una cantidad significativa de tiempo de procesador,
dependiendo de cuántos núcleos tenga la CPU.
Usamos la palabra clave volatile para declarar el campo estático _isCompleted . La palabra clave volatile
indica que un campo puede ser modificado por varios subprocesos que se ejecutan al mismo tiempo. Los
campos que se declaran volátiles no están sujetos a las optimizaciones del compilador y del procesador
que asumen el acceso mediante un solo subproceso. Esto garantiza que el valor más actualizado esté
presente en el campo en todo momento.
Luego, usamos una versión de SpinWait , que en cada iteración imprime una bandera especial que nos
muestra si un hilo va a cambiar a un estado bloqueado . Ejecutamos este hilo durante 5 milisegundos para
ver eso. Al principio, SpinWait intenta permanecer en el modo de usuario y, después de unas nueve
iteraciones, comienza a cambiar el hilo a un estado bloqueado. Si intentamos medir la carga de la CPU con
esta versión, no veremos ningún uso de la CPU en el administrador de tareas de Windows.
45
Machine Translated by Google
Machine Translated by Google
Uso de un grupo de subprocesos
3
En este capítulo, describiremos las técnicas comunes que se utilizan para trabajar con recursos compartidos de varios
subprocesos. Aprenderás las siguientes recetas:
f Invocar a un delegado en un grupo de subprocesos
f Publicar una operación asíncrona en un grupo de subprocesos f Un
grupo de subprocesos y el grado de paralelismo
f Implementación de una opción de cancelación
f Uso de un identificador de espera y tiempo de espera con un grupo de subprocesos
f Uso de un temporizador
f Uso del componente BackgroundWorker
Introducción
En los capítulos anteriores, discutimos varias formas de crear subprocesos y organizar su cooperación. Ahora,
consideremos otro escenario en el que crearemos muchas operaciones asincrónicas que tardan muy poco tiempo en
completarse. Como discutimos en la sección Introducción del Capítulo 1, Conceptos básicos de creación de subprocesos,
la creación de un subproceso es una operación costosa, por lo que hacer esto para cada operación asincrónica de corta
duración incluirá un gasto general significativo.
Para lidiar con este problema, existe un enfoque común llamado agrupación que se puede aplicar con éxito a cualquier
situación en la que necesitemos muchos recursos costosos y de corta duración. Asignamos una cierta cantidad de
estos recursos por adelantado y los organizamos en un grupo de recursos. Cada vez que necesitamos un nuevo
recurso, simplemente lo tomamos del grupo, en lugar de crear uno nuevo, y lo devolvemos al grupo cuando ya no se
necesita el recurso.
47
Machine Translated by Google
Uso de un grupo de subprocesos
El grupo de subprocesos de .NET es una implementación de este concepto. Es accesible a través del Sistema.
Tipo Threading.ThreadPool . Un grupo de subprocesos es administrado por .NET Common Language Runtime (CLR), lo que
significa que hay una instancia de un grupo de subprocesos por CLR. El tipo ThreadPool tiene un método estático
QueueUserWorkItem que acepta un delegado, que representa una operación asincrónica definida por el usuario. Después
de llamar a este método, este delegado va a la cola interna. Luego, si no hay subprocesos dentro del grupo, crea un
nuevo subproceso de trabajo y coloca el primer delegado en la cola.
Si colocamos nuevas operaciones en un grupo de subprocesos, después de que se completen las operaciones anteriores,
es posible reutilizar este único subproceso para ejecutar estas operaciones. Sin embargo, si colocamos nuevas
operaciones más rápido, el grupo de subprocesos creará más subprocesos para atender estas operaciones. Hay un límite para
evitar la creación de demasiados subprocesos y, en ese caso, las nuevas operaciones esperan en la cola hasta que los
subprocesos de trabajo en el grupo estén libres para atenderlos.
¡Es muy importante mantener las operaciones en un grupo de subprocesos de corta duración! No coloque
operaciones de ejecución prolongada en un grupo de subprocesos ni bloquee los subprocesos de
trabajo. Esto hará que todos los subprocesos de trabajo se ocupen y ya no podrán atender las
operaciones de los usuarios. Esto, a su vez, dará lugar a problemas de rendimiento y errores que son
muy difíciles de depurar.
Cuando dejamos de poner nuevas operaciones en un grupo de subprocesos, eventualmente eliminará los subprocesos que ya
no son necesarios después de estar inactivos durante algún tiempo. Esto liberará cualquier recurso del sistema operativo que
ya no sea necesario.
Me gustaría enfatizar una vez más que un grupo de subprocesos está destinado a ejecutar operaciones de ejecución corta. El
uso de un grupo de subprocesos nos permite ahorrar recursos del sistema operativo a costa de reducir el grado de paralelismo.
Usamos menos subprocesos, pero ejecutamos operaciones asincrónicas más lentamente de lo habitual, agrupandolas por
lotes según la cantidad de subprocesos de trabajo disponibles. Esto tiene sentido si las operaciones se completan
rápidamente, pero degradará el rendimiento si ejecutamos muchas operaciones vinculadas a la computación de ejecución
prolongada.
Otra cosa importante con la que hay que tener mucho cuidado es usar un grupo de subprocesos en las aplicaciones ASP.NET.
La infraestructura ASP.NET utiliza un grupo de subprocesos en sí mismo, y si desperdicia todos los subprocesos de
trabajo de un grupo de subprocesos, un servidor web ya no podrá atender las solicitudes entrantes. Se recomienda que
use solo operaciones asincrónicas vinculadas a entrada/salida en ASP.NET porque usan diferentes mecanismos llamados
subprocesos de E/S. Hablaremos de los subprocesos de E/S en el Capítulo 9, Uso de E/S asíncrona.
Tenga en cuenta que los subprocesos de trabajo en un grupo de subprocesos son subprocesos en
segundo plano. Esto significa que cuando todos los subprocesos en primer plano (incluido el subproceso
de la aplicación principal) estén completos, todos los subprocesos en segundo plano se detendrán.
En este capítulo, aprenderá a utilizar un grupo de subprocesos para ejecutar operaciones asincrónicas. Cubriremos diferentes
formas de poner una operación en un grupo de subprocesos y cómo cancelar una operación y evitar que se ejecute durante
mucho tiempo.
48
Machine Translated by Google
Capítulo 3
Invocar a un delegado en un grupo de subprocesos
Esta receta le mostrará cómo ejecutar un delegado de forma asincrónica en un grupo de subprocesos.
Además, analizaremos un enfoque denominado Modelo de programación asincrónica (APM), que históricamente
fue el primer patrón de programación asincrónica en .NET.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter3\Recipe1.
Cómo hacerlo...
Para comprender cómo invocar a un delegado en un grupo de subprocesos, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
System.Threading; usando System.Console
estático; usando System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
cadena de delegado privado RunOnThreadPool(out int threadId);
Devolución de llamada vacía estática privada (IAsyncResult ar) {
WriteLine("Iniciando una devolución de llamada...");
WriteLine($"Estado pasado a callbak: {ar.AsyncState}"); WriteLine($"Es el subproceso
del grupo de subprocesos:
{CurrentThread.IsThreadPoolThread}");
WriteLine($"Id. de subproceso de trabajo del grupo de
subprocesos: {CurrentThread.ManagedThreadId}"); }
Prueba de cadena estática privada (out int threadId) {
WriteLine("Iniciando..."); WriteLine($"Es
el subproceso del grupo de subprocesos:
{CurrentThread.IsThreadPoolThread}"); Dormir
(TimeSpan.FromSeconds (2));
49
Machine Translated by Google
Uso de un grupo de subprocesos
threadId = CurrentThread.ManagedThreadId; return $"El id. del
subproceso del trabajador del grupo de subprocesos era: {threadId}";
}
4. Agregue el siguiente código dentro del método Main :
int threadId = 0;
RunOnThreadPool poolDelegate = Prueba;
var t = new Thread(() => Test(out threadId)); t.Inicio(); t.Unirse();
WriteLine($"Id del subproceso: {threadId}");
IAsyncResult r = poolDelegate.BeginInvoke(out threadId, Callback, "una llamada asíncrona delegada");
r.AsyncWaitHandle.WaitOne();
resultado de cadena = poolDelegate.EndInvoke(out threadId, r);
WriteLine($"Id. de subproceso del trabajador del grupo de subprocesos: {threadId}");
WriteLine(resultado);
Dormir (TimeSpan.FromSeconds (2));
5. Ejecute el programa.
Cómo funciona...
Cuando el programa se ejecuta, crea un hilo a la antigua usanza y luego lo inicia y espera a que finalice.
Dado que un constructor de subprocesos solo acepta un método que no devuelve ningún resultado,
usamos una expresión lambda para concluir una llamada al método Test .
Nos aseguramos de que este subproceso no sea del grupo de subprocesos imprimiendo el subproceso.
Valor de la propiedad CurrentThread.IsThreadPoolThread . También imprimimos un ID de subproceso
administrado para identificar un subproceso en el que se ejecutó este código.
50
Machine Translated by Google
Capítulo 3
Luego, definimos un delegado y lo ejecutamos llamando al método BeginInvoke . Este método acepta una devolución
de llamada que se llamará después de que se complete la operación asíncrona y un estado definido por el usuario para
pasar a la devolución de llamada. Este estado se suele utilizar para distinguir una llamada asíncrona de otra. Como
resultado, obtenemos un objeto de resultado que implementa la interfaz IAsyncResult . El método BeginInvoke devuelve
el resultado inmediatamente, lo que nos permite continuar con cualquier trabajo mientras se ejecuta la operación
asincrónica en un subproceso de trabajo del grupo de subprocesos. Cuando necesitamos el resultado de una operación
asincrónica, usamos el objeto de resultado devuelto por la llamada al método BeginInvoke . Podemos sondearlo usando la
propiedad de resultado IsCompleted , pero en este caso, usamos la propiedad de resultado AsyncWaitHandle para
esperar hasta que se complete la operación. Una vez hecho esto, para obtener un resultado, llamamos al método
EndInvoke en un delegado, pasando los argumentos del delegado y nuestro objeto IAsyncResult .
En realidad, no es necesario usar AsyncWaitHandle. Si comentamos
r.AsyncWaitHandle.WaitOne, el código aún se ejecutará correctamente
porque el método EndInvoke en realidad espera a que se complete la
operación asincrónica. Siempre es importante llamar a EndInvoke (o
EndOperationName para otras API asincrónicas) porque arroja cualquier
excepción no controlada al subproceso de llamada. Siempre llame a los
métodos Begin y End cuando use este tipo de API asincrónica.
Cuando se complete la operación, se enviará una devolución de llamada al método BeginInvoke en un grupo de
subprocesos, más específicamente, en un subproceso de trabajo. Si comentamos el Thread.
Llamada al método de suspensión al final de la definición del método principal , la devolución de llamada no se
ejecutará. Esto se debe a que cuando se completa el subproceso principal, se detendrán todos los subprocesos en segundo
plano, incluida esta devolución de llamada. Es posible que tanto las llamadas asincrónicas a un delegado como una
devolución de llamada sean atendidas por el mismo subproceso de trabajo, lo cual es fácil de ver por un identificador de
subproceso de trabajo.
Este enfoque de usar el método BeginOperationName/EndOperationName y el objeto IAsyncResult en .NET se denomina
modelo de programación asincrónica o patrón APM, y dichos pares de métodos se denominan métodos asincrónicos.
Este patrón todavía se usa en varias API de biblioteca de clases de .NET, pero en la programación moderna, es preferible
usar Task Parallel Library (TPL) para organizar una API asíncrona. Cubriremos este tema en el Capítulo 4, Uso de la biblioteca
paralela de tareas.
51
Machine Translated by Google
Uso de un grupo de subprocesos
Publicar una operación asíncrona en un grupo de
subprocesos
Esta receta describirá cómo poner una operación asincrónica en un grupo de subprocesos.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter3\Recipe2.
Cómo hacerlo...
Para comprender cómo publicar una operación asincrónica en un grupo de subprocesos, realice los
siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
System.Threading; usando System.Console
estático; usando System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
vacío estático privado AsyncOperation (estado del objeto) {
WriteLine($"Operation state: {state ?? "(null)"}"); WriteLine($"Id. de
subproceso de trabajo: {CurrentThread.ManagedThreadId}"); Dormir (TimeSpan.FromSeconds
(2));
}
4. Agregue el siguiente fragmento de código dentro del método principal :
constante int x = 1;
constante int y = 2;
const string lambdaState = "estado lambda 2";
ThreadPool.QueueUserWorkItem(AsyncOperation); Dormir
(TimeSpan.FromSeconds (1));
ThreadPool.QueueUserWorkItem(AsyncOperation, "estado asíncrono");
52
Machine Translated by Google
Capítulo 3
Dormir (TimeSpan.FromSeconds (1));
ThreadPool.QueueUserWorkItem( estado => {
WriteLine($"Estado de operación: {estado}"); WriteLine($"Id. de
subproceso de trabajo: {CurrentThread.ManagedThreadId}"); Dormir (TimeSpan.FromSeconds (2)); },
"estado lambda");
ThreadPool.QueueUserWorkItem( { _ =>
WriteLine($"Estado de operación: {x + y}, {lambdaState}"); WriteLine($"Id. de subproceso
de trabajo: {CurrentThread.ManagedThreadId}"); Dormir (TimeSpan.FromSeconds (2)); }, "estado
lambda");
Dormir (TimeSpan.FromSeconds (2));
5. Ejecute el programa.
Cómo funciona...
Primero, definimos el método AsyncOperation que acepta un solo parámetro del tipo de objeto . Luego, publicamos
este método en un grupo de subprocesos utilizando el método QueueUserWorkItem . Luego, publicamos este método una
vez más, pero esta vez, pasamos un objeto de estado a esta llamada de método. Este objeto se pasará al método
AsynchronousOperation como parámetro de estado .
Hacer que un subproceso duerma durante 1 segundo después de estas operaciones permite que el grupo de subprocesos
reutilice los subprocesos para nuevas operaciones. Si comenta estas llamadas de Thread.Sleep , lo más seguro es que los ID
de subprocesos sean diferentes en todos los casos. Si no, probablemente los dos primeros subprocesos se reutilizarán
para ejecutar las siguientes dos operaciones.
Primero, publicamos una expresión lambda en un grupo de subprocesos. Nada especial aquí; en lugar de definir un método
separado, usamos la sintaxis de expresión lambda.
En segundo lugar, en lugar de pasar el estado de una expresión lambda, usamos la mecánica de cierre .
Esto nos brinda más flexibilidad y nos permite proporcionar más de un objeto para la operación asíncrona y tipificación
estática para esos objetos. Por lo tanto, el mecanismo anterior de pasar un objeto a un método de devolución de llamada
es realmente redundante y obsoleto. No hay necesidad de usarlo ahora que tenemos cierres en C#.
53
Machine Translated by Google
Uso de un grupo de subprocesos
Un grupo de subprocesos y el grado de paralelismo
Esta receta le mostrará cómo funciona un grupo de subprocesos con muchas operaciones asincrónicas y en qué
se diferencia de la creación de muchos subprocesos separados.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter3\Recipe3.
Cómo hacerlo...
Para aprender cómo funciona un grupo de subprocesos con muchas operaciones asincrónicas y en qué se
diferencia de la creación de muchos subprocesos separados, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando System.Diagnostics;
utilizando System.Threading;
usando System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static void UseThreads(int numberOfOperations) {
usando (var countdown = new CountdownEvent(numberOfOperations)) {
WriteLine("Programación del trabajo mediante la creación de hilos"); for
(int i = 0; i < numeroDeOperaciones; i++) {
var hilo = nuevo hilo (() => {
Write($"{CurrentThread.ManagedThreadId},"); Dormir (Intervalo
de tiempo. FromSeconds (0.1)); cuenta
regresiva.Señal(); });
hilo.Inicio();
} cuenta regresiva. Espera ();
54
Machine Translated by Google
Capítulo 3
Línea de escritura();
}
}
static void UseThreadPool(int numberOfOperations) {
usando (var countdown = new CountdownEvent(numberOfOperations)) {
WriteLine("Comenzando a trabajar en un threadpool"); for (int i = 0; i <
numeroDeOperaciones; i++) {
ThreadPool.QueueUserWorkItem( { _ =>
Write($"{CurrentThread.ManagedThreadId},"); Dormir (Intervalo de
tiempo. FromSeconds (0.1)); cuenta regresiva.Señal(); });
} cuenta regresiva. Espera
(); Línea de escritura();
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
const int numeroDeOperaciones = 500; var sw = nuevo
Cronómetro(); sw.Inicio();
UseThreads(númeroDeOperaciones); sw.Stop();
WriteLine($"Tiempo de ejecución usando subprocesos:
{sw.ElapsedMilliseconds}");
sw.Reset();
sw.Inicio();
UseThreadPool(númeroDeOperaciones); sw.Stop();
WriteLine($"Tiempo de ejecución usando el grupo de subprocesos:
{sw.ElapsedMilliseconds}");
5. Ejecute el programa.
55
Machine Translated by Google
Uso de un grupo de subprocesos
Cómo funciona...
Cuando se inicia el programa principal, creamos muchos hilos diferentes y ejecutamos una operación en
cada uno de ellos. Esta operación imprime una ID de subproceso y bloquea un subproceso durante 100
milisegundos. Como resultado, creamos 500 subprocesos que ejecutan todas estas operaciones en paralelo.
El tiempo total en mi máquina es de unos 300 milisegundos, pero consumimos muchos recursos del sistema
operativo para todos estos subprocesos.
Luego, seguimos el mismo flujo de trabajo, pero en lugar de crear un hilo para cada operación, las
publicamos en un grupo de hilos. Después de esto, el grupo de subprocesos comienza a atender estas
operaciones; comienza a crear más hilos cerca del final; sin embargo, todavía lleva mucho más tiempo, unos 12
segundos en mi máquina. Ahorramos memoria e hilos para el uso del sistema operativo, pero lo pagamos con
el rendimiento de la aplicación.
Implementar una opción de cancelación
Esta receta muestra un ejemplo de cómo cancelar una operación asincrónica en un grupo de subprocesos.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter3\Recipe4.
Cómo hacerlo...
Para comprender cómo implementar una opción de cancelación en un hilo, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
System.Threading; usando System.Console
estático; usando System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static void AsyncOperation1 (token de CancelaciónToken) {
WriteLine("Comenzando la primera tarea"); para (int i =
0; i < 5; i++) {
si (token.IsCancellationRequested)
56
Machine Translated by Google
Capítulo 3
{
WriteLine("La primera tarea ha sido cancelada.");
devolver;
}
Dormir (TimeSpan.FromSeconds (1));
}
WriteLine("La primera tarea se completó con éxito");
}
vacío estático AsyncOperation2 (token de Cancelación) {
intentar
WriteLine("Comenzando la segunda tarea");
para (int i = 0; i < 5; i++) {
token.ThrowIfCancellationRequested(); Dormir
(TimeSpan.FromSeconds (1));
}
WriteLine("La segunda tarea se completó con éxito");
} captura (Excepción Cancelada por Operación) {
WriteLine("La segunda tarea ha sido cancelada.");
}
}
vacío estático AsyncOperation3 (token de Cancelación) {
bool cancelacionFlag = false; token.Register(() =>
cancelacionFlag = verdadero); WriteLine("Comenzando la tercera tarea");
para (int i = 0; i < 5; i++) {
si (bandera de cancelación) {
WriteLine("La tercera tarea ha sido cancelada.");
devolver;
}
Dormir (TimeSpan.FromSeconds (1));
}
WriteLine("La tercera tarea se completó con éxito");
}
57
Machine Translated by Google
Uso de un grupo de subprocesos
4. Agregue el siguiente fragmento de código dentro del método principal :
usando (var cts = new CancellationTokenSource()) {
CancellationToken token = cts.Token;
ThreadPool.QueueUserWorkItem(_ => AsyncOperation1(token)); Dormir
(TimeSpan.FromSeconds (2)); cts.Cancelar();
usando (var cts = new CancellationTokenSource()) {
CancellationToken token = cts.Token;
ThreadPool.QueueUserWorkItem(_ => AsyncOperation2(token)); Dormir
(TimeSpan.FromSeconds (2)); cts.Cancelar();
usando (var cts = new CancellationTokenSource()) {
CancellationToken token = cts.Token;
ThreadPool.QueueUserWorkItem(_ => AsyncOperation3(token)); Dormir
(TimeSpan.FromSeconds (2)); cts.Cancelar();
Dormir (TimeSpan.FromSeconds (2));
5. Ejecute el programa.
Cómo funciona...
Aquí presentamos las construcciones CancellationTokenSource y CancellationToken .
Aparecieron en .NET 4.0 y ahora son el estándar de facto para implementar procesos de cancelación de
operaciones asincrónicas. Dado que el grupo de subprocesos existe desde hace mucho tiempo, no tiene una
API especial para tokens de cancelación; sin embargo, todavía se pueden utilizar.
En este programa, vemos tres formas de organizar un proceso de cancelación. El primero es solo para sondear
y verificar la propiedad CancellationToken.IsCancellationRequested . Si se establece en verdadero, esto significa
que nuestra operación se está cancelando y debemos abandonar la operación.
La segunda forma es lanzar una excepción OperationCancelledException . Esto nos permite controlar el
proceso de cancelación no desde dentro de la operación, que se está cancelando, sino desde el código en el
exterior.
La última opción es registrar una devolución de llamada que se llamará en un grupo de subprocesos cuando se
cancele una operación. Esto nos permitirá encadenar la lógica de cancelación en otra operación asíncrona.
58
Machine Translated by Google
Capítulo 3
Usar un identificador de espera y un tiempo de espera con un
grupo de subprocesos
Esta receta describirá cómo implementar un tiempo de espera para las operaciones del grupo de subprocesos y cómo esperar
correctamente en un grupo de subprocesos.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter3\Recipe5.
Cómo hacerlo...
Para obtener información sobre cómo implementar un tiempo de espera y cómo esperar correctamente en un grupo de subprocesos,
realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static void RunOperations(TimeSpan workerOperationTimeout) {
usando (var evt = new ManualResetEvent(false)) usando (var cts = new
CancellationTokenSource()) {
WriteLine("Registrando operación de tiempo de espera..."); var trabajador =
ThreadPool.RegisterWaitForSingleObject(evt
, (estado, está agotado) => WorkerOperationWait(cts,
está agotado)
, nulo
, workOperationTimeout , verdadero);
WriteLine("Iniciando una operación de ejecución prolongada...");
ThreadPool.QueueUserWorkItem(_ => WorkerOperation(cts.Token, evt));
Dormir(workerOperationTimeout.Add(TimeSpan.FromSeconds(2)));
59
Machine Translated by Google
Uso de un grupo de subprocesos
trabajador.Desregistrar(evt);
}
}
WorkerOperation vacío estático (token de CancelaciónToken,
ManualResetEvent evt) {
para(int i = 0; i < 6; i++) {
si (token.IsCancellationRequested) {
devolver;
}
Dormir (TimeSpan.FromSeconds (1));
} evt.Set();
}
static void WorkerOperationWait(CancellationTokenSource cts, bool isTimedOut) {
si (se ha agotado el tiempo de espera)
cts.Cancelar();
WriteLine("Se agotó el tiempo de espera de la operación del trabajador y se canceló.");
} demás
{
WriteLine("Operación del trabajador exitosa.");
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
EjecutarOperaciones(TimeSpan.FromSeconds(5));
EjecutarOperaciones(TimeSpan.FromSeconds(7));
5. Ejecute el programa.
Cómo funciona...
Un grupo de subprocesos tiene otro método útil: ThreadPool.RegisterWaitForSingleObject.
Este método nos permite poner en cola una devolución de llamada en un grupo de subprocesos, y esta devolución de llamada se
ejecutará cuando se señale el identificador de espera proporcionado o se agote el tiempo de espera. Esto nos permite
implementar un tiempo de espera para las operaciones del grupo de subprocesos.
60
Machine Translated by Google
Capítulo 3
Primero, registramos la operación asincrónica de manejo de tiempo de espera. Se llamará cuando ocurra uno de los
siguientes eventos: al recibir una señal del objeto ManualResetEvent , que establece la operación del trabajador
cuando se completa con éxito, o cuando se agota el tiempo de espera antes de que se complete la primera
operación. Si esto sucede, usamos CancellationToken para cancelar la primera operación.
Luego, ponemos en cola una operación de trabajador de ejecución prolongada en un grupo de subprocesos. Se ejecuta
durante 6 segundos y luego establece una construcción de señalización ManualResetEvent , en caso de que se complete
correctamente. En caso contrario, si se solicita la cancelación, simplemente se abandona la operación.
Finalmente, si proporcionamos un tiempo de espera de 5 segundos para la operación, eso no sería suficiente. Esto se
debe a que la operación tarda 6 segundos en completarse y tendríamos que cancelar esta operación. Entonces, si
proporcionamos un tiempo de espera de 7 segundos, que es aceptable, la operación se completa con éxito.
Hay más…
Esto es muy útil cuando tiene una gran cantidad de subprocesos que deben esperar en el estado bloqueado para que
alguna construcción de evento multiproceso señale. En lugar de bloquear todos estos subprocesos, podemos utilizar la
infraestructura del grupo de subprocesos. Nos permitirá liberar estos hilos hasta que se establezca el evento. Este es
un escenario muy importante para las aplicaciones de servidor, que requieren escalabilidad y rendimiento.
usando un temporizador
Esta receta describirá cómo usar un objeto System.Threading.Timer para crear operaciones asincrónicas
llamadas periódicamente en un grupo de subprocesos.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter3\Recipe6.
Cómo hacerlo...
Para aprender a crear operaciones asincrónicas llamadas periódicamente en un grupo de subprocesos, realice los siguientes
pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
System.Threading; usando System.Console
estático; usando System.Threading.Thread estático;
61
Machine Translated by Google
Uso de un grupo de subprocesos
3. Agregue el siguiente fragmento de código debajo del método principal :
Temporizador estático _timer;
static void TimerOperation(DateTime start) {
TimeSpan transcurrido = DateTime.Now inicio;
WriteLine($"{elapsed.Seconds} segundos desde el {inicio}". +
$"Id. de subproceso del grupo de subprocesos
del temporizador: {CurrentThread.ManagedThreadId}"); }
4. Agregue el siguiente fragmento de código dentro del método principal :
WriteLine("Presione 'Enter' para detener el temporizador..."); Fecha y hora de
inicio = Fecha y hora. Ahora; _timer = new
Timer(_ => TimerOperation(inicio), nulo
, Lapso de tiempo.FromSeconds(1)
, IntervaloDeTiempo.FromSeconds(2));
intentar
Dormir (TimeSpan.FromSeconds (6));
_timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(4));
LeerLínea();
} finalmente
{
_timer.Dispose();
}
5. Ejecute el programa.
Cómo funciona...
Primero, creamos una nueva instancia de Timer . El primer parámetro es una expresión lambda que se ejecutará
en un grupo de subprocesos. Llamamos al método TimerOperation y le proporcionamos una fecha de inicio. No
usamos el objeto de estado de usuario , por lo que el segundo parámetro es nulo; luego, especificamos cuándo
vamos a ejecutar TimerOperation por primera vez y cuál será el período entre llamadas. Entonces, el primer
valor en realidad significa que comenzamos la primera operación en 1 segundo y luego ejecutamos cada una de
ellas en 2 segundos.
Después de esto, esperamos 6 segundos y cambiamos nuestro temporizador. Iniciamos TimerOperation en
un segundo después de llamar al método _timer.Change y luego ejecutamos cada uno de ellos durante 4 segundos.
62
Machine Translated by Google
Capítulo 3
¡Un temporizador podría ser más complejo que esto!
Es posible usar un temporizador de formas más complicadas. Por ejemplo,
podemos ejecutar la operación del temporizador solo una vez, proporcionando un
parámetro de período del temporizador con el valor Timeout.Infinite. Luego,
dentro de la operación asíncrona del temporizador, podemos establecer la próxima
vez que se ejecutará la operación del temporizador, según alguna lógica personalizada.
Por último, esperamos a que se pulse la tecla Enter y finalice la aplicación. Mientras se ejecuta, podemos ver el
tiempo transcurrido desde que se inició el programa.
Uso del componente BackgroundWorker
Esta receta describe otro enfoque de la programación asíncrona a través de un ejemplo de un componente
BackgroundWorker . Con la ayuda de este objeto, podemos organizar nuestro código asíncrono como un
conjunto de eventos y controladores de eventos. Aprenderá a utilizar este componente para la programación
asíncrona.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter3\Recipe7.
Cómo hacerlo...
Para aprender a usar el componente BackgroundWorker , realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando System.ComponentModel;
usando System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static void Worker_DoWork(objeto remitente, DoWorkEventArgs e) {
WriteLine($"Id. de subproceso del grupo de subprocesos
DoWork: {CurrentThread.ManagedThreadId}");
var bw = (BackgroundWorker) remitente; para (int i =
1; i <= 100; i++)
63
Machine Translated by Google
Uso de un grupo de subprocesos
{
if (bw.CancelaciónPendiente) {
e.Cancelar = verdadero;
devolver;
} si (i%10 == 0) {
bw.ReportProgress(i);
}
Dormir (Intervalo de tiempo. FromSeconds (0.1));
}
e.Resultado = 42;
}
static void Worker_ProgressChanged (remitente del objeto,
ProgressChangedEventArgs e) {
WriteLine($"{e.ProgressPercentage}% completado". +
$"Id. de subproceso del grupo de subprocesos de progreso:
{CurrentThread.ManagedThreadId}"); }
static void Worker_Completed(objeto remitente,
RunWorkerCompletedEventArgs e) {
WriteLine($"Id. de subproceso del grupo de subprocesos completado:
{CurrentThread.ManagedThreadId}"); if (e.Error != null) {
WriteLine($"Excepción {e.Error.Message} ha ocurrido.");
} else if (e.Cancelado) {
WriteLine($"La operación ha sido cancelada");
} demás
{
WriteLine($"La respuesta es: {e.Result}");
}
}
64
Machine Translated by Google
Capítulo 3
4. Agregue el siguiente fragmento de código dentro del método principal :
var bw = new BackgroundWorker();
bw.WorkerReportsProgress = verdadero;
bw.WorkerSupportsCancellation = true;
bw.DoWork += Worker_DoWork;
bw.ProgressChanged += Worker_ProgressChanged;
bw.RunWorkerCompleted += Worker_Completed;
bw.RunWorkerAsync();
WriteLine("Presione C para cancelar el trabajo");
hacer
{
if (ReadKey(true).KeyChar == 'C') {
bw.CancelAsync();
}
} mientras (bw.IsBusy);
5. Ejecute el programa.
Cómo funciona...
Cuando se inicia el programa, creamos una instancia de un componente BackgroundWorker . Declaramos
explícitamente que queremos que nuestro trabajador en segundo plano admita la cancelación y las notificaciones
sobre el progreso de la operación.
Ahora, aquí es donde entra en juego la parte más interesante. En lugar de manipular con un grupo de
subprocesos y delegados, usamos otro modismo de C# llamado events. Un evento representa una fuente de
notificaciones y un número de suscriptores listos para reaccionar cuando llega una notificación.
En nuestro caso, declaramos que nos suscribiremos a tres eventos y, cuando ocurran, llamaremos a los
controladores de eventos correspondientes. Estos son métodos con una firma especialmente definida que se
llamará cuando un evento notifique a sus suscriptores.
Por lo tanto, en lugar de organizar una API asíncrona en un par de métodos Begin/End , es posible
simplemente iniciar una operación asíncrona y luego suscribirse a diferentes eventos que podrían ocurrir
mientras se ejecuta esta operación. Este enfoque se denomina Patrón asíncrono basado en eventos (EAP).
Históricamente, fue el segundo intento de estructurar programas asincrónicos y, ahora, se recomienda usar TPL
en su lugar, que se describirá en el Capítulo 4, Uso de la biblioteca paralela de tareas.
sesenta y cinco
Machine Translated by Google
Uso de un grupo de subprocesos
Entonces, nos suscribimos a tres eventos. El primero de ellos es el evento DoWork . Se llamará a un controlador de este evento cuando un objeto
de trabajo en segundo plano inicie una operación asincrónica con el método RunWorkerAsync . El controlador de eventos se ejecutará en un grupo de
subprocesos, y este es el punto operativo principal donde se cancela el trabajo si se solicita la cancelación y donde brindamos información sobre el
progreso de la operación. Por último, cuando obtenemos el resultado, lo configuramos en argumentos de evento y, a continuación, se llama al controlador
de eventos RunWorkerCompleted . Dentro de este método, descubrimos si nuestra operación tuvo éxito, hubo algunos errores o se canceló.
Además de esto, un componente de BackgroundWorker en realidad está diseñado para usarse en aplicaciones de Windows Forms (WPF). Su
implementación hace posible trabajar con controles de interfaz de usuario directamente desde el código del controlador de eventos de un trabajador en
segundo plano, lo cual es muy cómodo en comparación con la interacción de subprocesos de trabajo en un grupo de subprocesos con controles de
interfaz de usuario.
66
Machine Translated by Google
Uso de la tarea
4
Biblioteca paralela
En este capítulo, nos sumergiremos en un nuevo paradigma de programación asincrónica, la biblioteca paralela de tareas.
Aprenderás las siguientes recetas:
f Creación de una tarea
f Realización de operaciones básicas con una tarea f
Combinación de tareas f Conversión
del patrón APM en tareas
f Convertir el patrón EAP en tareas f Implementar
una opción de cancelación
f Manejo de excepciones en tareas
f Ejecutar tareas en paralelo f Ajustar
la ejecución de tareas con TaskScheduler
Introducción
En los capítulos anteriores, aprendió qué es un subproceso, cómo usar los subprocesos y por qué necesitamos un
grupo de subprocesos. El uso de un grupo de subprocesos nos permite ahorrar recursos del sistema operativo a costa
de reducir un grado de paralelismo. Podemos pensar en un grupo de subprocesos como una capa de abstracción que
oculta los detalles del uso de subprocesos de un programador, lo que nos permite concentrarnos en la lógica de un
programa en lugar de en los problemas de subprocesos.
67
Machine Translated by Google
Uso de la biblioteca paralela de tareas
Sin embargo, usar un grupo de subprocesos también es complicado. No hay una manera fácil de obtener un resultado
de un subproceso de trabajo de grupo de subprocesos. Necesitamos implementar nuestra propia forma de
recuperar un resultado y, en caso de una excepción, debemos propagarlo correctamente al hilo original. Además de
esto, no existe una manera fácil de crear un conjunto de acciones asincrónicas dependientes, donde una acción
se ejecuta después de que otra termina su trabajo.
Hubo varios intentos de solucionar estos problemas, lo que resultó en la creación del modelo de programación
asíncrona y el patrón asíncrono basado en eventos, mencionados en el Capítulo 3, Uso de un grupo de subprocesos.
Estos patrones facilitaron la obtención de resultados e hicieron un buen trabajo al propagar excepciones, pero la
combinación de acciones asincrónicas aun así requirió mucho trabajo y resultó en una gran cantidad de código.
Para resolver todos estos problemas, se introdujo una nueva API para operaciones asíncronas en .Net Framework
4.0. Se llamó Task Parallel Library (TPL). Se modificó ligeramente en .Net Framework 4.5 y, para que quede claro,
trabajaremos con la última versión de TPL utilizando la versión 4.6 de .Net Framework en nuestros proyectos. TPL
se puede considerar como una capa de abstracción más sobre un grupo de subprocesos, ocultando el código de
nivel inferior que funcionará con el grupo de subprocesos de un programador y proporcionando una API más
conveniente y detallada.
El concepto central de TPL es una tarea. Una tarea representa una operación asíncrona que se puede ejecutar de
varias formas, utilizando o no un subproceso independiente. Veremos todas las posibilidades en detalle en este
capítulo.
De forma predeterminada, un programador no sabe cómo se ejecuta exactamente una tarea.
TPL eleva el nivel de abstracción al ocultar al usuario los detalles de implementación de
la tarea. Desafortunadamente, en algunos casos, esto podría provocar errores
misteriosos, como que la aplicación se cuelgue al intentar obtener un resultado de la
tarea. Este capítulo lo ayudará a comprender la mecánica bajo el capó de TPL y cómo
evitar usarlo de manera inapropiada.
Una tarea se puede combinar con otras tareas en diferentes variaciones. Por ejemplo, podemos iniciar varias tareas
simultáneamente, esperar a que se completen todas y luego ejecutar una tarea que realizará algunos cálculos sobre
los resultados de todas las tareas anteriores. Las API convenientes para la combinación de tareas son una de las
ventajas clave de TPL en comparación con los patrones anteriores.
También hay varias formas de tratar las excepciones resultantes de las tareas. Dado que una tarea puede
constar de varias otras tareas y, a su vez, también tienen sus tareas secundarias, existe el concepto de
AggregateException. Este tipo de excepción contiene todas las excepciones de las tareas subyacentes en su
interior, lo que nos permite manejarlas por separado.
Y, por último, pero no menos importante, C# tiene soporte integrado para TPL desde la versión 5.0, lo que nos
permite trabajar con tareas de una manera muy fluida y cómoda utilizando las nuevas palabras clave await y async .
Trataremos este tema en el Capítulo 5, Uso de C# 6.0.
68
Machine Translated by Google
Capítulo 4
En este capítulo, aprenderá a usar TPL para ejecutar operaciones asincrónicas. Aprenderemos qué es una
tarea, cubriremos diferentes formas de crear tareas y aprenderemos cómo combinar tareas. También
discutiremos cómo convertir patrones heredados de APM y EAP para usar tareas, cómo manejar las
excepciones correctamente, cómo cancelar tareas y cómo trabajar con varias tareas que se ejecutan
simultáneamente. Además, descubriremos cómo manejar correctamente las tareas en las aplicaciones de la
GUI de Windows.
Creando una tarea
Esta receta muestra el concepto básico de lo que es una tarea. Aprenderá a crear y ejecutar tareas.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter4\Recipe1.
Cómo hacerlo...
Para crear y ejecutar una tarea, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
Esta vez, asegúrese de estar usando .Net Framework 4.5 o
superior para cada proyecto.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static void TaskMethod(nombre de cadena) {
WriteLine($"Task {name} is running on a thread id" +
$"{CurrentThread.ManagedThreadId}. Es el subproceso del grupo de subprocesos:
" +
$"{SubprocesoActual.IsThreadPoolThread}");
}
69
Machine Translated by Google
Uso de la biblioteca paralela de tareas
4. Agregue el siguiente fragmento de código dentro del método principal :
var t1 = nueva Tarea(() => TaskMethod("Tarea 1")); var t2 = nueva Tarea(()
=> TaskMethod("Tarea 2"));
t2.Inicio();
t1.Inicio();
Task.Run(() => TaskMethod("Tarea 3"));
Task.Factory.StartNew(() => TaskMethod("Tarea 4")); Task.Factory.StartNew(()
=> TaskMethod("Tarea 5"), TaskCreationOptions.LongRunning); Dormir
(TimeSpan.FromSeconds (1));
5. Ejecute el programa.
Cómo funciona...
Cuando el programa se ejecuta, crea dos tareas con su constructor. Pasamos la expresión lambda como
el delegado de Acción ; esto nos permite proporcionar un parámetro de cadena a TaskMethod. Luego,
ejecutamos estas tareas usando el método Start .
Tenga en cuenta que hasta que llamemos al método Start en estas tareas, no
comenzarán a ejecutarse. Es muy fácil olvidarse de comenzar realmente la tarea.
Luego, ejecutamos dos tareas más usando los métodos Task.Run y Task.Factory.StartNew .
La diferencia es que ambas tareas creadas comienzan a funcionar inmediatamente, por lo que no es necesario
llamar explícitamente al método Start en las tareas. Todas las tareas, numeradas de la Tarea 1 a la Tarea 4, se
colocan en subprocesos de trabajo del grupo de subprocesos y se ejecutan en un orden no especificado. Si
ejecuta el programa varias veces, encontrará que el orden de ejecución de la tarea no está definido.
El método Task.Run es solo un acceso directo a Task.Factory.StartNew, pero el último método tiene opciones
adicionales. En general, use el método anterior a menos que necesite hacer algo especial, como en el caso de
la Tarea 5. Marcamos esta tarea como de ejecución prolongada y, como resultado, esta tarea se ejecutará en
un hilo separado que no usa un grupo de subprocesos. Sin embargo, este comportamiento podría cambiar,
según el programador de tareas actual que ejecuta la tarea.
Aprenderá qué es un programador de tareas en la última receta de este capítulo.
Realizar operaciones básicas con una tarea
Esta receta describirá cómo obtener el valor del resultado de una tarea. Pasaremos por varios escenarios para
comprender la diferencia entre ejecutar una tarea en un grupo de subprocesos o en un subproceso principal.
70
Machine Translated by Google
Capítulo 4
preparándose
Para comenzar esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter4\Recipe2.
Cómo hacerlo...
Para realizar operaciones básicas con una tarea, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
Tarea estática<int> CreateTask(nombre de cadena) {
return new Task<int>(() => TaskMethod(nombre));
}
static int TaskMethod(nombre de cadena) {
WriteLine($"Task {name} is running on a thread id" +
$"{CurrentThread.ManagedThreadId}. Es el subproceso del grupo de subprocesos: " +
$"{SubprocesoActual.IsThreadPoolThread}"); Dormir
(TimeSpan.FromSeconds (2));
volver 42;
}
4. Agregue el siguiente fragmento de código dentro del método principal :
TaskMethod("Tarea principal del subproceso");
Tarea<int> tarea = CreateTask("Tarea 1"); tarea.Inicio(); int
resultado =
tarea.Resultado; WriteLine($"El resultado
es: {resultado}");
tarea = CreateTask("Tarea 2"); tarea.Ejecutar
sincrónicamente(); resultado =
tarea.Resultado; WriteLine($"El
resultado es: {resultado}");
71
Machine Translated by Google
Uso de la biblioteca paralela de tareas
tarea = CreateTask("Tarea 3");
WriteLine(tarea.Estado); tarea.Inicio();
while (!tarea.IsCompleted) {
WriteLine(tarea.Estado); Dormir
(TimeSpan.FromSeconds (0.5));
}
WriteLine(tarea.Estado); resultado =
tarea.Resultado;
WriteLine($"El resultado es: {resultado}");
5. Ejecute el programa.
Cómo funciona...
Al principio, ejecutamos TaskMethod sin incluirlo en una tarea. Como resultado, se ejecuta de forma
síncrona, brindándonos información sobre el hilo principal. Obviamente, no es un hilo de grupo de
subprocesos.
Luego, ejecutamos la Tarea 1, iniciándola con el método Start y esperando el resultado. Esta tarea se colocará
en un grupo de subprocesos y el subproceso principal esperará y se bloqueará hasta que la tarea regrese.
Hacemos lo mismo con la Tarea 2, excepto que la ejecutamos usando el método RunSynchronously() .
Esta tarea se ejecutará en el subproceso principal y obtendremos exactamente el mismo resultado que en el
primer caso cuando llamamos a TaskMethod sincrónicamente. Esta es una optimización muy útil que nos permite
evitar el uso de grupos de subprocesos para operaciones de muy corta duración.
Ejecutamos la Tarea 3 de la misma manera que hicimos con la Tarea 1, pero en lugar de bloquear el hilo
principal, simplemente giramos, imprimiendo el estado de la tarea hasta que se completa. Esto muestra varios
estados de tareas, que son Creado, Ejecutando y RanToCompletion, respectivamente.
Combinando tareas
Esta receta le mostrará cómo configurar tareas que dependen unas de otras. Aprenderemos cómo crear una
tarea que se ejecutará después de que se complete la tarea principal. Además, descubriremos una forma de
ahorrar el uso de subprocesos para tareas de muy corta duración.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter4\Recipe3.
72
Machine Translated by Google
Capítulo 4
Cómo hacerlo...
Para combinar tareas, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static int TaskMethod(nombre de cadena, int segundos) {
Línea de escritura(
$"La tarea {nombre} se está ejecutando en una identificación de subproceso" +
$"{CurrentThread.ManagedThreadId}. Es el subproceso del grupo de subprocesos: " +
$"{SubprocesoActual.IsThreadPoolThread}");
Dormir (TimeSpan.FromSeconds (segundos)); volver 42 *
segundos;
}
4. Agregue el siguiente fragmento de código dentro del método principal :
var firstTask = new Task<int>(() => TaskMethod("Primera tarea", 3)); var secondTask = new Task<int>(() =>
TaskMethod("Segunda tarea", 2));
primeraTarea.ContinuarCon(
t => WriteLine(
$"La primera respuesta es {t.Result}. ID de subproceso " +
$"{CurrentThread.ManagedThreadId}, es el subproceso del grupo de subprocesos: " +
$"{CurrentThread.IsThreadPoolThread}"),
TaskContinuationOptions.OnlyOnRanToCompletion);
primeraTarea.Inicio();
segundaTarea.Inicio();
Dormir (TimeSpan.FromSeconds (4));
Continuación de la tarea = secondTask.ContinueWith(
t => WriteLine(
$"La segunda respuesta es {t.Result}. Id. de subproceso" +
73
Machine Translated by Google
Uso de la biblioteca paralela de tareas
$"{CurrentThread.ManagedThreadId}, es el subproceso del grupo de subprocesos: " +
$"{CurrentThread.IsThreadPoolThread}"),
TaskContinuationOptions.OnlyOnRanToCompletion
| TaskContinuationOptions.ExecuteSynchronously);
continuación.GetAwaiter().OnCompleted(
() => EscribirLínea(
$"¡Tarea de continuación completada! Id. de subproceso" +
$"{CurrentThread.ManagedThreadId}, es el subproceso del grupo de subprocesos: " +
$"{SubprocesoActual.IsThreadPoolThread}"));
Dormir (TimeSpan.FromSeconds (2)); Línea de
escritura();
primeraTarea = nueva Tarea<int>(() => {
var innerTask = Task.Factory.StartNew(() => TaskMethod("Second Task",
5),TaskCreationOptions.AttachedToParent);
tareainterna.ContinueWith(t => TaskMethod("Tercera tarea", 2),
TaskContinuationOptions.AttachedToParent);
return TaskMethod("Primera tarea", 2); });
primeraTarea.Inicio();
while (!primeraTarea.IsCompleted) {
WriteLine(primeraTarea.Estado); Dormir
(TimeSpan.FromSeconds (0.5));
}
WriteLine(primeraTarea.Estado);
Dormir (TimeSpan.FromSeconds (10));
5. Ejecute el programa.
74
Machine Translated by Google
Capítulo 4
Cómo funciona...
Cuando se inicia el programa principal, creamos dos tareas y, para la primera, configuramos una
continuación (un bloque de código que se ejecuta después de que se completa la tarea anterior). Luego,
comenzamos ambas tareas y esperamos 4 segundos, que es suficiente para que ambas tareas se
completen. Luego, ejecutamos otra continuación de la segunda tarea e intentamos ejecutarla sincrónicamente
especificando una opción TaskContinuationOptions.ExecuteSynchronously . Esta es una técnica
útil cuando la continuación es muy breve y será más rápido ejecutarla en el subproceso principal que
ponerla en un grupo de subprocesos. Podemos lograr esto porque la segunda tarea se completa en ese
momento. Si comentamos el método Thread.Sleep de 4 segundos , veremos que este código se colocará
en un grupo de subprocesos porque aún no tenemos el resultado de la tarea antecedente.
Finalmente, definimos una continuación para la continuación anterior, pero de una manera ligeramente
diferente, usando los nuevos métodos GetAwaiter y OnCompleted . Estos métodos están pensados
para usarse junto con la mecánica asincrónica del lenguaje C#. Cubriremos este tema más adelante
en el Capítulo 5, Uso de C# 6.0.
La última parte de la demostración trata sobre las relaciones de tareas padrehijo. Creamos una
nueva tarea y, mientras ejecutamos esta tarea, ejecutamos una llamada tarea secundaria
proporcionando una opción TaskCreationOptions.AttachedToParent .
¡La tarea secundaria debe crearse mientras se ejecuta una tarea principal para que
se adjunte a la principal correctamente!
Esto significa que la tarea principal no estará completa hasta que todas las tareas secundarias
terminen su trabajo. También podemos ejecutar continuaciones en aquellas tareas
secundarias que proporcionan una opción TaskContinuationOptions.AttachedToParent . Estas tareas de
continuación también afectarán a la tarea principal y no se completará hasta que finalice la última tarea secundaria.
Convertir el patrón APM en tareas
En esta receta, veremos cómo convertir una API de APM anticuada en una tarea. Hay ejemplos de
diferentes situaciones que pueden tener lugar en el proceso de conversión.
preparándose
Para comenzar esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter4\Recipe4.
75
Machine Translated by Google
Uso de la biblioteca paralela de tareas
Cómo hacerlo...
Para convertir el patrón APM en tareas, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
delegado string AsynchronousTask(string threadName); cadena de delegado
IncompatibleAsynchronousTask(out int threadId);
Devolución de llamada vacía estática (IAsyncResult ar) {
WriteLine("Iniciando una devolución de llamada...");
WriteLine($"Estado pasado a callbak: {ar.AsyncState}"); WriteLine($"Es el subproceso del
grupo de subprocesos:
{CurrentThread.IsThreadPoolThread}");
WriteLine($"Id. de subproceso del trabajador del grupo de subprocesos:
{CurrentThread.ManagedThreadId}"); }
prueba de cadena estática (string threadName) {
WriteLine("Iniciando..."); WriteLine($"Es
el subproceso del grupo de subprocesos:
{CurrentThread.IsThreadPoolThread}"); Dormir
(TimeSpan.FromSeconds (2));
SubprocesoActual.Nombre = subprocesoNombre;
return $"Nombre del subproceso: {CurrentThread.Name}";
}
Prueba de cadena estática (out int threadId) {
WriteLine("Iniciando..."); WriteLine($"Es
el subproceso del grupo de subprocesos:
{CurrentThread.IsThreadPoolThread}"); Dormir
(TimeSpan.FromSeconds (2)); threadId =
CurrentThread.ManagedThreadId; return $"El id. del subproceso
del trabajador del grupo de subprocesos era: {threadId}";
}
76
Machine Translated by Google
Capítulo 4
4. Agregue el siguiente fragmento de código dentro del método principal :
int threadId;
AsynchronousTask d = Prueba;
IncompatibleAsynchronousTask e = Prueba;
WriteLine("Opción 1"); Task<string>
task = Task<string>.Factory.FromAsync( d.BeginInvoke("AsyncTaskThread", Callback,
"una llamada asíncrona delegada"), d.EndInvoke);
tarea.ContinueWith(t => WriteLine(
$"La devolución de llamada ha finalizado, ¡ahora se está ejecutando una continuación!
Resultado: {t.Result}"));
while (!tarea.IsCompleted) {
WriteLine(tarea.Estado); Dormir
(TimeSpan.FromSeconds (0.5));
}
WriteLine(tarea.Estado); Dormir
(TimeSpan.FromSeconds (1));
Línea de escritura("" );
Línea de escritura();
WriteLine("Opción 2");
tarea = Tarea<cadena>.Factory.FromAsync(
d.BeginInvoke, d.EndInvoke, "AsyncTaskThread",
"una llamada asíncrona de delegado");
tarea.ContinueWith(t => WriteLine(
$"La tarea se completó, ¡ahora se está ejecutando una continuación! Resultado: {t.Result}"));
while (!tarea.IsCompleted)
{
WriteLine(tarea.Estado); Dormir
(TimeSpan.FromSeconds (0.5));
}
WriteLine(tarea.Estado); Dormir
(TimeSpan.FromSeconds (1));
Línea de escritura("" );
Línea de escritura();
77
Machine Translated by Google
Uso de la biblioteca paralela de tareas
WriteLine("Opción 3");
IAsyncResult ar = e.BeginInvoke(out threadId, Callback, "una llamada asíncrona delegada");
tarea = Task<string>.Factory.FromAsync(ar,
e.EndInvoke(out threadId, ar)); _ =>
tarea.ContinuarCon(t =>
Línea de escritura(
$"La tarea se completó, ¡ahora se está ejecutando una continuación!" +
$"Resultado: {t.Result}, ThreadId: {threadId}"));
while (!tarea.IsCompleted) {
WriteLine(tarea.Estado); Dormir
(TimeSpan.FromSeconds (0.5));
}
WriteLine(tarea.Estado);
Dormir (TimeSpan.FromSeconds (1));
5. Ejecute el programa.
Cómo funciona...
Aquí, definimos dos tipos de delegados; uno de ellos usa el parámetro out y, por lo tanto, es incompatible con la
API TPL estándar para convertir el patrón APM en tareas. Entonces, tenemos tres ejemplos de tal conversión.
El punto clave para convertir APM a TPL es el método Task<T>.Factory.FromAsync , donde T es el tipo de
resultado de la operación asíncrona. Hay varias sobrecargas de este método; en el primer caso, pasamos
IAsyncResult y Func<IAsyncResult, string>, que es un método que acepta la implementación de IAsyncResult y
devuelve una cadena.
Dado que el primer tipo de delegado proporciona EndMethod, que es compatible con esta firma, no tenemos
problemas para convertir esta llamada asíncrona de delegado en una tarea.
En el segundo ejemplo, hacemos casi lo mismo, pero usamos una sobrecarga del método FromAsync
diferente , que no permite especificar una devolución de llamada que se ejecutará después de que se
complete la llamada del delegado asíncrono. Podemos reemplazar esto con una continuación, pero si la
devolución de llamada es importante, podemos usar el primer ejemplo.
El último ejemplo muestra un pequeño truco. Esta vez, EndMethod del delegado
IncompatibleAsynchronousTask usa el parámetro out y no es compatible con ninguna sobrecarga del método
FromAsync . Sin embargo, es muy fácil envolver la llamada EndMethod en una expresión lambda que sea
adecuada para la fábrica de tareas.
78
Machine Translated by Google
Capítulo 4
Para ver qué está pasando con la tarea subyacente, estamos imprimiendo su estado mientras
esperamos el resultado de la operación asíncrona. Vemos que el estado de la primera tarea es
WaitingForActivation, lo que significa que la infraestructura TPL aún no ha iniciado la tarea.
Convertir el patrón EAP en tareas
Esta receta describirá cómo traducir operaciones asincrónicas basadas en eventos en tareas. En esta receta,
encontrará un patrón sólido que es adecuado para cada API asincrónica basada en eventos en la biblioteca de
clases de .NET Framework.
preparándose
Para comenzar esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos. El código fuente de
esta receta se puede encontrar en BookSamples\Chapter4\Recipe5.
Cómo hacerlo...
Para convertir el patrón EAP en tareas, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.ComponentModel;
utilizando System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static int TaskMethod(nombre de cadena, int segundos) {
Línea de escritura(
$"La tarea {nombre} se está ejecutando en una identificación de subproceso" +
$"{CurrentThread.ManagedThreadId}. Es el subproceso del grupo de subprocesos: " +
$"{SubprocesoActual.IsThreadPoolThread}");
Dormir (TimeSpan.FromSeconds (segundos)); volver 42 *
segundos;
}
79
Machine Translated by Google
Uso de la biblioteca paralela de tareas
4. Agregue el siguiente fragmento de código dentro del método principal :
var tcs = new TaskCompletionSource<int>();
var trabajador = new BackgroundWorker(); trabajador.DoWork
+= (remitente, eventArgs) => {
eventArgs.Result = TaskMethod("Trabajador en segundo plano", 5); };
trabajador.RunWorkerCompleted += (remitente, eventArgs) => {
if (eventArgs.Error != null) {
tcs.SetException(eventArgs.Error);
} más si (eventArgs.Cancelled) {
tcs.SetCanceled();
}
demás
{
tcs.SetResult((int)eventArgs.Result);
} };
trabajador.RunWorkerAsync();
int resultado = tcs.Tarea.Resultado;
WriteLine($"El resultado es: {resultado}");
5. Ejecute el programa.
Cómo funciona...
Este es un ejemplo muy simple y elegante de convertir patrones EAP en tareas. El punto clave es usar el tipo
TaskCompletionSource<T> , donde T es un tipo de resultado de operación asincrónica.
También es importante no olvidar incluir la llamada al método tcs.SetResult en el bloque try/catch para
garantizar que la información del error siempre se establezca en el objeto de origen de finalización de la
tarea. También es posible utilizar el método TrySetResult en lugar de SetResult para asegurarse de que el
resultado se haya establecido correctamente.
80
Machine Translated by Google
Capítulo 4
Implementación de una opción de cancelación
Esta receta trata sobre la implementación del proceso de cancelación para operaciones asincrónicas
basadas en tareas. Aprenderá cómo usar el token de cancelación correctamente para las tareas y cómo averiguar
si una tarea se canceló antes de que se ejecutara.
preparándose
Para comenzar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter4\Recipe6.
Cómo hacerlo...
Para implementar una opción de cancelación para operaciones asincrónicas basadas en tareas, realice los
siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading;
utilizando System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static int TaskMethod(nombre de cadena, int segundos,
Token de cancelación) {
WriteLine ($"La
tarea {nombre} se está ejecutando en una identificación de subproceso" +
$"{CurrentThread.ManagedThreadId}. Es el subproceso del grupo de subprocesos: " +
$"{SubprocesoActual.IsThreadPoolThread}");
para (int i = 0; i < segundos; i ++) {
Dormir (TimeSpan.FromSeconds (1)); si
(token.IsCancellationRequested) devuelve 1;
} devuelve 42*segundos;
}
81
Machine Translated by Google
Uso de la biblioteca paralela de tareas
4. Agregue el siguiente fragmento de código dentro del método principal :
var cts = new CancellationTokenSource(); var longTask =
new Task<int>(() => TaskMethod("Tarea 1", 10,
cts.Token), cts.Token); WriteLine(longTarea.Estado); cts.Cancelar();
WriteLine(longTarea.Estado);
WriteLine("La
primera tarea ha sido cancelada antes
de la ejecución");
cts = new CancellationTokenSource(); longTask =
new Task<int>(() => TaskMethod("Tarea 2",
10, cts.Token), cts.Token); tarealarga.Inicio(); para (int i = 0; i < 5; i++ ) {
Dormir (TimeSpan.FromSeconds (0.5));
WriteLine(longTarea.Estado);
} cts.Cancelar();
para (int i = 0; i < 5; i++) {
Dormir (TimeSpan.FromSeconds (0.5));
WriteLine(longTarea.Estado);
}
WriteLine($"Se completó una tarea con resultado {longTask.
Resultado}.");
5. Ejecute el programa.
Cómo funciona...
Este es otro ejemplo muy simple de cómo implementar la opción de cancelación para una tarea TPL. Ya está
familiarizado con el concepto de token de cancelación que discutimos en el Capítulo 3, Uso de un grupo de
subprocesos.
Primero, observemos de cerca el código de creación de longTask . Estamos proporcionando un token de
cancelación a la tarea subyacente una vez y luego al constructor de la tarea por segunda vez. ¿Por qué necesitamos
proporcionar este token dos veces?
La respuesta es que si cancelamos la tarea antes de que realmente se haya iniciado, su infraestructura TPL es
responsable de lidiar con la cancelación porque nuestro código no se ejecutará en absoluto. Sabemos que la
primera tarea fue cancelada al obtener su estado. Si intentamos llamar al método Start en esta tarea, obtendremos
InvalidOperationException.
82
Machine Translated by Google
Capítulo 4
Luego, nos ocupamos del proceso de cancelación desde nuestro propio código. Esto significa que ahora somos
totalmente responsables del proceso de cancelación y, después de que cancelamos la tarea, su estado seguía
siendo RanToCompletion porque, desde la perspectiva de TPL, la tarea terminó su trabajo con normalidad.
Es muy importante distinguir estas dos situaciones y entender la diferencia de responsabilidad en cada caso.
Manejo de excepciones en tareas
Esta receta describe el tema muy importante del manejo de excepciones en tareas asincrónicas.
Revisaremos diferentes aspectos de lo que sucede con las excepciones lanzadas desde las tareas y cómo llegar
a su información.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter4\Recipe7.
Cómo hacerlo...
Para manejar las excepciones en las tareas, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static int TaskMethod(nombre de cadena, int segundos) {
WriteLine ($"La
tarea {nombre} se está ejecutando en una identificación de subproceso" +
$"{CurrentThread.ManagedThreadId}. Es el subproceso del grupo de subprocesos: " +
$"{SubprocesoActual.IsThreadPoolThread}");
Dormir (TimeSpan.FromSeconds (segundos)); lanzar una
nueva excepción ("¡Boom!"); volver 42 *
segundos;
}
83
Machine Translated by Google
Uso de la biblioteca paralela de tareas
4. Agregue el siguiente fragmento de código dentro del método principal :
Tarea<int> tarea;
intentar
tarea = Tarea.Ejecutar(() => TaskMethod("Tarea 1", 2)); int resultado =
tarea.Resultado; WriteLine($"Resultado:
{resultado}");
} catch (excepción ex) {
WriteLine($"Excepción detectada: {ex}");
}
Línea de escritura("" );
Línea de escritura();
intentar
tarea = Tarea.Ejecutar(() => TaskMethod("Tarea 2", 2)); resultado int =
tarea.GetAwaiter().GetResult();
WriteLine($"Resultado: {resultado}");
} catch (excepción ex) {
WriteLine($"Excepción capturada: {ex}");
}
Línea de escritura("" );
Línea de escritura();
var t1 = nueva Tarea<int>(() => TaskMethod("Tarea 3", 3)); var t2 = nueva Tarea<int>(()
=> TaskMethod("Tarea 4", 2)); var complexTask = Task.WhenAll(t1, t2);
varExceptionHandler = complexTask.ContinueWith(t =>
WriteLine($"Excepción detectada: {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted
);
t1.Inicio();
t2.Inicio();
Dormir (TimeSpan.FromSeconds (5));
5. Ejecute el programa.
84
Machine Translated by Google
Capítulo 4
Cómo funciona...
Cuando se inicia el programa, creamos una tarea e intentamos obtener los resultados de la tarea de forma sincrónica.
La parte Get de la propiedad Result hace que el subproceso actual espere hasta la finalización de la tarea y propaga
la excepción al subproceso actual. En este caso, capturamos fácilmente la excepción en un bloque catch, pero
esta excepción es una excepción contenedora llamada AggregateException. En este caso, contiene solo una
excepción porque solo una tarea ha generado esta excepción y es posible obtener la excepción subyacente accediendo
a la propiedad InnerException .
El segundo ejemplo es prácticamente el mismo, pero para acceder al resultado de la tarea, usamos los
métodos GetAwaiter y GetResult . En este caso, no tenemos una excepción de contenedor porque la infraestructura
TPL lo desenvuelve. Tenemos una excepción original a la vez, lo cual es bastante cómodo si solo tenemos una tarea
subyacente.
El último ejemplo muestra la situación en la que tenemos dos excepciones de lanzamiento de tareas.
Para manejar las excepciones, ahora usamos una continuación, que se ejecuta solo en caso de que la tarea
antecedente finalice con una excepción. Este comportamiento se logra proporcionando una opción
TaskContinuationOptions.OnlyOnFaulted a una continuación. Como resultado, se imprime AggregateException y tenemos
dos excepciones internas de las dos tareas que contiene.
Hay más…
Como las tareas pueden estar conectadas de una manera muy diferente, la excepción AggregateException resultante puede
contener otras excepciones agregadas junto con las excepciones habituales.
Esas excepciones agregadas internas pueden contener otras excepciones agregadas dentro de ellas.
Para deshacernos de esos envoltorios, debemos usar el método Flatten de la excepción agregada raíz .
Devolverá una colección de todas las excepciones internas de cada excepción agregada secundaria en la jerarquía.
Ejecutar tareas en paralelo
Esta receta muestra cómo manejar muchas tareas asincrónicas que se ejecutan simultáneamente.
Aprenderá cómo ser notificado de manera efectiva cuando todas las tareas estén completas o cualquiera de las tareas en
ejecución tenga que terminar su trabajo.
preparándose
Para comenzar esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos. El código fuente de esta
receta se puede encontrar en BookSamples\Chapter4\Recipe8.
85
Machine Translated by Google
Uso de la biblioteca paralela de tareas
Cómo hacerlo...
Para ejecutar tareas en paralelo, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
usando System.Collections.Generic; utilizando
System.Threading.Tasks; usando System.Console
estático; usando System.Threading.Thread
estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static int TaskMethod(nombre de cadena, int segundos) {
Línea de escritura(
$"La tarea {nombre} se está ejecutando en una identificación de subproceso" +
$"{CurrentThread.ManagedThreadId}. Es el subproceso del grupo de subprocesos: " +
$"{SubprocesoActual.IsThreadPoolThread}");
Dormir (TimeSpan.FromSeconds (segundos)); volver 42 *
segundos;
}
4. Agregue el siguiente fragmento de código dentro del método principal :
var firstTask = new Task<int>(() => TaskMethod("Primera tarea", 3)); var secondTask = new Task<int>(() =>
TaskMethod("Segunda tarea", 2)); var whenAllTask =
Task.WhenAll(firstTask, secondTask);
whenAllTask.ContinueWith(t =>
WriteLine($"La primera respuesta es {t.Result[0]}, la segunda es
{t.Resultado[1]}"),
TaskContinuationOptions.OnlyOnRanToCompletion);
primeraTarea.Inicio();
segundaTarea.Inicio();
Dormir (TimeSpan.FromSeconds (4));
var tareas = new List<Tarea<int>>(); para (int i = 1; i < 4;
i++) {
contador int = i;
86
Machine Translated by Google
Capítulo 4
var tarea = nueva tarea<int>(() =>
TaskMethod($"Tarea {contador}", contador)); tareas.Add(tarea);
tarea.Inicio();
while (tareas.Cuenta > 0)
{
var completeTask = Task.WhenAny(tasks).Result; tareas. Eliminar (tarea
completada);
Línea de escritura
($"Se ha completado una tarea con resultado {completedTask.Result}."); }
Dormir (TimeSpan.FromSeconds (1));
5. Ejecute el programa.
Cómo funciona...
Cuando se inicia el programa, creamos dos tareas y luego, con la ayuda del método Task.WhenAll , creamos
una tercera tarea, que se completará después de que se completen todas las tareas iniciales. La tarea resultante
nos proporciona una matriz de respuesta, donde el primer elemento contiene el resultado de la primera tarea,
el segundo elemento contiene el segundo resultado, y así sucesivamente.
Luego, creamos otra lista de tareas y esperamos a que cualquiera de esas tareas se complete con el
método Task.WhenAny . Una vez que tenemos una tarea terminada, la eliminamos de la lista y continuamos
esperando a que se completen las otras tareas hasta que la lista esté vacía. Este método es útil para obtener el
progreso de finalización de la tarea o para usar un tiempo de espera mientras se ejecutan las tareas. Por
ejemplo, esperamos una serie de tareas y una de esas tareas es contar un tiempo de espera. Si esta tarea
se completa primero, simplemente cancelamos todas las demás tareas que aún no se completaron.
Ajustar la ejecución de tareas con
Programador de tareas
Esta receta describe otro aspecto muy importante del manejo de tareas, que es una forma adecuada de
trabajar con una interfaz de usuario desde el código asíncrono. Aprenderá qué es un programador de
tareas, por qué es tan importante, cómo puede dañar nuestra aplicación y cómo usarlo para evitar errores.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter4\Recipe9.
87
Machine Translated by Google
Uso de la biblioteca paralela de tareas
Cómo hacerlo...
Para modificar la ejecución de tareas con TaskScheduler, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación C# WPF . Esta vez, necesitaremos un
subproceso de interfaz de usuario con un bucle de mensajes, que no está disponible en las aplicaciones de consola.
2. En el archivo MainWindow.xaml , agregue el siguiente marcado dentro de un elemento de cuadrícula (que
es, entre las etiquetas <Grid> y </Grid> ):
<TextBlock Name="ContenidoTextBlock"
AlineaciónHorizontal="Izquierda"
Margen = "44,134,0,0"
VerticalAlignment="Arriba"
Ancho = "425"
Altura="40"/>
<Contenido del botón="Sincronizar"
AlineaciónHorizontal="Izquierda"
Margen = "45,190,0,0"
VerticalAlignment="Arriba"
Ancho = "75"
Haga clic en = "ButtonSync_Click"/>
<Contenido del botón="Asíncrono"
AlineaciónHorizontal="Izquierda"
Margen = "165,190,0,0"
VerticalAlignment="Arriba"
Ancho = "75"
Haga clic en = "ButtonAsync_Click"/>
<Contenido del botón="Asíncrono OK"
AlineaciónHorizontal="Izquierda"
Margen = "285,190,0,0"
VerticalAlignment="Arriba"
Ancho = "75"
Haga clic en = "ButtonAsyncOK_Click"/>
3. En el archivo MainWindow.xaml.cs , utilice las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading;
utilizando System.Threading.Tasks; usando
Sistema.Windows; usando
Sistema.Windows.Input;
4. Agregue el siguiente fragmento de código debajo del constructor MainWindow :
void ButtonSync_Click (remitente del objeto, RoutedEventArgs e) {
ContentTextBlock.Text = cadena.Vacío;
88
Machine Translated by Google
Capítulo 4
intentar
//resultado de la cadena = TaskMethod( //
TaskScheduler.FromCurrentSynchronizationContext()).Resultado; cadena resultado =
TaskMethod().Result; ContentTextBlock.Text = resultado;
} catch (excepción ex) {
ContentTextBlock.Text = ex.InnerException.Message;
}
}
void ButtonAsync_Click (remitente del objeto, RoutedEventArgs e) {
ContentTextBlock.Text = cadena.Vacío; Mouse.OverrideCursor
= Cursores.Esperar;
Tarea<cadena> tarea = TaskMethod();
tarea.ContinuarCon(t =>
{
ContentTextBlock.Text = t.Exception.InnerException.Message; Ratón.OverrideCursor = null; },
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.FromCurrentSynchronizationContext());
void ButtonAsyncOK_Click (remitente del objeto, RoutedEventArgs e) {
ContentTextBlock.Text = cadena.Vacío; Mouse.OverrideCursor
= Cursores.Esperar; Tarea<cadena> tarea = TaskMethod(
TaskScheduler.FromCurrentSynchronizationContext());
tarea.ContinueWith(t => Mouse.OverrideCursor = null,
CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
Tarea<cadena> TaskMethod() {
return TaskMethod(TaskScheduler.Default);
89
Machine Translated by Google
Uso de la biblioteca paralela de tareas
Task<cadena> TaskMethod(programador TaskScheduler) {
Retraso de la tarea = Task.Delay(TimeSpan.FromSeconds(5));
retraso de regreso.ContinueWith(t => {
cadena cadena =
"La tarea se está ejecutando en una identificación de subproceso" +
$"{CurrentThread.ManagedThreadId}. Es el subproceso del grupo de subprocesos:
" +
$"{SubprocesoActual.IsThreadPoolThread}";
ContentTextBlock.Text = str;
devolver str;
}, programador);
}
5. Ejecute el programa.
Cómo funciona...
Aquí nos encontramos con muchas cosas nuevas. Primero, creamos una aplicación WPF en lugar de una
aplicación de consola. Es necesario porque necesitamos un subproceso de interfaz de usuario con un bucle de
mensajes para demostrar las diferentes opciones de ejecutar una tarea de forma asíncrona.
Hay una abstracción muy importante llamada TaskScheduler. Este componente es realmente responsable de
cómo se ejecutará la tarea. El programador de tareas predeterminado coloca las tareas en un subproceso de
trabajo del grupo de subprocesos. Este es el escenario más común; como era de esperar, es la opción predeterminada
en TPL. También sabemos cómo ejecutar una tarea sincrónicamente y cómo adjuntarlas a las tareas principales
para ejecutar esas tareas juntas. Ahora, veamos qué más podemos hacer con las tareas.
Cuando se inicia el programa, creamos una ventana con tres botones. El primer botón invoca una ejecución de
tarea síncrona. El código se coloca dentro del método ButtonSync_Click .
Mientras se ejecuta la tarea, ni siquiera podemos mover la ventana de la aplicación. La interfaz de usuario se congela
por completo mientras el subproceso de la interfaz de usuario está ocupado ejecutando la tarea y no puede responder
a ningún bucle de mensajes hasta que se complete la tarea. Esta es una mala práctica bastante común para las
aplicaciones GUI de Windows, y necesitamos encontrar una manera de solucionar este problema.
90
Machine Translated by Google
Capítulo 4
El segundo problema es que intentamos acceder a los controles de la interfaz de usuario desde otro hilo. Los controles
de la interfaz gráfica de usuario nunca se han diseñado para ser utilizados desde varios subprocesos y, para evitar
posibles errores, no se le permite acceder a estos componentes desde un subproceso que no sea aquel en el que se
creó. Cuando intentamos hacer eso, obtenemos una excepción y el mensaje de excepción se imprime en la ventana
principal en 5 segundos.
Para resolver el primer problema, intentamos ejecutar la tarea de forma asíncrona. Esto es lo que hace el segundo
botón; el código para esto se coloca dentro del método ButtonAsync_Click . Si ejecuta la tarea en un depurador, verá que
se coloca en un grupo de subprocesos y, al final, obtendremos la misma excepción. Sin embargo, la interfaz de usuario
sigue respondiendo todo el tiempo que se ejecuta la tarea. Esto es algo bueno, pero necesitamos deshacernos de la
excepción.
¡Y ya lo hicimos! Para generar el mensaje de error, se proporcionó una continuación con la opción
TaskScheduler.FromCurrentSynchronizationContext . Si esto no se hiciera, no veríamos el mensaje de error porque
obtendríamos la misma excepción que tuvo lugar dentro de la tarea. Esta opción le indica a la infraestructura TPL que
coloque un código dentro de la continuación en el subproceso de la interfaz de usuario y lo ejecute de forma asíncrona
con la ayuda del bucle de mensajes del subproceso de la interfaz de usuario. Esto resuelve el problema de acceder a
los controles de la interfaz de usuario desde otro subproceso, pero mantiene nuestra interfaz de usuario receptiva.
Para verificar si esto es cierto, presionamos el último botón que ejecuta el código dentro del método
ButtonAsyncOK_Click . Todo lo que es diferente es que proporcionamos el programador de tareas de subprocesos de
interfaz de usuario a nuestra tarea. Una vez completada la tarea, verá que se ejecuta en el subproceso de la interfaz de
usuario de forma asíncrona. La interfaz de usuario sigue respondiendo e incluso es posible presionar otro botón a pesar de
que el cursor de espera está activo.
Sin embargo, hay algunos trucos para usar el subproceso de la interfaz de usuario para ejecutar tareas. Si volvemos al
código de la tarea síncrona y descomentamos la línea para obtener el resultado con el programador de tareas del subproceso
de la interfaz de usuario proporcionado, nunca obtendremos ningún resultado. Esta es una situación de interbloqueo
clásica: estamos enviando una operación en la cola del subproceso de la interfaz de usuario, y el subproceso de la
interfaz de usuario espera a que se complete esta operación, pero mientras espera, no puede ejecutar la operación,
que nunca terminará (o incluso comenzará ). Esto también sucederá si llamamos al método Wait en una tarea. Para
evitar interbloqueos, nunca use operaciones sincrónicas en una tarea programada para el subproceso de la interfaz de
usuario; simplemente use ContinueWith o async/await desde C#.
91
Machine Translated by Google
Machine Translated by Google
5
Usando C# 6.0
En este capítulo, veremos la compatibilidad con la programación asincrónica nativa en el lenguaje de
programación C# 6.0. Aprenderás las siguientes recetas:
f Uso del operador await para obtener resultados de tareas asincrónicas
f Usar el operador await en una expresión lambda f Usar el
operador await con las tareas asíncronas consiguientes f Usar el operador await
para la ejecución de tareas asíncronas paralelas
f Manejo de excepciones en operaciones asincrónicas f Evitar
el uso del contexto de sincronización capturado
f Trabajando alrededor del método de vacío asíncrono
f Diseño de un tipo awaitable personalizado f
Uso del tipo dinámico con await
Introducción
Hasta ahora, aprendió sobre Task Parallel Library, la última infraestructura de programación asíncrona de
Microsoft. Nos permite diseñar nuestro programa de forma modular, combinando diferentes
operaciones asíncronas entre sí.
Desafortunadamente, todavía es difícil entender el flujo real del programa cuando se lee un programa
de este tipo. En un programa grande, habrá numerosas tareas y continuaciones que dependen unas
de otras, continuaciones que ejecutan otras continuaciones y continuaciones para el manejo de
excepciones. Todos ellos están reunidos en el código del programa en lugares muy diferentes. Por lo tanto,
comprender la secuencia de qué operación va primero y qué sucede a continuación se convierte en un
problema muy desafiante.
93
Machine Translated by Google
Usando C# 6.0
Otro tema a tener en cuenta es si el contexto de sincronización adecuado se propaga a cada tarea asíncrona
que podría tocar los controles de la interfaz de usuario. Solo se permite usar estos controles desde el
subproceso de la interfaz de usuario; de lo contrario, obtendríamos una excepción de acceso multiproceso.
Hablando de excepciones, también tenemos que usar tareas de continuación separadas para manejar los
errores que ocurren dentro de la operación u operaciones asincrónicas precedentes. Esto, a su vez, da
como resultado un código de manejo de errores complicado que se propaga a través de diferentes partes
del código, sin relación lógica entre sí.
Para abordar estos problemas, los autores de C# introdujeron nuevas mejoras en el lenguaje
denominadas funciones asincrónicas junto con la versión 5.0 de C#. Realmente simplifican la programación
asíncrona, pero al mismo tiempo, es una abstracción de mayor nivel sobre TPL. Como mencionamos
en el Capítulo 4, Uso de la biblioteca paralela de tareas, la abstracción oculta detalles importantes de
implementación y facilita la programación asíncrona a costa de quitarle muchas cosas importantes a un
programador. Es muy importante entender el concepto detrás de las funciones asíncronas para crear
aplicaciones robustas y escalables.
Para crear una función asincrónica, primero marca un método con la palabra clave async .
No es posible tener la propiedad asíncrona o los métodos y constructores de acceso a eventos sin hacer
esto primero. El código se verá de la siguiente manera:
tarea asíncrona<cadena> GetStringAsync() {
esperar Task.Delay(TimeSpan.FromSeconds(2)); volver "¡Hola,
mundo!";
}
Otro dato importante es que las funciones asíncronas deben devolver el tipo Task o Task<T> . Es posible
tener métodos vacíos asíncronos , pero es preferible usar el método Tarea asíncrona en su lugar. La
única opción razonable para usar funciones de anulación asíncronas es cuando se usan controladores de
eventos de control de interfaz de usuario de nivel superior en su aplicación.
Dentro de un método marcado con la palabra clave async , puede usar el operador await . Este operador
trabaja con tareas de TPL y obtiene el resultado de la operación asíncrona dentro de la tarea. Los detalles se
cubrirán más adelante en el capítulo. No puede usar el operador de espera fuera del método asíncrono ; habrá
un error de compilación. Además, las funciones asincrónicas deben tener al menos un operador de
espera dentro de su código. Sin embargo, no tener un operador de espera generará solo una advertencia
de compilación, no un error.
94
Machine Translated by Google
Capítulo 5
Es importante tener en cuenta que este método regresa inmediatamente después de la línea con la llamada
en espera . En caso de una ejecución síncrona, el subproceso en ejecución se bloqueará durante 2 segundos
y luego devolverá un resultado. Aquí, esperamos de forma asíncrona mientras devolvemos un subproceso
de trabajo a un grupo de subprocesos inmediatamente después de ejecutar el operador await . Después de
2 segundos, obtenemos el subproceso de trabajo de un grupo de subprocesos una vez más y ejecutamos el
resto del método asíncrono en él. Esto nos permite reutilizar este subproceso de trabajo para hacer otro trabajo
mientras pasan estos 2 segundos, lo cual es extremadamente importante para la escalabilidad de la aplicación.
Con la ayuda de funciones asíncronas, tenemos un flujo de control de programa lineal, pero sigue siendo
asíncrono. Esto es muy cómodo y muy confuso. Las recetas de este capítulo le ayudarán a aprender todos los
aspectos importantes de las funciones asíncronas.
En mi experiencia, existe un malentendido común acerca de cómo
funcionan los programas si hay dos operadores de espera consecutivos
en él. Mucha gente piensa que si usamos la función await en una
operación asíncrona tras otra, se ejecutan en paralelo. Sin embargo, en
realidad se ejecutan secuencialmente; el segundo comienza solo cuando
se completa la primera operación. Es muy importante recordar esto, y
más adelante en el capítulo, cubriremos este tema en detalle.
Hay una serie de limitaciones relacionadas con el uso de operadores async y await . En C# 5.0, por
ejemplo, no es posible marcar el método principal de la aplicación de consola como asíncrono; no puede
tener el operador await dentro de un bloque catch, finalmente, lock o inseguro . No está permitido tener
parámetros ref y out en una función asíncrona. Hay más sutilezas, pero estos son los puntos principales.
En C# 6.0, se eliminaron algunas de estas limitaciones; puede usar await inside catch y finalmente
bloquea debido a las mejoras internas del compilador.
Las funciones asincrónicas se convierten en construcciones de programa complejas gracias al compilador
de C# en segundo plano. Intencionalmente no describiré esto en detalle; el código resultante es bastante
similar a otra construcción de C#, denominada iteradores, y se implementa como una especie de
máquina de estado. Dado que muchos desarrolladores han comenzado a usar el modificador asíncrono en
casi todos los métodos, me gustaría enfatizar que no tiene sentido marcar un método asíncrono si no está
destinado a usarse de manera asíncrona o paralela. Llamar al método async incluye un impacto significativo
en el rendimiento, y la llamada al método habitual será entre 40 y 50 veces más rápida en comparación con el
mismo método marcado con la palabra clave async . Por favor, tenga en cuenta eso.
En este capítulo, aprenderá a usar las palabras clave async y await de C# para trabajar con
operaciones asincrónicas. Cubriremos cómo esperar operaciones asíncronas secuencial y paralelamente.
Discutiremos cómo usar await en expresiones lambda, cómo manejar excepciones y cómo evitar errores
al usar los métodos de vacío asíncrono . Para concluir el capítulo, profundizaremos en la propagación del
contexto de sincronización y aprenderá a crear sus propios objetos de espera en lugar de usar tareas.
95
Machine Translated by Google
Usando C# 6.0
Uso del operador await para obtener
resultados de tareas asincrónicas
Esta receta lo guía a través del escenario básico del uso de funciones asincrónicas. Compararemos cómo
obtener un resultado de operación asíncrona con TPL y con el operador await .
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter5\Recipe1.
Cómo hacerlo...
Para usar el operador await para obtener resultados de tareas asincrónicas, realice los siguientes
pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
Asincronía estática de tareas con TPL() {
Tarea<cadena> t = GetInfoAsync("Tarea 1"); Tarea t2 =
t.ContinueWith(tarea => WriteLine(t.Result),
TaskContinuationOptions.NotOnFaulted);
Tarea t3 = t.ContinueWith(tarea =>
WriteLine(t.Exception.InnerException),
TaskContinuationOptions.OnlyOnFaulted);
devuelve Task.WhenAny(t2, t3);
}
tarea asíncrona estática AsynchronyWithAwait() {
intentar
resultado de cadena = esperar GetInfoAsync ("Tarea 2");
WriteLine(resultado);
96
Machine Translated by Google
Capítulo 5
} catch (excepción ex) {
WriteLine(ex);
}
}
Tarea asincrónica estática <cadena> GetInfoAsync (nombre de cadena) {
esperar Task.Delay(TimeSpan.FromSeconds(2)); // lanza una nueva
excepción ("¡Boom!");
devolver
$"La tarea {nombre} se está ejecutando en una identificación de subproceso {CurrentThread.
ManagedThreadId}." + $" Es el
subproceso del grupo de subprocesos: {CurrentThread.IsThreadPoolThread}"; }
4. Agregue el siguiente fragmento de código dentro del método principal :
Tarea t = AsincroníaConTPL(); t.Esperar();
t = AsincroníaConEspera(); t.Esperar();
5. Ejecute el programa.
Cómo funciona...
Cuando se ejecuta el programa, ejecutamos dos operaciones asincrónicas. Uno de ellos es el código
estándar con tecnología TPL y el segundo usa las nuevas funciones de C# asíncrono y en espera . El
método AsynchronyWithTPL inicia una tarea que se ejecuta durante 2 segundos y luego devuelve una
cadena con información sobre el subproceso de trabajo. Luego, definimos una continuación para imprimir el
resultado de la operación asincrónica después de que se complete la operación y otra para imprimir los
detalles de la excepción en caso de que ocurran errores. Finalmente, devolvemos una tarea que representa
una de las tareas de continuación y esperamos su finalización en el método Main .
En el método AsynchronyWithAwait , logramos el mismo resultado usando await con la tarea. Es como si
escribiésemos solo el código síncrono habitual: obtenemos el resultado de la tarea, imprimimos el resultado y
detectamos una excepción si la tarea se completa con errores. La diferencia clave es que en realidad tenemos
un programa asíncrono. Inmediatamente después de usar await, C# crea una tarea que tiene una tarea de
continuación con todo el código restante después del operador await y también se ocupa de la propagación de
excepciones. Luego, devolvemos esta tarea al método Main y esperamos hasta que se complete.
97
Machine Translated by Google
Usando C# 6.0
Tenga en cuenta que, según la naturaleza de la operación asincrónica
subyacente y el contexto de sincronización actual, los medios exactos
para ejecutar código asincrónico pueden diferir. Explicaremos esto más
adelante en el capítulo.
Por lo tanto, podemos ver que la primera y la segunda parte del programa son conceptualmente equivalentes,
pero en la segunda parte el compilador de C# hace el trabajo de manejar el código asíncrono implícitamente. Es,
de hecho, aún más complicado que la primera parte, y cubriremos los detalles en las próximas recetas de este
capítulo.
Recuerde que no se recomienda utilizar los métodos Task.Wait y Task.Result en entornos como Windows GUI o
ASP.NET. Esto podría conducir a interbloqueos si el programador no es 100% consciente de lo que realmente
está pasando en el código. Esto se ilustró en la receta Ajuste de la ejecución de tareas con TaskScheduler en
el Capítulo 4, Uso de la biblioteca paralela de tareas, cuando usamos Task.Result en la aplicación WPF.
Para probar cómo funciona el manejo de excepciones, simplemente elimine el comentario de la línea throw new
Exception dentro del método GetInfoAsync .
Usando el operador await en una expresión
lambda
Esta receta le mostrará cómo usar await dentro de una expresión lambda. Escribiremos un método anónimo
que use await y obtenga un resultado de la ejecución del método de forma asíncrona.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter5\Recipe2.
Cómo hacerlo...
Para escribir un método anónimo que use await y obtener un resultado de la ejecución del método de forma
asíncrona usando el operador await en una expresión lambda, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando System.Threading.Tasks;
usando System.Console estático; usando
System.Threading.Thread estático;
98
Machine Translated by Google
Capítulo 5
3. Agregue el siguiente fragmento de código debajo del método principal :
Tarea asincrónica estática Procesamiento asincrónico () {
Func<string, Task<string>> asyncLambda = nombre asíncrono => {
esperar Task.Delay(TimeSpan.FromSeconds(2));
devolver
$"La tarea {nombre} se está ejecutando en una identificación de subproceso {CurrentThread.
ID de subproceso administrado}." +
$" Es el subproceso del grupo de subprocesos: {CurrentThread.IsThreadPoolThread}"; };
resultado de cadena = esperar asyncLambda("async lambda");
WriteLine(resultado);
}
4. Agregue el siguiente fragmento de código dentro del método principal :
Tarea t = Procesamientoasincrónico(); t.Esperar();
5. Ejecute el programa.
Cómo funciona...
Primero, movemos la función asincrónica al método AsynchronousProcessing , ya que no podemos usar async
con Main. Luego, describimos una expresión lambda usando la palabra clave async . Como el tipo de
cualquier expresión lambda no se puede inferir de lambda en sí, debemos especificar su tipo en el compilador de
C# de forma explícita. En nuestro caso, el tipo significa que nuestra expresión lambda acepta un parámetro de
cadena y devuelve un objeto Task<string> .
Luego, definimos el cuerpo de la expresión lambda. Una aberración es que el método está definido para devolver
un objeto Task<string> , ¡pero en realidad devolvemos una cadena y no obtenemos errores de compilación!
El compilador de C# genera automáticamente una tarea y nos la devuelve.
El último paso es esperar la ejecución de la expresión lambda asíncrona e imprimir el resultado.
99
Machine Translated by Google
Usando C# 6.0
Uso del operador await con las consiguientes tareas
asincrónicas
Esta receta le mostrará exactamente cómo fluye el programa cuando tenemos varios métodos de espera
consecutivos en el código. Aprenderá a leer el código con el método await y
comprender por qué la llamada en espera es una operación asíncrona.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter5\Recipe3.
Cómo hacerlo...
Para comprender el flujo de un programa en presencia de métodos de espera consecutivos , realice los
siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando System.Threading.Tasks;
usando System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
Asincronía estática de tareas con TPL() {
var containerTask = new Task(() => {
Tarea<cadena> t = GetInfoAsync("TPL 1");
t.ContinueWith(tarea =>
{ WriteLine(t.Result);
Task<string> t2 = GetInfoAsync("TPL 2");
t2.ContinueWith(innerTask => WriteLine(innerTask.Result),
TaskContinuationOptions.NotOnFaulted |
TaskContinuationOptions.AttachedToParent);
t2.ContinueWith(innerTask =>
WriteLine(innerTask.Exception.InnerException),
TaskContinuationOptions.OnlyOnFaulted |
TaskContinuationOptions.AttachedToParent);
},
TaskContinuationOptions.NotOnFaulted |
TaskContinuationOptions.AttachedToParent);
100
Machine Translated by Google
Capítulo 5
t.ContinueWith(tarea => WriteLine(t.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted
|
TaskContinuationOptions.AttachedToParent);
});
containerTask.Start();
volver containerTask;
}
tarea asíncrona estática AsynchronyWithAwait() {
intentar
{
resultado de cadena = esperar GetInfoAsync ("Async 1");
WriteLine(resultado); resultado
= esperar GetInfoAsync("Async 2");
WriteLine(resultado);
} catch (excepción ex) {
WriteLine(ex);
}
}
Tarea asincrónica estática <cadena> GetInfoAsync (nombre de cadena) {
WriteLine($"¡Tarea {nombre} iniciada!"); esperar
Task.Delay(TimeSpan.FromSeconds(2)); if(name == "TPL 2") throw new
Exception("¡Boom!");
devolver
$"La tarea {nombre} se está ejecutando en una identificación de subproceso {CurrentThread.
ID de subproceso administrado}." +
$" Es el subproceso del grupo de subprocesos: {CurrentThread.IsThreadPoolThread}";
}
4. Agregue el siguiente fragmento de código dentro del método principal :
Tarea t = AsincroníaConTPL(); t.Esperar();
t = AsincroníaConEspera(); t.Esperar();
5. Ejecute el programa.
101
Machine Translated by Google
Usando C# 6.0
Cómo funciona...
Cuando se ejecuta el programa, ejecutamos dos operaciones asincrónicas tal como lo hicimos en la primera receta.
Sin embargo, esta vez, comenzaremos con el método AsynchronyWithAwait . Todavía parece el código
síncrono habitual; la única diferencia son las dos declaraciones de espera . El punto más importante es que el
código aún es secuencial, y la tarea Async 2 comenzará solo después de que se complete la anterior. Cuando
leemos el código, el flujo del programa es muy claro: vemos qué se ejecuta primero y qué sucede después.
Entonces, ¿cómo es este programa asíncrono? Bueno, primero, no siempre es asíncrono. Si una tarea ya está
completa cuando usamos await, obtendremos su resultado de forma sincrónica. De lo contrario, el enfoque
común cuando vemos una declaración de espera dentro del código es notar que en este punto, el método
regresará inmediatamente y el resto del código se ejecutará en una tarea de continuación. Como no bloqueamos
la ejecución, esperando el resultado de una operación, es una llamada asíncrona. En lugar de llamar a t.Wait
en el método Main , podemos realizar cualquier otra tarea mientras se ejecuta el código en el método
AsynchronyWithAwait . Sin embargo, el subproceso principal debe esperar hasta que se completen todas las
operaciones asincrónicas, o se detendrán mientras se ejecutan en subprocesos en segundo plano.
El método AsynchronyWithTPL imita el mismo flujo de programa que el método
AsynchronyWithAwait . Necesitamos una tarea de contenedor para manejar todas las tareas
dependientes juntas. Luego, comenzamos la tarea principal y le agregamos un conjunto de continuaciones.
Cuando la tarea está completa, imprimimos el resultado; luego comenzamos una tarea más, que a su vez tiene
más continuaciones para continuar el trabajo después de que se complete la segunda tarea. Para probar el
manejo de excepciones, lanzamos una excepción a propósito cuando ejecutamos la segunda tarea e imprimimos
su información. Este conjunto de continuaciones crea el mismo flujo de programa que en el primer método, y
cuando lo comparamos con el código con los métodos de espera , podemos ver que es mucho más fácil de leer
y comprender. El único truco es recordar que asincronía no siempre significa ejecución paralela.
Uso del operador await para la ejecución de tareas
asincrónicas paralelas
En esta receta, aprenderá a usar await para ejecutar operaciones asincrónicas en paralelo en lugar de la
ejecución secuencial habitual.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter5\Recipe4.
102
Machine Translated by Google
Capítulo 5
Cómo hacerlo...
Para comprender el uso del operador await para la ejecución de tareas asincrónicas en paralelo, realice
los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente código debajo del método principal :
Tarea asincrónica estática Procesamiento asincrónico () {
Tarea<cadena> t1 = GetInfoAsync("Tarea 1", 3);
Tarea<cadena> t2 = GetInfoAsync("Tarea 2", 5);
string[] resultados = espera Task.WhenAll(t1, t2); foreach (resultado de cadena
en resultados) {
WriteLine(resultado);
}
}
Tarea asincrónica estática <cadena> GetInfoAsync (nombre de cadena, segundos int) {
esperar Task.Delay(TimeSpan.FromSeconds(segundos)); //espera Task.Run(()
=> //
Thread.Sleep(TimeSpan.FromSeconds(segundos)));
devolver
$"La tarea {nombre} se está ejecutando en una identificación de subproceso" +
$"{CurrentThread.ManagedThreadId}". +
$"Es el subproceso del grupo de subprocesos: {CurrentThread.IsThreadPoolThread}";
}
4. Agregue el siguiente fragmento de código dentro del método principal :
Tarea t = Procesamientoasincrónico(); t.Esperar();
5. Ejecute el programa.
103
Machine Translated by Google
Usando C# 6.0
Cómo funciona...
Aquí, definimos dos tareas asincrónicas que se ejecutan durante 3 y 5 segundos, respectivamente. Luego, usamos un
método auxiliar Task.WhenAll para crear otra tarea que se completará solo cuando se completen todas las tareas subyacentes.
Luego, esperamos el resultado de esta tarea conjunta. Después de 5 segundos, obtenemos todos los resultados, lo que
significa que las tareas se estaban ejecutando simultáneamente.
Sin embargo, hay una observación interesante. Cuando ejecuta el programa, puede notar que es probable que ambas
tareas sean atendidas por el mismo subproceso de trabajo de un grupo de subprocesos. ¿Cómo es esto posible cuando hemos
ejecutado las tareas en paralelo? Para hacer las cosas aún más interesantes, comentemos la línea await Task.Delay dentro del
método GetIntroAsync y eliminemos el comentario de la línea await Task.Run , y luego ejecutemos el programa.
Veremos que en este caso, ambas tareas serán atendidas por diferentes subprocesos de trabajo. La diferencia es que
Task.Delay usa un temporizador bajo el capó y el procesamiento es el siguiente: obtenemos el subproceso de trabajo de un
grupo de subprocesos, que espera que el método Task.Delay devuelva un resultado. Luego, el método Task.Delay inicia el
temporizador y especifica un fragmento de código que se llamará cuando el temporizador cuente la cantidad de segundos
especificados para la tarea.
Método de retraso . Luego, devolvemos inmediatamente el subproceso de trabajo a un grupo de subprocesos. Cuando se
ejecuta el evento del temporizador, obtenemos cualquier subproceso de trabajo disponible de un grupo de subprocesos una
vez más (que podría ser el mismo subproceso que usamos primero) y ejecutamos el código proporcionado al temporizador en él.
Cuando usamos el método Task.Run , obtenemos un subproceso de trabajo de un grupo de subprocesos y lo bloqueamos
durante una cantidad de segundos, proporcionados al método Thread.Sleep . Luego, obtenemos un segundo subproceso
de trabajo y lo bloqueamos también. En este escenario, consumimos dos subprocesos de trabajo y no hacen absolutamente
nada, ya que no pueden realizar ninguna otra tarea mientras esperan.
Hablaremos en detalle sobre el primer escenario en el Capítulo 9, Uso de E/S asíncrona, donde analizaremos un gran
conjunto de operaciones asíncronas que trabajan con entradas y salidas de datos.
Usar el primer enfoque siempre que sea posible es la clave para crear aplicaciones de servidor escalables.
Manejo de excepciones en operaciones
asíncronas
Esta receta describirá cómo lidiar con el manejo de excepciones usando funciones asincrónicas en C#. Aprenderá a trabajar con
excepciones agregadas en caso de que use await con varias operaciones asincrónicas paralelas.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter5\Recipe5.
104
Machine Translated by Google
Capítulo 5
Cómo hacerlo...
Para comprender el manejo de excepciones en operaciones asincrónicas, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading.Tasks; usando
System.Console estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
Tarea asincrónica estática Procesamiento asincrónico () {
WriteLine("1. Excepción única");
intentar
resultado de cadena = esperar GetInfoAsync ("Tarea 1", 2);
WriteLine(resultado);
} catch (excepción ex) {
WriteLine($"Detalles de la excepción: {ex}");
}
Línea de escritura();
WriteLine("2. Múltiples excepciones");
Tarea<cadena> t1 = GetInfoAsync("Tarea 1", 3);
Tarea<cadena> t2 = GetInfoAsync("Tarea 2", 2); intentar {
string[] resultados = espera Task.WhenAll(t1, t2); WriteLine(resultados.Longitud);
} catch (excepción ex) {
WriteLine($"Detalles de la excepción: {ex}");
}
Línea de escritura();
WriteLine("3. Múltiples excepciones con AggregateException");
105
Machine Translated by Google
Usando C# 6.0
t1 = GetInfoAsync("Tarea 1", 3); t2 =
GetInfoAsync("Tarea 2", 2); Task<string[]> t3 =
Task.WhenAll(t1, t2); intentar {
string[] resultados = esperar t3;
WriteLine(resultados.Longitud);
} atrapar
{
var ae = t3.Exception.Flatten(); var excepciones =
ae.InnerExceptions; WriteLine($"Excepciones capturadas:
{excepciones.Count}"); foreach (var e en excepciones) {
WriteLine($"Detalles de la excepción: {e}");
Línea de escritura();
}
}
Línea de escritura();
WriteLine("4. esperar en catch y finalmente bloques");
intentar
resultado de cadena = esperar GetInfoAsync ("Tarea 1", 2);
WriteLine(resultado);
} catch (excepción ex) {
esperar Task.Delay(TimeSpan.FromSeconds(1)); WriteLine($"Capturar
bloque con espera: Detalles de la excepción: {ex}");
} finalmente
{
esperar Task.Delay(TimeSpan.FromSeconds(1));
WriteLine("Finalmente bloquear");
}
}
Tarea asincrónica estática <cadena> GetInfoAsync (nombre de cadena, segundos int) {
esperar Task.Delay(TimeSpan.FromSeconds(segundos)); throw new
Exception($"Boom from {name}!");
}
106
Machine Translated by Google
Capítulo 5
4. Agregue el siguiente fragmento de código dentro del método principal :
Tarea t = Procesamientoasincrónico(); t.Esperar();
5. Ejecute el programa.
Cómo funciona...
Ejecutamos cuatro escenarios para ilustrar los casos más comunes de manejo de errores usando async y await en
C#. El primer caso es muy simple y casi idéntico al código síncrono habitual.
Simplemente usamos la instrucción try/catch y obtenemos los detalles de la excepción.
Un error muy común es utilizar el mismo enfoque cuando se esperan más de una operación asíncrona. Si
usamos el bloque catch de la misma manera que lo hicimos antes, obtendremos solo la primera excepción del
objeto AggregateException subyacente .
Para recopilar toda la información, tenemos que usar la propiedad Exception de las tareas en espera . En el
tercer escenario, aplanamos la jerarquía AggregateException y luego desenvolvemos todas las excepciones
subyacentes usando el método Flatten de AggregateException.
Para ilustrar los cambios de C# 6.0, usamos await inside catch y finalmente bloques del código de manejo
de excepciones. Para verificar que no era posible usar await dentro de catch y finalmente bloques en la
versión anterior de C#, puede compilarlo contra C# 5.0 especificándolo en las propiedades del proyecto en
la configuración avanzada de la sección de compilación.
Evitar el uso del contexto de
sincronización capturado
Esta receta analiza los detalles del comportamiento del contexto de sincronización cuando se usa await para
obtener resultados de operaciones asincrónicas. Aprenderá cómo y cuándo desactivar el flujo de contexto
de sincronización.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter5\Recipe6.
107
Machine Translated by Google
Usando C# 6.0
Cómo hacerlo...
Para comprender los detalles del comportamiento del contexto de sincronización cuando se usa await y aprender cómo
y cuándo desactivar el flujo de contexto de sincronización, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. Agregue referencias a la Biblioteca de Windows Presentation Foundation siguiendo
estos pasos:
1. Haga clic con el botón derecho en la carpeta Referencias del proyecto y seleccione Agregar
referencia… opción de menú.
2. Agregue referencias a estas bibliotecas: PresentationCore, PresentationFramework, System.Xaml y
WindowsBase. Puede usar la función de búsqueda en el cuadro de diálogo del administrador
de referencias de la siguiente manera:
3. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando System.Diagnostics;
usando Sistema.Texto;
utilizando System.Threading.Tasks;
108
Machine Translated by Google
Capítulo 5
usando Sistema.Windows;
utilizando System.Windows.Controls; usando
System.Console estático;
4. Agregue el siguiente fragmento de código debajo del método principal :
Etiqueta estática privada _label;
Clic vacío asíncrono estático (remitente del objeto, EventArgs e) {
_label.Content = new TextBlock {Text = "Calculando..."}; TimeSpan resultWithContext
= await Test(); TimeSpan resultNoContext = await
TestNoContext(); //TimeSpan resultNoContext = // espera
TestNoContext().ConfigureAwait(false);
var sb = nuevo StringBuilder(); sb.AppendLine($"Con el contexto:
{resultWithContext}"); sb.AppendLine($"Sin
el contexto: {resultNoContext}"); sb.AppendLine("Proporción: "
+
$"{resultadoConContexto.TotalMillisegundos/resultadoSinContexto.
TotalMilisegundos:0.00}");
_label.Content = new TextBlock {Text = sb.ToString()};
}
Tarea asincrónica estática<TimeSpan> Test() {
const número iteraciones = 100000;
var sw = nuevo Cronómetro();
sw.Inicio(); for
(int i = 0; i < número de iteraciones; i++) {
var t = Tarea.Ejecutar(() => { }); esperar t;
} sw.Stop();
volver sw.Elapsed;
}
Tarea asincrónica estática<TimeSpan> TestNoContext() {
const número iteraciones = 100000; var sw = nuevo
Cronómetro(); sw.Inicio(); for (int i =
0; i < número
de iteraciones; i++) {
109
Machine Translated by Google
Usando C# 6.0
var t = Tarea.Ejecutar(() => { }); esperar
t.ConfigureAwait(
continueOnCapturedContext: falso);
} sw.Stop();
volver sw.Elapsed;
}
5. Reemplace el método principal con el siguiente fragmento de código:
[STAThread]
static void Principal(cadena[] argumentos) {
aplicación var = nueva aplicación (); var
ganar = nueva ventana (); var panel
= new StackPanel(); botón var = nuevo botón
();
_etiqueta = nueva etiqueta();
_etiqueta.Tamaño de fuente = 32;
_etiqueta.Altura = 200;
botón.Altura = 100; botón.
Tamaño de fuente = 32;
button.Content = new TextBlock { Text = "Iniciar
operaciones asincrónicas"}; botón.Haga clic += Haga clic;
panel.Niños.Add(_etiqueta);
panel.Niños.Agregar(botón); ganar.Contenido
= panel; aplicación. Ejecutar (ganar);
LeerLínea();
}
6. Ejecute el programa.
Cómo funciona...
En este ejemplo, estudiamos uno de los aspectos más importantes del comportamiento predeterminado de
una función asíncrona. Ya conoce los programadores de tareas y los contextos de sincronización del
Capítulo 4, Uso de la biblioteca paralela de tareas. De forma predeterminada, el operador await
intenta capturar contextos de sincronización y ejecuta el código anterior en él. Como ya sabemos, esto nos
ayuda a escribir código asíncrono al trabajar con controles de interfaz de usuario. Además, las
situaciones de interbloqueo, como las que se describieron en el capítulo anterior, no ocurrirán al usar
await, ya que no bloqueamos el subproceso de la interfaz de usuario mientras esperamos el resultado.
110
Machine Translated by Google
Capítulo 5
Esto es razonable, pero veamos qué puede suceder potencialmente. En este ejemplo, creamos una aplicación
de Windows Presentation Foundation mediante programación y nos suscribimos a su evento de clic de
botón. Al hacer clic en el botón, ejecutamos dos operaciones asíncronas.
Uno de ellos usa un operador de espera regular , mientras que el otro usa el método ConfigureAwait con falso
como valor de parámetro. Instruye explícitamente que no debemos usar contextos de sincronización
capturados para ejecutar código de continuación en él. Dentro de cada operación, medimos el tiempo que
tardan en completarse y luego, mostramos el tiempo y las proporciones respectivas en la pantalla principal.
Como resultado, vemos que el operador de espera normal tarda mucho más en completarse. Esto se debe a que
publicamos 100 000 tareas de continuación en el subproceso de la interfaz de usuario, que usa su bucle de
mensajes para trabajar de forma asíncrona con esas tareas. En este caso, no necesitamos que este código se
ejecute en el subproceso de la interfaz de usuario, ya que no accedemos a los componentes de la interfaz de usuario
desde la operación asíncrona; usar ConfigureAwait con falso será una solución mucho más eficiente.
Hay una cosa más que vale la pena señalar. Intente ejecutar el programa simplemente haciendo clic en el botón
y esperando los resultados. Ahora, vuelve a hacer lo mismo, pero esta vez haz clic en el botón e intenta arrastrar
la ventana de la aplicación de lado a lado de forma aleatoria. Notará que el código en el contexto de
sincronización capturado se vuelve más lento. Este divertido efecto secundario ilustra perfectamente lo
peligrosa que es la programación asíncrona. Es muy fácil experimentar una situación como esta, y sería casi
imposible depurarla si nunca antes ha experimentado tal comportamiento.
Para ser justos, veamos el escenario opuesto. En el fragmento de código anterior, dentro del método Click ,
elimine el comentario de la línea comentada y comente la línea inmediatamente anterior.
Al ejecutar la aplicación, obtendremos una excepción de acceso de control multiproceso porque el código que
establece el texto de control de la etiqueta no se publicará en el contexto capturado, sino que se ejecutará en un
subproceso de trabajo del grupo de subprocesos.
Trabajando alrededor del método de vacío asíncrono
Esta receta describe por qué los métodos de vacío asíncrono son bastante peligrosos de usar. Aprenderá en qué
situaciones es aceptable usar este método y qué usar en su lugar, cuando sea posible.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter5\Recipe7.
111
Machine Translated by Google
Usando C# 6.0
Cómo hacerlo...
Para aprender a trabajar con el método de vacío asíncrono , realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
tarea asíncrona estática AsyncTaskWithErrors() {
resultado de cadena = esperar GetInfoAsync ("AsyncTaskException", 2);
WriteLine(resultado);
}
vacío asíncrono estático AsyncVoidWithErrors() {
cadena resultado = esperar GetInfoAsync("AsyncVoidException", 2);
WriteLine(resultado);
}
tarea asíncrona estática AsyncTask() {
resultado de cadena = esperar GetInfoAsync ("AsyncTask", 2);
WriteLine(resultado);
}
vacío asíncrono estático AsyncVoid() {
resultado de cadena = esperar GetInfoAsync("AsyncVoid", 2);
WriteLine(resultado);
}
Tarea asincrónica estática <cadena> GetInfoAsync (nombre de cadena, segundos int) {
esperar Task.Delay(TimeSpan.FromSeconds(segundos));
if(name.Contains("Exception")) throw new
Exception($"Boom from {name}!");
devolver
112
Machine Translated by Google
Capítulo 5
$"La tarea {nombre} se está ejecutando en una identificación de subproceso {CurrentThread.
ID de subproceso administrado}." +
$" Es el subproceso del grupo de subprocesos: {CurrentThread.IsThreadPoolThread}";
}
4. Agregue el siguiente fragmento de código dentro del método principal :
Tarea t = AsyncTask(); t.Esperar();
AsyncVoid();
Dormir (TimeSpan.FromSeconds (3));
t = AsyncTaskWithErrors(); mientras (! t.
Está fallado) {
Dormir (TimeSpan.FromSeconds (1));
}
WriteLine(t.Excepción);
//
intentar //{ // AsyncVoidWithErrors(); //
Thread.Sleep(TimeSpan.FromSeconds(3)); // //capturar (excepción
ex) //
{ // Console.WriteLine(ex); //
int[] números = {1, 2, 3, 4, 5}; Array.ForEach(números,
número asíncrono => {
esperar Task.Delay(TimeSpan.FromSeconds(1)); if (número == 3)
lanza una nueva excepción ("¡Boom!"); WriteLine(número); });
LeerLínea();
5. Ejecute el programa.
113
Machine Translated by Google
Usando C# 6.0
Cómo funciona...
Cuando se inicia el programa, iniciamos dos operaciones asincrónicas llamando a los dos métodos, AsyncTask
y AsyncVoid. El primer método devuelve un objeto Task , mientras que el otro no devuelve nada ya que se
declara asíncrono vacío. Ambos regresan inmediatamente ya que son asincrónicos, pero luego, el
primero se puede monitorear fácilmente con el estado de la tarea devuelta o simplemente llamando al método
Wait en él. La única forma de esperar a que se complete el segundo método es esperar literalmente un tiempo
porque no hemos declarado ningún objeto que podamos usar para monitorear el estado de la operación
asíncrona. Por supuesto, es posible usar algún tipo de variable de estado compartida y establecerla desde
el método de vacío asíncrono mientras se verifica desde el método de llamada , pero es mejor simplemente
devolver un objeto Task en su lugar.
La parte más peligrosa es el manejo de excepciones. En el caso del método de vacío asíncrono , se
publicará una excepción en un contexto de sincronización actual; en nuestro caso, un grupo de subprocesos.
Una excepción no controlada en un grupo de subprocesos terminará todo el proceso. Es posible interceptar
excepciones no controladas mediante el evento AppDomain.UnhandledException , pero no hay forma de
recuperar el proceso desde allí. Para experimentar esto, debemos descomentar el bloque try/catch dentro del
método Main y luego ejecutar el programa.
Otro hecho sobre el uso de expresiones lambda asíncronas vacías es que son compatibles con el tipo
de acción , que se usa ampliamente en la biblioteca de clases estándar de .NET Framework.
Es muy fácil olvidarse del manejo de excepciones dentro de esta expresión lambda, lo que bloqueará el
programa nuevamente. Para ver un ejemplo de esto, descomente el segundo bloque comentado dentro del
método Main .
Recomiendo encarecidamente usar async void solo en los controladores de eventos de la interfaz de usuario. En todas las demás
situaciones, utilice los métodos que devuelven Task en su lugar.
Diseño de un tipo esperable personalizado
Esta receta le muestra cómo diseñar un tipo awaitable muy básico que sea compatible con el operador await .
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter5\Recipe8.
114
Machine Translated by Google
Capítulo 5
Cómo hacerlo...
Para diseñar un tipo de espera personalizado, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Runtime.CompilerServices; utilizando
System.Threading; utilizando
System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
Tarea asincrónica estática Procesamiento asincrónico () {
var sync = new CustomAwaitable(true); resultado de cadena
= esperar sincronización;
WriteLine(resultado);
var async = new CustomAwaitable (falso); resultado = espera
asíncrono;
WriteLine(resultado);
}
clase CustomAwaitable
{
public CustomAwaitable(bool completeSynchronously) {
_completeSynchronously = completeSynchronously;
}
public CustomAwaiter GetAwaiter() {
devolver nuevo CustomAwaiter(_completeSynchronously);
}
privado solo lectura bool _completeSynchronously;
}
115
Machine Translated by Google
Usando C# 6.0
clase CustomAwaiter: INotifyCompletion {
private string _result = "Completado sincrónicamente"; privado solo lectura bool
_completeSynchronously;
public bool IsCompleted => _completeSynchronously;
public CustomAwaiter(bool completeSynchronously) {
_completeSynchronously = completeSynchronously;
}
cadena pública ObtenerResultado() {
devolver _resultado;
}
public void OnCompleted(Continuación de la acción) {
ThreadPool.QueueUserWorkItem( state =>
{ Sleep(TimeSpan.FromSeconds(1)); _result =
GetInfo(); continuación?.Invoke(); });
cadena privada GetInfo() {
devolver
$"La tarea se está ejecutando en una identificación de subproceso {CurrentThread.
ManagedThreadId}." + $" Es el
subproceso del grupo de subprocesos: {CurrentThread.IsThreadPoolThread}";
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
Tarea t = Procesamientoasincrónico(); t.Esperar();
5. Ejecute el programa.
116
Machine Translated by Google
Capítulo 5
Cómo funciona...
Para ser compatible con el operador await , un tipo debe cumplir con una serie de requisitos que se establecen
en la especificación del lenguaje C#. Si tiene instalado Visual Studio 2015, puede encontrar el documento
de especificaciones dentro de la carpeta C:\Program Files (x86)\Microsoft Visual Studio
14.0\VC#\Specifications\1033 (suponiendo que tiene un sistema operativo de 64 bits y usó la carpeta
predeterminada ruta de instalación).
En el párrafo 7.7.7.1, encontramos una definición de expresiones de espera:
Se requiere que la tarea de una expresión de espera esté disponible. Una expresión t es esperable si se
cumple uno de los siguientes:
f t es de tiempo de compilación tipo dinámico
f t tiene una instancia accesible o un método de extensión llamado GetAwaiter sin parámetros
ni parámetros de tipo, y un tipo de retorno A para el cual se cumple todo lo siguiente:
1. A implementa la interfaz System.Runtime.CompilerServices.
INotifyCompletion (en lo sucesivo, INotifyCompletion por razones de brevedad).
2. A tiene una propiedad de instancia accesible y legible IsCompleted de tipo
bool.
3. A tiene un método de instancia accesible GetResult sin parámetros
y sin parámetros de tipo.
Esta información es suficiente para empezar. Primero, definimos un tipo de espera CustomAwaitable e
implementamos el método GetAwaiter . Esto, a su vez, devuelve una instancia del tipo
CustomAwaiter . CustomAwaiter implementa la interfaz INotifyCompletion , tiene la propiedad IsCompleted del
tipo bool y tiene el método GetResult , que devuelve un tipo de cadena . Finalmente, escribimos un fragmento
de código que crea dos objetos CustomAwaitable y los espera a ambos.
Ahora, debemos entender la forma en que se evalúan las expresiones de espera . En esta
ocasión, no se han citado las especificaciones para evitar detalles innecesarios. Básicamente, si la
propiedad IsCompleted devuelve verdadero, simplemente llamamos al método GetResult de forma síncrona.
Esto nos impide asignar recursos para la ejecución de tareas asincrónicas si la operación ya se ha completado.
Cubrimos este escenario proporcionando el parámetro completeSynchronously al método constructor del
objeto CustomAwaitable .
De lo contrario, registramos una acción de devolución de llamada al método OnCompleted de CustomAwaiter
e iniciamos la operación asíncrona. Cuando se completa, llama a la devolución de llamada proporcionada, que
obtendrá el resultado llamando al método GetResult en el objeto CustomAwaiter .
117
Machine Translated by Google
Usando C# 6.0
Esta implementación se ha utilizado únicamente con fines educativos.
Siempre que escriba funciones asincrónicas, el enfoque más natural es
utilizar el tipo de tarea estándar. Debe definir su propio tipo de espera solo
si tiene una razón sólida por la que no puede usar Task y sabe
exactamente lo que está haciendo.
Hay muchos otros temas relacionados con el diseño de tipos disponibles personalizados, como la implementación
de la interfaz ICriticalNotifyCompletion y la propagación del contexto de sincronización. Después de comprender los
conceptos básicos de cómo se diseña un tipo disponible, podrá usar la especificación del lenguaje C# y otras fuentes
de información para encontrar los detalles que necesita con facilidad. Pero me gustaría enfatizar que solo debe usar el
tipo Tarea , a menos que tenga una muy buena razón para no hacerlo.
Usando el tipo dinámico con await
Esta receta le muestra cómo diseñar un tipo muy básico que sea compatible con el operador await y el tipo C#
dinámico.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. Necesitará acceso a Internet para descargar el paquete NuGet.
No hay otros requisitos previos. El código fuente de esta receta se puede encontrar en BookSamples\Chapter5\Recipe9.
Cómo hacerlo...
Para aprender a usar el tipo dinámico con await, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. Agregue referencias al paquete ImpromptuInterface NuGet siguiendo estos pasos:
1. Haga clic con el botón derecho en la carpeta Referencias del proyecto y seleccione Administrar
Opción de menú Paquetes NuGet… .
2. Ahora, agregue sus referencias preferidas a ImpromptuInterface NuGet
paquete. Puede usar la función de búsqueda en el cuadro de diálogo Administrar paquetes NuGet
de la siguiente manera:
118
Machine Translated by Google
Capítulo 5
3. En el archivo Program.cs , utilice las siguientes directivas using :
utilizando el sistema;
utilizando System.Dynamic;
utilizando System.Runtime.CompilerServices; utilizando
System.Threading; utilizando
System.Threading.Tasks; utilizando
ImpromptuInterface; usando
System.Console estático; usando
System.Threading.Thread estático;
4. Agregue el siguiente fragmento de código debajo del método principal :
Tarea asincrónica estática Procesamiento asincrónico () {
resultado de cadena = esperar GetDynamicAwaitableObject (verdadero);
WriteLine(resultado);
resultado = esperar GetDynamicAwaitableObject (falso);
WriteLine(resultado);
119
Machine Translated by Google
Usando C# 6.0
estático dinámico GetDynamicAwaitableObject(bool completeSynchronously)
{
resultado dinámico = nuevo ExpandoObject(); espera dinámica
= new ExpandoObject();
awaiter.Message = "Completado sincrónicamente"; awaiter.IsCompleted =
completeSynchronously; awaiter.GetResult = (Func<string>)(() =>
awaiter.Message);
awaiter.OnCompleted = (Acción<Acción>) (devolución de llamada =>
ThreadPool.QueueUserWorkItem(state =>
{ Sleep(TimeSpan.FromSeconds(1)); awaiter.Message
= GetInfo(); callback?.Invoke();
})
);
IAwaiter<string> proxy = Impromptu.ActLike(awaiter);
result.GetAwaiter = (Func<dynamic>) ( () => proxy );
resultado devuelto;
}
cadena estática GetInfo() {
devolver
$"La tarea se está ejecutando en una identificación de subproceso {CurrentThread.
ManagedThreadId}." + $" Es el
subproceso del grupo de subprocesos: {CurrentThread.IsThreadPoolThread}";
}
5. Agregue el siguiente código debajo de la definición de la clase Programa :
interfaz pública IAwaiter<T>: INotifyCompletion {
bool EstáCompleto { get; }
T ObtenerResultado();
}
120
Machine Translated by Google
Capítulo 5
6. Agregue el siguiente fragmento de código dentro del método principal :
Tarea t = Procesamientoasincrónico(); t.Esperar();
7. Ejecute el programa.
Cómo funciona...
Aquí repetimos el truco de la receta anterior pero esta vez con la ayuda de expresiones dinámicas. Podemos
lograr este objetivo con la ayuda de NuGet, un administrador de paquetes que contiene muchas bibliotecas
útiles. Esta vez, usamos una biblioteca que crea envoltorios dinámicamente, implementando las interfaces que
necesitamos.
Para empezar, creamos dos instancias del tipo ExpandoObject y las asignamos a variables locales
dinámicas. Estas variables serán nuestros objetos awaitable y awaiter .
Dado que un objeto awaitable solo requiere tener el método GetAwaiter , no hay problemas para
proporcionarlo. ExpandoObject (combinado con la palabra clave dinámica ) nos permite personalizarlo y
agregar propiedades y métodos asignando los valores correspondientes.
De hecho, es una colección de tipo diccionario con claves de tipo cadena y valores de tipo objeto. Si está
familiarizado con el lenguaje de programación JavaScript, puede notar que esto es muy similar a los objetos
JavaScript.
Dado que la dinámica nos permite omitir las comprobaciones en tiempo de compilación en C#, ExpandoObject
está escrito de tal manera que si asigna algo a una propiedad, crea una entrada de diccionario, donde la
clave es el nombre de la propiedad y un valor es cualquier valor que es suministrado. Cuando intenta obtener
el valor de la propiedad, entra en el diccionario y proporciona el valor que se almacena en la entrada del
diccionario correspondiente. Si el valor es del tipo Action o Func, en realidad almacenamos un delegado, que a
su vez puede usarse como un método. Por tanto, una combinación del tipo dinámico con ExpandoObject nos
permite crear un objeto y dotarlo dinámicamente de propiedades y métodos.
Ahora, necesitamos construir nuestros objetos awaiter y awaitable . Comencemos con awaiter.
Primero, proporcionamos una propiedad llamada Mensaje y un valor inicial para esta propiedad. Luego,
definimos el método GetResult usando un tipo Func<string> . Asignamos una expresión lambda, que devuelve el
valor de la propiedad Message . Luego implementamos la propiedad IsCompleted .
Si se establece en verdadero, podemos omitir el resto del trabajo y continuar con nuestro objeto en espera que
se almacena en la variable local de resultado . Solo necesitamos agregar un método que devuelva el objeto
dinámico y devolver nuestro objeto awaiter desde él. Entonces, podemos usar result como la expresión
de espera ; sin embargo, se ejecutará sincrónicamente.
El principal desafío es implementar el procesamiento asíncrono en nuestro objeto dinámico.
Las especificaciones del lenguaje C# establecen que un objeto awaiter debe implementar la
interfaz INotifyCompletion o ICriticalNotifyCompletion , lo que no hace ExpandoObject . E incluso cuando
implementamos el método OnCompleted dinámicamente, añadiéndolo al objeto awaiter , no lo lograremos
porque nuestro objeto no implementa ninguna de las interfaces antes mencionadas.
121
Machine Translated by Google
Usando C# 6.0
Para solucionar este problema, usamos la biblioteca ImpromptuInterface que obtuvimos de NuGet. Nos
permite usar el método Impromptu.ActLike para crear dinámicamente objetos proxy que implementarán la
interfaz requerida. Si intentamos crear un proxy que implemente la interfaz INotifyCompletion , fallaremos
porque el objeto proxy ya no es dinámico y esta interfaz solo tiene el método OnCompleted , pero no
tiene la propiedad IsCompleted ni el método GetResult . Como última solución, definimos una interfaz
genérica, IAwaiter<T>, que implementa INotifyCompletion y agrega todas las propiedades y métodos
necesarios. Ahora, lo usamos para la generación de proxy y cambiamos el objeto de resultado para que
devuelva un proxy en lugar de un awaiter del método GetAwaiter . El programa ahora funciona; acabamos
de construir un objeto esperable que es completamente dinámico en tiempo de ejecución.
122
Machine Translated by Google
6
Usando concurrente
Colecciones
En este capítulo, veremos las diferentes estructuras de datos para la programación concurrente incluidas
en la biblioteca de clases base de .NET Framework. Aprenderás las siguientes recetas:
f Usando ConcurrentDictionary
f Implementar procesamiento asincrónico usando ConcurrentQueue f Cambiar el
orden de procesamiento asincrónico con ConcurrentStack f Crear un rastreador
escalable con ConcurrentBag
f Generalización del procesamiento asíncrono con BlockingCollection
Introducción
La programación requiere comprensión y conocimiento de estructuras de datos y algoritmos básicos.
Para elegir la estructura de datos más adecuada para una situación concurrente, un programador debe
conocer muchas cosas, como el tiempo del algoritmo, la complejidad del espacio y la notación O grande.
En diferentes escenarios bien conocidos, siempre sabemos qué estructuras de datos son más eficientes.
Para cálculos concurrentes, necesitamos tener estructuras de datos apropiadas. Estas estructuras
de datos deben ser escalables, evitar bloqueos cuando sea posible y, al mismo tiempo, proporcionar
un acceso seguro para subprocesos. .NET Framework, desde la versión 4, tiene System.Collections.
Espacio de nombres simultáneo con varias estructuras de datos en él. En este capítulo, cubriremos
varias estructuras de datos y le mostraremos ejemplos muy simples de cómo usarlas.
123
Machine Translated by Google
Uso de colecciones concurrentes
Comencemos con ConcurrentQueue. Esta colección utiliza operaciones atómicas de comparación e intercambio
(CAS) , que nos permiten intercambiar valores de dos variables de forma segura, y SpinWait para garantizar
la seguridad de los subprocesos. Implementa una colección First In, First Out (FIFO) , lo que significa que los
elementos salen de la cola en el mismo orden en que se agregaron a la cola. Para agregar un elemento a una
cola, llame al método Enqueue . El método TryDequeue intenta tomar el primer elemento de la cola y el
método TryPeek intenta obtener el primer elemento sin eliminarlo de la cola.
La colección ConcurrentStack también se implementa sin usar bloqueos y solo con operaciones CAS. Esta
es la colección Último en entrar, primero en salir (LIFO) , lo que significa que el elemento agregado más
recientemente se devolverá primero. Para agregar elementos, puede usar los métodos Push y PushRange ;
para recuperar, usa TryPop y TryPopRange, y para inspeccionar, puede usar el método TryPeek .
La colección ConcurrentBag es una colección desordenada que admite elementos duplicados. Está optimizado
para un escenario en el que múltiples subprocesos dividen su trabajo de tal manera que cada subproceso
produce y consume sus propias tareas, lidiando con las tareas de otros subprocesos muy raramente (en cuyo
caso, utiliza bloqueos). Agrega artículos a una bolsa utilizando el método Agregar ; usted inspecciona con
TryPeek y toma artículos de una bolsa con el método TryTake .
Evite usar la propiedad Count en las colecciones mencionadas. Se
implementan mediante listas enlazadas y, por lo tanto, Count es una operación O(N).
Si necesita comprobar si la colección está vacía, utilice la propiedad
IsEmpty, que es una operación O(1).
ConcurrentDictionary es una implementación de colección de diccionarios segura para subprocesos. Es libre
de bloqueo para operaciones de lectura. Sin embargo, requiere bloqueo para operaciones de escritura. El
diccionario simultáneo usa múltiples bloqueos, implementando un modelo de bloqueo detallado
sobre los cubos del diccionario. La cantidad de bloqueos podría definirse mediante un constructor con el
parámetro concurrencyLevel , lo que significa que una cantidad estimada de subprocesos actualizará el
diccionario al mismo tiempo.
Dado que un diccionario concurrente usa bloqueo, hay una serie de operaciones
que requieren adquirir todos los bloqueos dentro del diccionario. Estas
operaciones son: Count, IsEmpty, Keys, Values, CopyTo y ToArray. Evite usar
estas operaciones sin necesidad.
BlockingCollection es un contenedor avanzado sobre la implementación de la interfaz genérica
IProducerConsumerCollection . Tiene muchas características que son más avanzadas y es muy útil para
implementar escenarios de canalización cuando tiene algunos pasos que usan los resultados del procesamiento
de los pasos anteriores. La clase BlockingCollection admite funciones como el bloqueo, la limitación de la
capacidad de colecciones internas, la cancelación de operaciones de recopilación y la recuperación de
valores de varias colecciones de bloqueo.
124
Machine Translated by Google
Capítulo 6
Los algoritmos concurrentes pueden ser muy complicados y cubrir todas las colecciones concurrentes,
ya sean más o menos avanzadas, requeriría escribir un libro aparte.
Aquí, ilustramos solo los ejemplos más simples del uso de colecciones concurrentes.
Uso de diccionario concurrente
Esta receta le muestra un escenario muy simple, comparando el rendimiento de una colección de
diccionario habitual con el diccionario simultáneo en un entorno de subproceso único.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter6\Recipe1.
Cómo hacerlo...
Para comprender la diferencia entre el rendimiento de una colección de diccionario habitual y el diccionario
concurrente, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando System.Collections.Concurrent; usando
System.Collections.Generic; utilizando
System.Diagnostics; usando
System.Console estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
const string Item = "Elemento del diccionario"; const int
Iteraciones = 1000000; cadena estática pública
CurrentItem;
4. Agregue el siguiente fragmento de código dentro del método principal :
var diccionarioconcurrente = new DiccionarioConcurrente<int, string>(); var diccionario
= nuevo
Diccionario<int, string>();
var sw = nuevo Cronómetro();
sw.Inicio(); for
(int i = 0; i < Iteraciones; i++) {
candado (diccionario)
125
Machine Translated by Google
Uso de colecciones concurrentes
{
diccionario[i] = Elemento;
}
} sw.Stop();
WriteLine($"Escribiendo en el diccionario con un candado: {sw.Elapsed}");
sw.Reiniciar();
for (int i = 0; i < Iteraciones; i++) {
diccionarioconcurrente[i] = Elemento;
} sw.Stop();
WriteLine($"Escribiendo en un diccionario concurrente: {sw.Elapsed}");
sw.Reiniciar(); for
(int i = 0; i < Iteraciones; i++) {
bloquear (diccionario) {
ElementoActual = diccionario[i];
}
} sw.Stop();
WriteLine($"Leyendo del diccionario con un candado: {sw.Elapsed}");
sw.Reiniciar(); for
(int i = 0; i < Iteraciones; i++) {
ElementoActual = DiccionarioConcurrente[i];
} sw.Stop();
WriteLine($"Leyendo de un diccionario concurrente: {sw.Elapsed}");
5. Ejecute el programa.
Cómo funciona...
Cuando se inicia el programa, creamos dos colecciones. Uno de ellos es una colección de diccionarios
estándar y el otro es un nuevo diccionario concurrente. Luego, comenzamos a agregarles, usando
un diccionario estándar con un candado y midiendo el tiempo que toma completar un millón de
iteraciones. Luego, medimos el rendimiento de la colección ConcurrentDictionary en el mismo
escenario y finalmente comparamos el rendimiento de la recuperación de valores de ambas
colecciones.
126
Machine Translated by Google
Capítulo 6
En este escenario muy simple, encontramos que ConcurrentDictionary es significativamente más lento
en las operaciones de escritura que un diccionario habitual con un bloqueo, pero es más rápido en las
operaciones de recuperación. Por lo tanto, si necesitamos muchas lecturas seguras para subprocesos
de un diccionario, la colección ConcurrentDictionary es la mejor opción.
Si solo necesita acceso de subprocesos múltiples de solo lectura al diccionario, es
posible que no sea necesario realizar lecturas seguras para subprocesos. En este
escenario, es mucho mejor usar solo un diccionario regular o las colecciones ReadOnlyDictionary.
La colección ConcurrentDictionary se implementa mediante la técnica de bloqueo de granularidad fina , y esto le
permite escalar mejor en varias escrituras que usar un diccionario normal con un bloqueo (lo que se denomina bloqueo
de granularidad gruesa). Como vimos en este ejemplo, cuando usamos solo un subproceso, un diccionario concurrente
es mucho más lento, pero cuando escalamos esto hasta cinco o seis subprocesos (si tenemos suficientes núcleos de
CPU que puedan ejecutarlos simultáneamente), el diccionario concurrente en realidad funcionan mejor.
Implementación de procesamiento asíncrono
usando ConcurrentQueue
Esta receta le mostrará un ejemplo de cómo crear un conjunto de tareas para que varios trabajadores
las procesen de forma asíncrona.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter6\Recipe2.
Cómo hacerlo...
Para comprender el funcionamiento de la creación de un conjunto de tareas para que varios trabajadores las
procesen de forma asincrónica, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando System.Collections.Concurrent;
utilizando System.Threading;
utilizando System.Threading.Tasks; usando
System.Console estático;
127
Machine Translated by Google
Uso de colecciones concurrentes
3. Agregue el siguiente fragmento de código debajo del método principal :
Tarea asincrónica estática RunProgram () {
var taskQueue = new ConcurrentQueue<CustomTask>(); var cts = new
CancellationTokenSource();
var taskSource = Task.Run(() => TaskProducer(taskQueue));
Task[] procesadores = new Task[4]; para (int i = 1; i
<= 4; i++) {
string procesadorId = i.ToString(); procesadores[i1] =
Tarea.Ejecutar(
() => TaskProcessor(taskQueue, $"Processor {processorId}", cts.Token)); }
aguardar taskSource;
cts.CancelAfter(TimeSpan.FromSeconds(2));
esperar Task.WhenAll(procesadores);
}
Tarea asincrónica estática TaskProducer(ConcurrentQueue<CustomTask> queue) {
para (int i = 1; i <= 20; i++) {
espera Tarea.Retraso(50); var
workItem = new CustomTask {Id = i}; cola. Poner en cola (elemento
de trabajo); WriteLine($"La tarea
{workItem.Id} ha sido publicada");
}
}
Tarea asincrónica estática TaskProcessor(
ConcurrentQueue<CustomTask> cola, nombre de cadena,
Token de cancelación)
{
elemento de trabajo de tarea personalizada;
bool dequeueSuccesful = falso;
esperar GetRandomDelay();
hacer
128
Machine Translated by Google
Capítulo 6
dequeueSuccesful = queue.TryDequeue(out workItem); si (eliminar la cola con
éxito) {
WriteLine($"La tarea {workItem.Id} ha sido procesada por {name}"); }
esperar GetRandomDelay();
} while (!token.IsCancellationRequested);
}
Tarea estática GetRandomDelay() {
int delay = new Random(DateTime.Now.Millisecond).Next(1, 500); volver Task.Delay (retraso);
clase CustomTask
{
Id int público { obtener; colocar; }
}
4. Agregue el siguiente fragmento de código dentro del método principal :
Tarea t = EjecutarPrograma();
t.Esperar();
5. Ejecute el programa.
Cómo funciona...
Cuando se ejecuta el programa, creamos una cola de tareas con una instancia de la colección
ConcurrentQueue . Luego, creamos un token de cancelación, que se utilizará para detener el trabajo una vez que
hayamos terminado de publicar tareas en la cola. A continuación, iniciamos un subproceso de trabajo independiente
que publicará tareas en la cola de tareas. Esta parte produce una carga de trabajo para nuestro
procesamiento asíncrono.
Ahora, definamos una parte del programa que consume tareas. Creamos cuatro trabajadores que esperarán un
tiempo aleatorio, obtendrán una tarea de la cola de tareas, la procesarán y repetirán todo el proceso hasta que
señalemos el token de cancelación. Finalmente, comenzamos el subproceso de producción de tareas, esperamos a
que finalice y luego indicamos a los consumidores que hemos terminado de trabajar con el token de cancelación.
El último paso será esperar a que completen todos nuestros consumidores, para terminar de procesar todas las tareas.
129
Machine Translated by Google
Uso de colecciones concurrentes
Vemos que tenemos tareas que se procesan de principio a fin, pero es posible que una tarea posterior
se procese antes que una anterior porque tenemos cuatro trabajadores que se ejecutan de forma
independiente y el tiempo de procesamiento de la tarea no es constante. Vemos que el acceso a la cola es
seguro para subprocesos; ningún elemento de trabajo se tomó dos veces.
Cambiar el orden de procesamiento asíncrono con
ConcurrentStack
Esta receta es una ligera modificación de la anterior. Una vez más, crearemos un conjunto de tareas para
que varios trabajadores las procesen de forma asincrónica, pero esta vez lo implementaremos con
ConcurrentStack y veremos las diferencias.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter6\Recipe3.
Cómo hacerlo...
Para comprender el procesamiento de un conjunto de tareas implementadas con ConcurrentStack, realice
los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Collections.Concurrent; utilizando
System.Threading; utilizando
System.Threading.Tasks; usando
System.Console estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
Tarea asincrónica estática RunProgram ()
{
var taskStack = new ConcurrentStack<CustomTask>(); var cts = new
CancellationTokenSource();
var taskSource = Task.Run(() => TaskProducer(taskStack));
Task[] procesadores = new Task[4]; para (int i = 1; i
<= 4; i++) {
string procesadorId = i.ToString();
130
Machine Translated by Google
Capítulo 6
procesadores[i 1] = Tarea.Ejecutar(
() => TaskProcessor(taskStack, $"Processor {processorId}", cts.Token)); }
aguardar taskSource;
cts.CancelAfter(TimeSpan.FromSeconds(2));
esperar Task.WhenAll(procesadores);
}
Tarea asincrónica estática TaskProducer(ConcurrentStack<CustomTask> stack) {
para (int i = 1; i <= 20; i++) {
espera Tarea.Retraso(50); var
workItem = new CustomTask { Id = i }; stack.Push(elemento de
trabajo); WriteLine($"La tarea
{workItem.Id} ha sido publicada");
}
}
Tarea asincrónica estática TaskProcessor(
pila ConcurrentStack<CustomTask>, nombre de cadena,
Token de cancelación) {
esperar GetRandomDelay(); hacer
{
elemento de trabajo de tarea
personalizada; bool popSuccesful = stack.TryPop(out workItem); si
(popExitoso) {
WriteLine($"La tarea {workItem.Id} ha sido procesada por {name}"); }
esperar GetRandomDelay();
} while (!token.IsCancellationRequested);
}
Tarea estática GetRandomDelay() {
131
Machine Translated by Google
Uso de colecciones concurrentes
int delay = new Random(DateTime.Now.Millisecond).Next(1, 500); volver Task.Delay (retraso);
clase CustomTask
{
Id int público { obtener; colocar; }
}
4. Agregue el siguiente fragmento de código dentro del método principal :
Tarea t = EjecutarPrograma();
t.Esperar();
5. Ejecute el programa.
Cómo funciona...
Cuando se ejecuta el programa, ahora creamos una instancia de la colección ConcurrentStack .
El resto es casi como en la receta anterior, excepto que en lugar de usar los métodos Push y TryPop en la pila
concurrente, usamos Enqueue y TryDequeue en una cola concurrente.
Ahora vemos que se ha cambiado el orden de procesamiento de tareas. La pila es una colección LIFO y los
trabajadores procesan primero las últimas tareas. En el caso de una cola concurrente, las tareas se
procesaron casi en el mismo orden en que se agregaron. Esto significa que dependiendo de la cantidad de
trabajadores, seguramente procesaremos la tarea que se creó primero en un período de tiempo determinado. En
el caso de una pila, las tareas que se crearon antes tendrán menor prioridad y es posible que no se procesen
hasta que un productor deje de asignar más tareas a la pila. Este comportamiento es muy específico y es
mucho mejor usar una cola en este escenario.
Creación de un rastreador escalable con
ConcurrentBag
Esta receta le muestra cómo escalar la carga de trabajo entre una cantidad de trabajadores independientes que
producen trabajo y lo procesan.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter6\Recipe4.
132
Machine Translated by Google
Capítulo 6
Cómo hacerlo...
Los siguientes pasos demuestran cómo escalar la carga de trabajo entre una cantidad de trabajadores
independientes que producen trabajo y lo procesan:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Collections.Concurrent; usando
System.Collections.Generic; utilizando
System.Threading.Tasks; usando System.Console
estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
Diccionario estático<cadena, cadena[]> _contentEmulation = nuevo
Diccionario<cadena, cadena[]>();
Tarea asincrónica estática RunProgram () {
var bag = new ConcurrentBag<CrawlingTask>();
cadena[] direcciones URL = {"http://microsoft.com/", "http://google.com/",
"http://facebook.com/", "http://twitter.com/"};
var rastreadores = nueva tarea[4]; para (int i
= 1; i <= 4; i++) {
string crawlerName = $"Rastreador {i}"; bag.Add(nueva
Tarea de Rastreo { UrlToCrawl = urls[i1],
ProducerName = "raíz"}); rastreadores[i
1] = Task.Run(() => Crawl(bag, crawlerName));
}
esperar Task.WhenAll(rastreadores);
}
Rastreo de tareas asíncrono estático (ConcurrentBag<CrawlingTask> bag, string crawlerName) {
tarea CrawlingTask; while
(bag.TryTake(out task)) {
IEnumerable<cadena> urls = esperar GetLinksFromContent(tarea);
si (URL! = nulo)
133
Machine Translated by Google
Uso de colecciones concurrentes
{
foreach (var URL en URL) {
var t = nueva tarea de rastreo {
UrlParaRastrear = url,
Nombre del productor = nombre del rastreador
};
bolsa.Añadir(t);
}
}
WriteLine($"Url de indexación {task.UrlToCrawl} publicada por " +
$"{task.ProducerName} es completado por {crawlerName}!");
}
}
Tarea asincrónica estática<IEnumerable<cadena>> GetLinksFromContent(Tarea de gTask de rastreo) {
esperar GetRandomDelay();
if (_contentEmulation.ContainsKey(tarea.UrlToCrawl)) devuelve contentEmulation[tarea.UrlToCrawl];
_
devolver nulo;
}
vacío estático CreateLinks() {
_contentEmulation["http://microsoft.com/"] = nuevo [] { "http://microsoft.com/a.html", "http://microsoft.com/
b.html" };
_contentEmulation["http://microsoft.com/a.html"] = nuevo[] { "http://microsoft.com/c.html", "http://
microsoft.com/d.html" }; _contentEmulation["http://microsoft.com/b.html"] = nuevo[] { "http://microsoft.com/
e.html" };
_contentEmulation["http://google.com/"] = nuevo[] { "http://
google.com/a.html", "http://google.com/b.html" }; _contentEmulation["http://
google.com/a.html"] = nuevo[] { "http:// google.com/c.html", "http://google.com/d.html" };
_contentEmulation["http://google.com/b.html"] = nuevo[] { "http:// google.com/
e.html", "http://google.com/f.html" }; _contentEmulation["http://google.com/c.html"] = nuevo[] { "http://
google.com/h.html", "http://google.com/i.html" };
134
Machine Translated by Google
Capítulo 6
_contentEmulation["http://facebook.com/"] = nuevo [] { "http://
facebook.com/a.html", "http://facebook.com/b.html" };
_contentEmulation["http://facebook.com/a.html"] = nuevo[] { "http://facebook.com/
c.html", "http://facebook.com/d.html" }; _contentEmulation["http://facebook.com/b.html"] =
nuevo[] { "http://facebook.com/e.html" };
_contentEmulation["http://twitter.com/"] = nuevo[] { "http://
twitter.com/a.html", "http://twitter.com/b.html" };
_contentEmulation["http://twitter.com/a.html"] = nuevo[] { "http://twitter.com/c.html",
"http://twitter.com/d.html" }; _contentEmulation["http://twitter.com/b.html"] = nuevo[]
{ "http://twitter.com/e.html" }; _contentEmulation["http://twitter.com/c.html"] = nuevo[]
{ "http://twitter.com/f.html", "http://twitter.com/
g.html" }; _contentEmulation["http://twitter.com/d.html"] = nuevo[] { "http://twitter.com/
h.html" }; _contentEmulation["http://twitter.com/e.html"] = nuevo[] { "http://twitter.com/
i.html" }; }
Tarea estática GetRandomDelay() {
int delay = new Random(DateTime.Now.Millisecond).Next(150, 200); volver Task.Delay (retraso);
clase Tarea de rastreo {
public string UrlToCrawl { get; colocar; }
public string ProducerName { get; colocar; }
}
4. Agregue el siguiente fragmento de código dentro del método principal :
CrearEnlaces();
Tarea t = EjecutarPrograma();
t.Esperar();
5. Ejecute el programa.
135
Machine Translated by Google
Uso de colecciones concurrentes
Cómo funciona...
El programa simula la indexación de páginas web con múltiples rastreadores web. Un rastreador web es un
programa que abre una página web por su dirección, indexa el contenido, intenta visitar todos los enlaces que contiene
esta página y también indexa estas páginas enlazadas. Al principio, definimos un diccionario que contiene diferentes
URL de páginas web. Este diccionario simula páginas web que contienen enlaces a otras páginas. La implementación es
muy ingenua; no se preocupa por indexar las páginas ya visitadas, pero es sencillo y nos permite centrarnos en la
carga de trabajo concurrente.
Luego, creamos una bolsa concurrente que contiene tareas de rastreo. Creamos cuatro rastreadores y
proporcionamos una URL raíz del sitio diferente para cada uno de ellos. Luego, esperamos a que todos los rastreadores compitan.
Ahora, cada rastreador comienza a indexar la URL del sitio que se le proporcionó. Simulamos el proceso de E/S
de la red esperando una cantidad de tiempo aleatoria; luego, si la página contiene más URL, el rastreador publica más
tareas de rastreo en la bolsa. Luego, verifica si quedan tareas para rastrear en la bolsa. Si no, el rastreador está completo.
Si comprobamos el resultado debajo de las primeras cuatro líneas, que son URL raíz, veremos que normalmente, que
eran URL raíz, veremos que normalmente una tarea publicada por el rastreador número N es procesada por el
mismo rastreador . Sin embargo, las líneas posteriores serán diferentes. Esto sucede porque internamente,
ConcurrentBag está optimizado exactamente para este escenario en el que hay varios subprocesos que agregan
elementos y los eliminan. Esto se logra al permitir que cada subproceso funcione con su propia cola local de elementos y,
por lo tanto, no necesitamos bloqueos mientras esta cola está ocupada. Solo cuando no queden elementos en la cola
local, realizaremos algún bloqueo e intentaremos robar el trabajo de la cola local de otro subproceso. Este comportamiento
ayuda a distribuir el trabajo entre todos los trabajadores y evitar el bloqueo.
Generalización del procesamiento asíncrono con
BlockingCollection
Esta receta describirá cómo usar BlockingCollection para simplificar la implementación del procesamiento asincrónico
de la carga de trabajo.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No se requieren otros requisitos previos. El código
fuente de esta receta se puede encontrar en BookSamples\Chapter6\Recipe5.
136
Machine Translated by Google
Capítulo 6
Cómo hacerlo...
Para comprender cómo BlockingCollection simplifica la implementación del procesamiento asincrónico de
la carga de trabajo, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Collections.Concurrent; utilizando
System.Threading.Tasks; usando System.Console
estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
Programa de ejecución de tareas asíncronas estáticas (IProducerConsumerCollection<CustomTask> colección
= null) {
var taskCollection = new BlockingCollection<CustomTask>();
si (colección! = nulo)
taskCollection= new BlockingCollection<CustomTask>(colección);
var taskSource = Task.Run(() => TaskProducer(taskCollection));
Task[] procesadores = new Task[4]; para (int i = 1; i
<= 4; i++) {
string procesadorId = $"Procesador {i}"; procesadores[i 1] =
Task.Run( () => TaskProcessor(taskCollection,
procesadorId));
}
aguardar taskSource;
esperar Task.WhenAll(procesadores);
}
Tarea asincrónica estática TaskProducer (colección BlockingCollection<CustomTask>)
{
para (int i = 1; i <= 20; i++) {
espera Tarea.Retraso(20); var
workItem = new CustomTask { Id = i };
137
Machine Translated by Google
Uso de colecciones concurrentes
colección. Agregar (elemento de
trabajo); WriteLine($"La tarea {workItem.Id} ha sido publicada");
} colección. Completar Adición ();
}
Tarea asincrónica estática TaskProcessor(
colección BlockingCollection<CustomTask>, nombre de cadena)
{
esperar GetRandomDelay(); foreach
(elemento CustomTask en la colección.GetConsumingEnumerable()) {
WriteLine($"La tarea {item.Id} ha sido procesada por {name}"); esperar GetRandomDelay();
}
}
Tarea estática GetRandomDelay() {
int delay = new Random(DateTime.Now.Millisecond).Next(1, 500); volver Task.Delay (retraso);
clase CustomTask
{
Id int público { obtener; colocar; }
}
4. Agregue el siguiente fragmento de código dentro del método principal :
WriteLine("Uso de una cola dentro de BlockingCollection"); Línea de escritura(); Tarea t
= EjecutarPrograma();
t.Esperar();
Línea de escritura();
WriteLine("Uso de una pila dentro de BlockingCollection");
Línea de escritura();
t = RunProgram(new ConcurrentStack<CustomTask>()); t.Esperar();
5. Ejecute el programa.
138
Machine Translated by Google
Capítulo 6
Cómo funciona...
Aquí, tomamos exactamente el primer escenario, pero ahora usamos una clase BlockingCollection que brinda
muchos beneficios útiles. En primer lugar, podemos cambiar la forma en que se almacenan las tareas dentro
de la colección de bloqueo. De manera predeterminada, usa un contenedor ConcurrentQueue , pero podemos
usar cualquier colección que implemente la interfaz genérica IProducerConsumerCollection . Para ilustrar
esto, ejecutamos el programa dos veces, usando ConcurrentStack como la colección subyacente la
segunda vez.
Los trabajadores obtienen elementos de trabajo iterando el resultado de la llamada al método
GetConsumingEnumerable en una colección de bloqueo. Si no hay elementos dentro de la colección, el
iterador simplemente bloqueará el subproceso de trabajo hasta que se publique un elemento en la colección. El
ciclo finaliza cuando el productor llama al método CompleteAdding en la colección. Señala que el trabajo está hecho.
Es muy fácil cometer un error y simplemente iterar BlockingCollection a medida
que implementa IEnumerable. No olvide usar
GetConsumingEnumerable, o de lo contrario, simplemente iterará una
"instantánea" de una colección y obtendrá un comportamiento del programa completamente inesperado.
El productor de la carga de trabajo inserta las tareas en BlockingCollection y luego llama al método
CompleteAdding , lo que hace que se completen todos los trabajadores. Ahora, en la salida del
programa, vemos dos secuencias de resultados que ilustran la diferencia entre las colecciones
simultáneas de cola y pila.
139
Machine Translated by Google
Machine Translated by Google
7
Uso de PLINQ
En este capítulo, revisaremos diferentes paradigmas de programación paralela, como el paralelismo de
tareas y datos, y cubriremos los conceptos básicos del paralelismo de datos y las consultas LINQ paralelas.
Aprenderás las siguientes recetas:
f Usando la clase Parallel
f Paralelizar una consulta LINQ f
Ajustar los parámetros de una consulta PLINQ f Manejar
excepciones en una consulta PLINQ
f Administrar la partición de datos en una consulta PLINQ f
Crear un agregador personalizado para una consulta PLINQ
Introducción
En .NET Framework, hay un subconjunto de bibliotecas que se llama Parallel Framework, a menudo
denominado Parallel Framework Extensions (PFX), que fue el nombre de la primera versión de estas
bibliotecas. Parallel Framework se lanzó con .NET Framework 4.0 y consta de tres partes principales:
f La biblioteca paralela de tareas (TPL)
f Cobros concurrentes
f Paralelo LINQ o PLINQ
Hasta ahora, ha aprendido a ejecutar varias tareas en paralelo y sincronizarlas entre sí. De hecho, dividimos
nuestro programa en un conjunto de tareas y teníamos diferentes subprocesos ejecutando diferentes tareas.
Este enfoque se llama paralelismo de tareas, y hasta ahora solo has estado aprendiendo sobre el paralelismo
de tareas.
141
Machine Translated by Google
Uso de PLINQ
Imagine que tenemos un programa que realiza algunos cálculos pesados sobre un gran conjunto de datos.
La forma más fácil de paralelizar este programa es dividir este conjunto de datos en fragmentos más
pequeños, ejecutar los cálculos necesarios sobre estos fragmentos de datos en paralelo y luego agregar
los resultados de estos cálculos. Este modelo de programación se llama paralelismo de datos.
El paralelismo de tareas tiene el nivel de abstracción más bajo. Definimos un programa como una
combinación de tareas, definiendo explícitamente cómo se combinan. Un programa compuesto de esta
manera podría ser muy complejo y detallado. Las operaciones paralelas se definen en diferentes lugares de
este programa y, a medida que crece, el programa se vuelve más difícil de entender y mantener. Esta forma de
hacer que el programa sea paralelo se llama paralelismo no estructurado. Es el precio que tenemos que
pagar si tenemos una lógica de paralelización compleja.
Sin embargo, cuando tenemos una lógica de programa más simple, podemos intentar descargar más
detalles de paralelización a las bibliotecas PFX y al compilador C#. Por ejemplo, podríamos decir: "Me gustaría
ejecutar esos tres métodos en paralelo, y no me importa cómo sucede exactamente esta paralelización;
deje que la infraestructura .NET decida los detalles". Esto eleva el nivel de abstracción ya que no tenemos
que proporcionar una descripción detallada de cómo exactamente estamos paralelizando esto. Este enfoque
se denomina paralelismo estructurado , ya que la paralelización suele ser una especie de declaración y cada
caso de paralelización se define exactamente en un lugar del programa.
Podría tener la impresión de que el paralelismo no estructurado es una mala práctica
y que siempre se debe usar el paralelismo estructurado en su lugar. Me gustaría
enfatizar que esto no es cierto. De hecho, el paralelismo estructurado es más fácil
de mantener y se prefiere cuando es posible, pero es un enfoque mucho menos
universal. En general, hay muchas situaciones en las que simplemente no
podemos usarlo, y está perfectamente bien usar el paralelismo de tareas TPL
de una manera no estructurada.
TPL tiene una clase Parallel , que proporciona API para el paralelismo estructurado. Esto sigue siendo parte
de TPL, pero lo revisaremos en este capítulo porque es un ejemplo perfecto de transición de un nivel de
abstracción más bajo a uno más alto. Cuando usamos las API de clase Parallel , no necesitamos proporcionar
los detalles de cómo particionamos nuestro trabajo. Sin embargo, todavía necesitamos definir explícitamente
cómo hacemos un solo resultado a partir de resultados particionados.
PLINQ tiene el nivel de abstracción más alto. Particiona automáticamente los datos en fragmentos y
decide si realmente necesitamos paralelizar la consulta o si será más efectivo usar el procesamiento de
consultas secuenciales habitual. Luego, la infraestructura PLINQ se encarga de combinar los
resultados particionados. Hay muchas opciones que los programadores pueden modificar para optimizar
la consulta y lograr el mejor rendimiento y resultado posibles.
En este capítulo, cubriremos el uso de la API de clase paralela y muchas opciones PLINQ diferentes,
como hacer una consulta LINQ paralela, configurar un modo de ejecución y ajustar el grado de paralelismo
de una consulta PLINQ, tratar con el orden de un elemento de consulta y manejar Excepciones de PLINQ.
También aprenderá a administrar la partición de datos para consultas PLINQ.
142
Machine Translated by Google
Capítulo 7
Usando la clase Parallel
Esta receta le muestra cómo usar las API de clase paralela . Aprenderá cómo invocar métodos en paralelo,
cómo realizar bucles paralelos y modificar la mecánica de paralelización.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter7\Recipe1.
Cómo hacerlo...
Para invocar métodos en paralelo, realizar bucles paralelos y modificar la mecánica de paralelización mediante
la clase Parallel , realice los pasos indicados:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Linq;
utilizando System.Threading; utilizando
System.Threading.Tasks; usando System.Console
estático; usando System.Threading.Thread
estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
cadena estática EmulateProcessing(string taskName) {
Suspender(TimeSpan.FromMilliseconds(
new Random(DateTime.Now.Millisecond).Next(250, 350)));
" +
La tarea WriteLine($"{taskName} se procesó en un
$"id del subproceso {CurrentThread.ManagedThreadId}");
devuelve el nombre de la tarea;
4. Agregue el siguiente fragmento de código dentro del método principal :
Parallel.Invoke(
() => EmularProcesamiento("Tarea1"),
() => EmularProcesamiento("Tarea2"),
() => EmularProcesamiento("Tarea3")
);
var cts = new CancellationTokenSource();
143
Machine Translated by Google
Uso de PLINQ
var resultado = Parallel.ForEach(
Enumerable.Range(1, 30), new
ParallelOptions {
CancellationToken = cts.Token,
MaxDegreeOfParallelism = Environment.ProcessorCount,
Programador de tareas = Programador de tareas.Predeterminado
},
(yo, estado) => {
WriteLine(i); si (yo ==
20) {
estado.Break();
WriteLine($"El bucle está detenido: {state.IsStopped}");
} });
Línea de escritura("");
WriteLine($"IsCompleted: {result.IsCompleted}"); WriteLine($"Iteración de
interrupción más baja: {result.
Iteración de rotura más baja}");
5. Ejecute el programa.
Cómo funciona...
Este programa demuestra diferentes características de la clase Parallel . El método Invoke nos permite
ejecutar varias acciones en paralelo sin mayor problema en comparación con la definición de tareas en
TPL. El método Invoke bloquea el otro subproceso hasta que se completan todas las acciones, lo cual
es un escenario bastante común y conveniente.
La siguiente característica son los bucles paralelos, que se definen con los métodos For y ForEach .
Miraremos de cerca a ForEach ya que es muy similar a For. Con el bucle paralelo ForEach , puede
procesar cualquier colección de IEnumerable en paralelo aplicando un delegado de acción a cada elemento
de la colección. Podemos proporcionar varias opciones, personalizar el comportamiento de paralelización
y obtener un resultado que muestre si el bucle se completó correctamente.
Para modificar nuestro ciclo paralelo, proporcionamos una instancia de la clase ParallelOptions al
método ForEach . Esto nos permite cancelar el bucle con CancellationToken, restringir el grado de
paralelismo máximo (cuántas operaciones máximas se pueden ejecutar en paralelo) y proporcionar una
clase TaskScheduler personalizada para programar tareas de acción con ella. Las acciones pueden aceptar
un parámetro ParallelLoopState adicional , que es útil para romper el ciclo o para verificar qué sucede con
el ciclo en este momento.
144
Machine Translated by Google
Capítulo 7
Hay dos formas de detener el ciclo paralelo con este estado. Podríamos usar los métodos Break o Stop .
El método Stop le dice al ciclo que deje de procesar más trabajo y establece la propiedad IsStopped del estado
del ciclo paralelo en verdadero. El método Break detiene las iteraciones posteriores, pero las iniciales
seguirán funcionando. En ese caso, la propiedad LowestBreakIteration del resultado del bucle contendrá
el número de iteraciones de bucle más bajas en las que se llamó al método Break .
Paralelizar una consulta LINQ
Esta receta describirá cómo usar PLINQ para hacer una consulta en paralelo y cómo volver de una consulta en
paralelo al procesamiento secuencial.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter7\Recipe2.
Cómo hacerlo...
Para usar PLINQ para hacer una consulta en paralelo y volver de una consulta en paralelo a un
procesamiento secuencial, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; usando System.Collections.Generic;
utilizando System.Diagnostics;
utilizando System.Linq;
usando System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static void PrintInfo(string typeName) {
Dormir (TimeSpan.FromMillisegundos (150));
WriteLine($"{typeName} tipo se imprimió en un subproceso " $"id +
{CurrentThread.ManagedThreadId}");
}
cadena estática EmulateProcessing (nombre de tipo de cadena) {
Dormir (TimeSpan.FromMillisegundos (150));
145
Machine Translated by Google
Uso de PLINQ
WriteLine($"{typeName} tipo se procesó en un subproceso " $"id +
{CurrentThread.ManagedThreadId}"); devuelve el nombre del
tipo;
}
estático IEnumerable<cadena> GetTypes() {
volver del ensamblado en AppDomain.CurrentDomain.GetAssemblies()
from type in assembly.GetExportedTypes() where
type.Name.StartsWith("Web") select type.Name;
4. Agregue el siguiente fragmento de código dentro del método principal :
var sw = nuevo Cronómetro(); sw.Inicio();
var consulta = de
t en GetTypes() seleccione EmulateProcessing(t);
foreach (cadena typeName en la consulta) {
PrintInfo(tipoNombre);
} sw.Stop();
Línea de escritura("");
WriteLine("Consulta LINQ secuencial."); WriteLine($"Tiempo
transcurrido: {sw.Elapsed}"); WriteLine("Presione ENTER para
continuar...");
LeerLínea();
Claro();
sw.Reset();
sw.Inicio(); var
paraleloQuery = de t en GetTypes().AsParallel()
seleccione EmulateProcessing(t);
foreach (var typeName en paraleloQuery) {
PrintInfo(tipoNombre);
} sw.Stop();
Línea de escritura("");
146
Machine Translated by Google
Capítulo 7
WriteLine("Consulta LINQ paralela. Los resultados se fusionan en un único subproceso"); WriteLine($"Tiempo
transcurrido:
{sw.Elapsed}"); WriteLine("Presione ENTER para continuar...");
LeerLínea(); Claro();
sw.Reset();
sw.Inicio();
ParallelQuery = de t en GetTypes().AsParallel()
seleccione EmulateProcessing(t);
ParallelQuery.ForAll(PrintInfo);
sw.Stop(); Línea
de escritura("");
WriteLine("Consulta LINQ paralela. Los resultados se procesan en paralelo"); WriteLine($"Tiempo transcurrido:
{sw.Elapsed}");
WriteLine("Presione ENTER para continuar...");
LeerLínea();
Claro();
sw.Reset();
sw.Inicio();
consulta = de t en GetTypes().AsParallel().AsSequential()
seleccione EmulateProcessing(t);
foreach (cadena typeName en la consulta) {
PrintInfo(tipoNombre);
}
sw.Stop(); Línea
de escritura("");
WriteLine("Consulta LINQ paralela, transformada en secuencial."); WriteLine($"Tiempo transcurrido:
{sw.Elapsed}"); WriteLine("Presione ENTER para continuar...");
LeerLínea(); Claro();
5. Ejecute el programa.
147
Machine Translated by Google
Uso de PLINQ
Cómo funciona...
Cuando se ejecuta el programa, creamos una consulta LINQ que usa la API de reflexión para obtener todos los tipos
cuyos nombres comienzan con Web de los ensamblados cargados en el dominio de la aplicación actual.
Emulamos retrasos para procesar cada elemento y para imprimirlo con los métodos EmulateProcessing y PrintInfo .
También usamos la clase Stopwatch para medir el tiempo de ejecución de cada consulta.
Primero, ejecutamos una consulta LINQ secuencial habitual. No hay paralelización aquí, por lo que todo se ejecuta
en el subproceso actual. La segunda versión de la consulta usa la clase ParallelEnumerable explícitamente.
ParallelEnumerable contiene la implementación lógica de PLINQ y está organizado como una serie de métodos de
extensión para la funcionalidad de la colección IEnumerable .
Normalmente, no usamos esta clase explícitamente; lo estamos usando aquí para ilustrar cómo funciona realmente
PLINQ. La segunda versión ejecuta EmulateProcessing en paralelo; sin embargo, de forma predeterminada, los
resultados se fusionan en un solo hilo, por lo que el tiempo de ejecución de la consulta debería ser un par de
segundos menos que la primera versión.
La tercera versión muestra cómo usar el método AsParallel para ejecutar la consulta LINQ en paralelo de manera
declarativa. No nos importan los detalles de implementación aquí, solo indicamos que queremos ejecutar esto en paralelo.
Sin embargo, la diferencia clave en esta versión es que usamos el método ForAll para imprimir los resultados de la
consulta. Ejecuta la acción en todos los elementos de la consulta en el mismo subproceso en el que se procesaron,
omitiendo el paso de combinación de resultados. También nos permite ejecutar PrintInfo en paralelo, y esta versión se
ejecuta incluso más rápido que la anterior.
El último ejemplo muestra cómo convertir una consulta PLINQ en secuencial con el método AsSequential . Podemos ver
que esta consulta se ejecuta exactamente igual que la primera.
Ajustando los parámetros de una consulta PLINQ
Esta receta muestra cómo podemos administrar las opciones de procesamiento en paralelo mediante una consulta
PLINQ y qué podrían afectar estas opciones durante la ejecución de una consulta.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter7\Recipe3.
148
Machine Translated by Google
Capítulo 7
Cómo hacerlo...
Para comprender cómo administrar las opciones de procesamiento en paralelo mediante una consulta PLINQ
y sus efectos, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
usando System.Collections.Generic; utilizando
System.Linq; utilizando
System.Threading; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
cadena estática EmulateProcessing (nombre de tipo de cadena) {
Suspender(TimeSpan.FromMilliseconds(
new Random(DateTime.Now.Millisecond).Next(250,350))); WriteLine($"{typeName}
tipo se procesó en un subproceso " $"id {CurrentThread.ManagedThreadId}"); devuelve +
el nombre del tipo;
estático IEnumerable<cadena> GetTypes() {
volver del ensamblado en AppDomain.CurrentDomain.GetAssemblies() del tipo en
ensamblado.GetExportedTypes() where
type.Name.StartsWith("Web") orderby
type.Name.Length select type.Name;
4. Agregue el siguiente fragmento de código dentro del método principal :
var paraleloQuery = de t en GetTypes().AsParallel()
seleccione EmulateProcessing(t);
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(3));
intentar
{
consultaparalela
149
Machine Translated by Google
Uso de PLINQ
.WithDegreeOfParallelism(Environment.ProcessorCount)
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
.WithMergeOptions(ParallelMergeOptions.Predeterminado)
.ConCancelación(cts.Token)
.ParaTodos(EscribirLínea);
} captura (Excepción Cancelada por Operación) {
Línea de escritura("");
WriteLine("¡La operación ha sido cancelada!");
}
Línea de escritura("");
WriteLine("Ejecución de consulta PLINQ desordenada"); var unorderedQuery
= from i in ParallelEnumerable.Range(1, 30) select i;
foreach (var i en consulta desordenada) {
WriteLine(i);
}
Línea de escritura("");
WriteLine("Ejecución de consulta PLINQ ordenada"); var orderQuery =
de i en ParallelEnumerable.Range(1, 30).
Según lo ordenado
() seleccione i;
foreach (var i en la consulta ordenada) {
WriteLine(i);
}
5. Ejecute el programa.
Cómo funciona...
El programa demuestra diferentes opciones útiles de PLINQ que los programadores pueden usar. Comenzamos
con la creación de una consulta PLINQ y luego creamos otra consulta que proporciona ajustes de PLINQ.
Comencemos con la cancelación primero. Para poder cancelar una consulta PLINQ, existe
un método WithCancellation que acepta un objeto token de cancelación. Aquí, señalamos el token de
cancelación después de 3 segundos, lo que lleva a OperationCanceledException en la consulta y
cancelación del resto del trabajo.
150
Machine Translated by Google
Capítulo 7
Luego, podemos especificar un grado de paralelismo para la consulta. Es el número exacto de particiones
paralelas que se utilizarán para ejecutar la consulta. En la primera receta, usamos el bucle Parallel.ForEach ,
que tiene la opción de grado máximo de paralelismo. Es diferente porque especifica un valor máximo de
particiones, pero podría haber menos particiones si la infraestructura decide que es mejor usar menos
paralelismo para ahorrar recursos y lograr un rendimiento óptimo.
Otra opción interesante es anular el modo de ejecución de consultas con el método
WithExecutionMode . La infraestructura de PLINQ puede procesar algunas consultas en modo secuencial
si decide que paralelizar la consulta solo agregará más sobrecarga y en realidad se ejecutará más lentamente.
Usando WithExecutionMode, podemos forzar que la consulta se ejecute en paralelo.
Para afinar el procesamiento de resultados de consultas, tenemos el método WithMergeOptions . El modo
predeterminado se utiliza para almacenar en búfer una serie de resultados seleccionados por la
infraestructura de PLINQ antes de devolverlos de la consulta. Si la consulta lleva mucho tiempo, es más
razonable desactivar el almacenamiento en búfer de resultados para obtener los resultados lo antes posible.
La última opción es el método AsOrdered . Es posible que cuando usamos la ejecución en paralelo, el orden de
los elementos en la colección no se conserve. Los elementos posteriores de la colección podrían
procesarse antes que los anteriores. Para evitar esto, debemos llamar a AsOrdered en una consulta paralela
para decirle explícitamente a la infraestructura de PLINQ que pretendemos conservar el orden de los
artículos para su procesamiento.
Manejo de excepciones en una consulta PLINQ
Esta receta describirá cómo manejar las excepciones en una consulta PLINQ.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter7\Recipe4.
Cómo hacerlo...
Para comprender cómo manejar las excepciones en una consulta PLINQ, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; usando System.Collections.Generic;
utilizando System.Linq;
usando System.Console estático;
151
Machine Translated by Google
Uso de PLINQ
3. Agregue el siguiente fragmento de código dentro del método principal :
IEnumerable<int> números = Enumerable.Range(5, 10);
var consulta = de número en números seleccione 100 /
número;
intentar
foreach(var n en la consulta)
WriteLine(n);
} atrapar (DivideByZeroException) {
WriteLine("¡Dividido por cero!");
}
Línea de escritura("");
WriteLine("Procesamiento secuencial de consultas LINQ");
Línea de escritura();
var paraleloQuery = de número en números.AsParallel() seleccione 100 / número;
intentar
ParallelQuery.ForAll(WriteLine);
} atrapar (DivideByZeroException) {
WriteLine("Dividido por cero controlador de excepciones habitual!");
} captura (Excepción agregada e) {
e.Flatten().Handle(ex => {
si (ex es DivideByZeroException) {
WriteLine("Dividido por cero controlador de excepción agregado!");
devolver verdadero;
}
152
Machine Translated by Google
Capítulo 7
falso retorno; });
Línea de escritura("");
WriteLine("Procesamiento de consultas LINQ en paralelo y combinación de resultados");
4. Ejecute el programa.
Cómo funciona...
Primero, ejecutamos una consulta LINQ habitual sobre un rango de números de 5 a 4. Cuando dividimos por 0,
obtenemos DivideByZeroException y lo manejamos como de costumbre en un bloque try/catch .
Sin embargo, cuando usamos AsParallel, obtenemos AggregateException porque ahora estamos ejecutando
en paralelo, aprovechando la infraestructura de tareas detrás de escena.
AggregateException contendrá todas las excepciones que ocurrieron mientras se ejecutaba la consulta
PLINQ. Para manejar la clase interna DivideByZeroException , usamos los métodos Flatten y Handle , que se
explicaron en la receta Manejo de excepciones en operaciones asincrónicas en el Capítulo 5, Uso de C# 6.0.
Es muy fácil olvidar que cuando manejamos excepciones agregadas, tener
más de una excepción interna dentro es una situación muy común. Si
olvida manejarlos todos, la excepción aparecerá y la aplicación dejará de
funcionar.
Administrar la partición de datos en una consulta PLINQ
Esta receta le muestra cómo crear una estrategia de partición personalizada muy básica para paralelizar una
consulta LINQ de una manera específica.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter7\Recipe5.
153
Machine Translated by Google
Uso de PLINQ
Cómo hacerlo...
Para aprender a crear una estrategia de partición personalizada muy básica para paralelizar una consulta
LINQ, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Collections.Concurrent; usando
System.Collections.Generic; utilizando
System.Diagnostics; utilizando System.Linq;
usando System.Console
estático; usando System.Threading.Thread
estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
static void PrintInfo(string typeName) {
Dormir (TimeSpan.FromMillisegundos (150));
WriteLine($"{typeName} tipo se imprimió en un subproceso " $"id +
{CurrentThread.ManagedThreadId}"); }
cadena estática EmulateProcessing (nombre de tipo de cadena) {
Dormir (TimeSpan.FromMillisegundos (150)); El tipo
+
WriteLine($"{typeName} se procesó en un subproceso " $"id { CurrentThread.ManagedThreadId}.
Tiene " $"{(typeName.Length % 2 == 0 ? "par" : "odd")} de longitud". ); +
devuelve el nombre del tipo;
}
estático IEnumerable<cadena> GetTypes() {
var tipos = AppDomain.CurrentDomain
.GetAssemblies()
.SelectMany(a => a.GetExportedTypes());
volver de tipo en tipos donde
tipo.Nombre.ComienzaCon("Web") select tipo.Nombre;
154
Machine Translated by Google
Capítulo 7
public class StringPartitioner : Partitioner<string> {
privado de solo lectura IEnumerable<string> _data;
public StringPartitioner(IEnumerable<cadena> datos) {
_datos = datos;
}
public override bool SupportsDynamicPartitions => false;
public override IList<IEnumerator<string>>GetPartitions(
número de partición int)
{
var result = new List<IEnumerator<string>>(partitionCount);
for (int i = 1; i <= número de particiones; i++) {
resultado. Agregar (Crear Enumerador (i, conteo de particiones));
}
resultado devuelto;
}
IEnumerator<cadena> CreateEnumerator(int número de partición, int
recuento de particiones)
{
int particiones pares = número de particiones / 2; bool es par =
número de partición % 2 == 0; int paso = es par? particiones pares:
conteo de particiones particiones pares;
int startIndex = número de partición / 2 + número de partición %
2;
var q = _datos .Dónde(v
^
=> !(v.Longitud % 2 == 0 || recuento de particiones incluso)
== 1)
.Saltar(índiceInicio 1);
155
Machine Translated by Google
Uso de PLINQ
return
q .Dónde((x, i) => i % paso == 0)
.ObtenerEnumerador();
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
var timer = Cronómetro.StartNew(); var particionador
= new StringPartitioner(GetTypes()); var paraleloQuery = de t en el
particionador.AsParallel() //
.ConGradoDeParalelismo(1)
seleccione EmulateProcessing(t);
ParallelQuery.ForAll(PrintInfo); int cuenta =
paraleloConsulta.Cuenta(); temporizador.Stop(); Línea
de escritura("
"); WriteLine($"Total de artículos procesados:
{count}"); WriteLine($"Tiempo transcurrido: {temporizador.Transcurrido}");
5. Ejecute el programa.
Cómo funciona...
Para ilustrar que podemos elegir estrategias de partición personalizadas para la consulta PLINQ, creamos un
particionador muy simple que procesa cadenas de longitudes pares e impares en paralelo. Para lograr esto,
derivamos nuestra clase StringPartitioner personalizada de una clase base estándar Partitioner<T> usando
string como parámetro de tipo.
Declaramos que solo admitimos particiones estáticas anulando la propiedad
SupportsDynamicPartitions y estableciéndola en false. Esto significa que predefinimos nuestra estrategia
de partición. Esta es una manera fácil de particionar la colección inicial, pero podría ser ineficiente según los
datos que tengamos dentro de la colección. Por ejemplo, en nuestro caso, si tuviéramos muchas cadenas con
longitudes impares y solo una cadena con longitudes pares, uno de los subprocesos habría terminado antes y no
habría ayudado a procesar cadenas de longitudes impares.
Por otro lado, la partición dinámica significa que particionamos la colección inicial sobre la marcha, equilibrando
la carga de trabajo entre los subprocesos de trabajo.
Luego, implementamos el método GetPartitions , donde definimos la siguiente lógica: si solo hay una
partición, simplemente procesamos todo en ella. Sin embargo, si tenemos más de una partición, procesamos
cadenas de longitud impar en particiones impares y cadenas de longitud par en particiones pares.
156
Machine Translated by Google
Capítulo 7
Tenga en cuenta que necesitamos crear tantas particiones como se indica
en el parámetro PartitionCount, o de lo contrario, el particionador
devolverá un número incorrecto de error de particiones.
Finalmente, creamos una instancia de nuestro particionador y realizamos una consulta PLINQ con él. Podemos ver
que diferentes hilos procesan las cadenas de longitud par e impar. Además, podemos experimentar descomentando el
método WithDegreeOfParallelism y cambiando el valor de su parámetro. En el caso de 1, habrá un procesamiento
secuencial de elementos de trabajo, y al aumentar el valor, podemos ver que se realiza más trabajo en paralelo.
Creación de un agregador personalizado para
una consulta PLINQ
Esta receta le muestra cómo crear una función de agregación personalizada para una consulta PLINQ.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter7\Recipe6.
Cómo hacerlo...
Para comprender el funcionamiento de una función de agregación personalizada para una consulta PLINQ, realice
los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Collections.Concurrent; usando
System.Collections.Generic; utilizando System.Linq;
usando System.Console
estático; usando System.Threading.Thread
estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
diccionario concurrente estático<char, int>
AccumulateLettersInformation(
ConcurrentDictionary<char, int> taskTotal , elemento de cadena)
{
foreach (var c en el artículo)
157
Machine Translated by Google
Uso de PLINQ
{
if (totaltarea.ContainsKey(c)) {
tareaTotal[c] = tareaTotal[c] + 1;
} demás
{
tareaTotal[c] = 1;
}
}
WriteLine($"El tipo {item} se agregó en un hilo" +
$"id {CurrentThread.ManagedThreadId}"); volver tareaTotal;
static ConcurrentDictionary<char, int> MergeAccumulators(
ConcurrentDictionary<char, int> total,
Diccionario Concurrente<char, int> taskTotal) {
foreach (clave var en taskTotal.Keys) {
if (total.ContainsKey(clave)) {
total[clave] = total[clave] + taskTotal[clave];
}
demás
{
total[clave] = taskTotal[clave];
}
}
Línea de escritura("");
WriteLine($"El valor agregado total se calculó en un hilo" $"id {CurrentThread.ManagedThreadId}"); +
devolución total;
}
estático IEnumerable<cadena> GetTypes() {
var tipos = AppDomain.CurrentDomain .GetAssemblies()
.SelectMany(a => a.GetExportedTypes());
retorno de tipo en tipos
donde tipo.Nombre.ComienzaCon("Web") select
tipo.Nombre;
}
158
Machine Translated by Google
Capítulo 7
4. Agregue el siguiente fragmento de código dentro del método principal :
var paraleloQuery = de t en GetTypes().AsParallel()
seleccionar t;
var parallelAggregator = parallelQuery.Aggregate( () => new
ConcurrentDictionary<char, int>(), (taskTotal, item) =>
AccumulateLettersInformation(taskTotal, item), (total, taskTotal) => MergeAccumulators(total,
taskTotal), total => totales);
Línea de
escritura(); WriteLine("Había las siguientes letras en los nombres de tipo:"); varorderedKeys
= de k en paraleloAggregator.Keys
ordenar por paraleloAggregator[k] descendente seleccionar
k;
foreach (var c en claves ordenadas) {
WriteLine($"Letra '{c}' {parallelAggregator[c]} veces");
}
5. Ejecute el programa.
Cómo funciona...
Aquí, implementamos mecanismos de agregación personalizados que pueden funcionar con las
consultas PLINQ. Para implementar esto, debemos comprender que, dado que varias tareas procesan
una consulta en paralelo, debemos proporcionar mecanismos para agregar el resultado de cada tarea en
paralelo y luego combinar esos valores agregados en un solo valor de resultado.
En esta receta, escribimos una función de agregación que cuenta letras en una consulta PLINQ, que devuelve
la colección IEnumerable<string> . Cuenta todas las letras de cada elemento de la colección. Para ilustrar el
proceso de agregación en paralelo, imprimimos información sobre qué hilo procesa cada parte de la
agregación.
Agregamos los resultados de la consulta PLINQ mediante el método de extensión Aggregate definido en la clase
ParallelEnumerable . Acepta cuatro parámetros, cada uno de los cuales es una función que realiza diferentes
partes del proceso de agregación. El primero es una fábrica que construye el valor inicial vacío del agregador.
También se le llama valor inicial.
159
Machine Translated by Google
Uso de PLINQ
Tenga en cuenta que el primer valor proporcionado al método Aggregate en realidad no
es un valor semilla inicial para la función de agregación, sino un método de fábrica
que construye este valor semilla inicial. Si proporciona solo una instancia, se usará
en todas las particiones que se ejecuten en paralelo, lo que conducirá a un resultado incorrecto.
La segunda función agrega cada elemento de la colección en el objeto de agregación de partición.
Implementamos esta función con el método AccumulateLettersInformation . Itera la cadena y cuenta las letras
dentro de ella. Aquí, los objetos de agregación son diferentes para cada partición de consulta que se ejecuta en
paralelo, razón por la cual los llamamos taskTotal.
La tercera función es una función de agregación de nivel superior que toma un objeto agregador de una
partición y lo fusiona en un objeto agregador global. Lo implementamos con el método MergeAccumulators .
La última función es una función selectora que especifica qué datos exactos necesitamos del objeto
agregador global.
Finalmente, imprimimos el resultado de la agregación, ordenándolo por las letras más utilizadas en los
elementos de la colección.
160
Machine Translated by Google
Extensiones reactivas
8
En este capítulo, veremos otra biblioteca .NET interesante que nos ayuda a crear programas asincrónicos,
Reactive Extensions (Rx). Cubriremos las siguientes recetas:
f Convertir una colección en Observable asíncrono
f Escribiendo un Observable
personalizado f Usando el tipo
de Sujeto f Creando un objeto Observable
f Uso de consultas LINQ contra una colección Observable f
Creación de operaciones asincrónicas con Rx
Introducción
Como ya aprendió, existen varios enfoques para crear programas asincrónicos en .NET y C#. Uno de ellos
es el patrón asíncrono basado en eventos, que ya se ha mencionado en los capítulos anteriores. El
objetivo inicial de introducir eventos era simplificar la implementación del patrón de diseño Observer . Este
patrón es común para implementar notificaciones entre objetos.
Cuando discutimos la biblioteca paralela de tareas, notamos que la principal deficiencia del evento era su
incapacidad para estar integrados de manera efectiva entre sí. El otro inconveniente era que no se
suponía que el patrón asincrónico basado en eventos se usara para tratar la secuencia de notificaciones.
Imagina que tenemos IEnumerable<string> que nos da valores de cadena.
Sin embargo, cuando lo iteramos, no sabemos cuánto tiempo tomará una iteración. Podría ser lento, y
si usamos el bucle foreach normal u otras construcciones de iteración síncrona, bloquearemos nuestro
subproceso hasta que tengamos el siguiente valor. Esta situación se denomina enfoque basado en
extracción , cuando nosotros, como clientes, extraemos valores del productor.
161
Machine Translated by Google
Extensiones reactivas
El enfoque opuesto es el enfoque basado en push , cuando el productor notifica al cliente sobre
nuevos valores. Esto permite descargar trabajo al productor, mientras que el cliente es libre de
hacer cualquier otra cosa en el tiempo que espera por otro valor. Por lo tanto, el objetivo es obtener
algo como la versión asincrónica de IEnumerable, que produce una secuencia de valores y notifica
al consumidor sobre cada elemento de la secuencia, cuando la secuencia está completa o cuando
se genera una excepción.
.NET Framework a partir de la versión 4.0 contiene la definición de las interfaces IObservable<out T>
e IObserver<in T> que juntas representan la colección asíncrona basada en inserción y su cliente.
Vienen de la biblioteca llamada Reactive Extensions (o simplemente Rx) que se creó dentro de
Microsoft para ayudarnos a componer de manera efectiva la secuencia de eventos y todos los demás
tipos de programas asíncronos usando colecciones observables. Las interfaces se incluyeron
en .NET Framework, pero sus implementaciones y todos los demás mecanismos aún se distribuyen
por separado en la biblioteca Rx.
Rx globalmente es una biblioteca multiplataforma. Hay bibliotecas
para .NET 3.5, Silverlight y Windows Phone. También está disponible
en JavaScript, Ruby y Python. También es de código abierto; puede
encontrar el código fuente de Reactive Extensions para .NET en el sitio web
de CodePlex y otras implementaciones en GitHub.
Lo más sorprendente es que las colecciones observables son compatibles con LINQ y, por lo
tanto, podemos usar consultas declarativas para transformar y componer esas colecciones de manera
asincrónica. Esto también nos permite usar los métodos de extensión para agregar funcionalidades a
los programas Rx de la misma manera que se usa en los proveedores LINQ habituales.
Reactive Extensions también admite la transición de todos los patrones de programación asíncrona
(incluido el modelo de programación asíncrona, el patrón asíncrono basado en eventos y la
biblioteca paralela de tareas) a colecciones observables, y admite su propia forma de ejecutar
operaciones asíncronas, que sigue siendo bastante similar a TPL.
La biblioteca Reactive Extensions es un instrumento muy potente y complejo, que merece la pena
escribir un libro aparte. En este capítulo, me gustaría revisar el escenario más útil, es decir, cómo
trabajar con secuencias de eventos asincrónicos de manera efectiva. Observaremos los tipos clave del
marco de Reactive Extensions, aprenderemos a crear secuencias y manipularlas de diferentes
maneras y, finalmente, comprobaremos cómo podemos usar Reactive Extensions para ejecutar
operaciones asíncronas y administrar sus opciones.
Convertir una colección en una asíncrona
Observable
Esta receta lo guía a través del proceso de creación de una colección observable a partir de
una clase Enumerable y cómo procesarla de forma asíncrona.
162
Machine Translated by Google
Capítulo 8
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No se requieren otros requisitos previos. El
código fuente de esta receta se puede encontrar en BookSamples\Chapter8\Recipe1.
Cómo hacerlo...
Para comprender cómo crear una colección observable a partir de una clase Enumerable y procesarla de
forma asincrónica, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. Agregue una referencia al paquete NuGet de la biblioteca principal de Reactive Extensions siguiendo
estos pasos:
1. Haga clic con el botón derecho en la carpeta Referencias del proyecto y seleccione Administrar
Opción de menú Paquetes NuGet… .
2. Ahora, agregue el paquete NuGet Extensiones reactivas Biblioteca principal . Puede buscar
rxmain en el cuadro de diálogo Administrar paquetes NuGet , como se muestra en la siguiente
captura de pantalla:
163
Machine Translated by Google
Extensiones reactivas
3. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
usando System.Collections.Generic; usando
System.Reactive.Concurrency; utilizando
System.Reactive.Linq; utilizando
System.Threading; usando
System.Console estático; usando
System.Threading.Thread estático;
4. Agregue el siguiente fragmento de código debajo del método principal :
estático IEnumerable<int> EnumerableEventSequence() {
para (int i = 0; i < 10; i++) {
Dormir (TimeSpan.FromSeconds (0.5)); rendimiento
retorno i;
}
}
5. Agregue el siguiente fragmento de código dentro del método principal :
foreach (int i en EnumerableEventSequence()) {
escribir(yo);
}
Línea de escritura();
WriteLine("IEnumerable");
IObservable<int> o = EnumerableEventSequence().().ToObservable(); usando (suscripción IDisposable =
o.Subscribe(Write)) {
Línea de escritura();
WriteLine("IObservable");
}
o = EnumerableEventSequence().ToObservable()
.SubscribeOn(TaskPoolScheduler.Default);
usando (suscripción IDisposable = o.Subscribe(Write)) {
Línea de escritura();
WriteLine("IObservable asíncrono");
LeerLínea();
}
6. Ejecute el programa.
164
Machine Translated by Google
Capítulo 8
Cómo funciona...
Aquí, simulamos una colección enumerable lenta con el método EnumerableEventSequence . Luego, lo
iteramos con el ciclo foreach habitual y podemos ver que en realidad es lento; esperamos a que se complete
cada iteración.
Luego, convertimos esta colección enumerable en Observable con la ayuda del método de extensión
ToObservable de la biblioteca Reactive Extensions. A continuación, nos suscribimos a las actualizaciones
de esta colección observable, proporcionando el método Console.Write como acción, que se ejecutará en cada
actualización de la colección. Como resultado, obtenemos exactamente el mismo comportamiento que
antes; esperamos a que se complete cada iteración porque usamos el hilo principal para suscribirnos a las
actualizaciones.
Envolvemos los objetos de suscripción en instrucciones de uso. Aunque no siempre
es necesario, deshacerse de las suscripciones es una buena práctica que lo ayudará a
evitar errores relacionados con la vida útil.
Para hacer que el programa sea asíncrono, usamos el método SubscribeOn , proporcionándole el
programador de grupo de tareas TPL. Este planificador colocará la suscripción en el grupo de tareas de TPL,
descargando el trabajo del subproceso principal. Esto nos permite mantener la interfaz de usuario receptiva
y hacer algo más mientras se actualiza la colección. Para verificar este comportamiento, puede eliminar la
última llamada de Console.ReadLine del código. Al hacerlo, finalizamos nuestro subproceso principal de
inmediato, lo que obliga a todos los subprocesos en segundo plano (incluidos los subprocesos de trabajo del
grupo de tareas TPL) a finalizar también, y no obtendremos ningún resultado de la colección asíncrona.
Si usamos un marco de interfaz de usuario, debemos interactuar con los controles de interfaz de usuario
solo desde el subproceso de interfaz de usuario. Para lograr esto, debemos usar el método ObserveOn
con el planificador correspondiente. Para Windows Presentation Foundation, tenemos la clase
DispatcherScheduler y el método de extensión ObserveOnDispatcher definidos en un paquete NuGet
independiente denominado RxXAML o biblioteca de compatibilidad con XAML de extensiones reactivas.
Para otras plataformas, también existen paquetes NuGet independientes correspondientes.
Escribiendo un Observable personalizado
Esta receta describirá cómo implementar las interfaces IObservable<in T> e IObserver<out T> para obtener la
secuencia observable personalizada y consumirla correctamente.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No se requieren otros requisitos previos. El
código fuente de esta receta se puede encontrar en BookSamples\Chapter8\ Recipe2.
165
Machine Translated by Google
Extensiones reactivas
Cómo hacerlo...
Para comprender cómo implementar las interfaces IObservable<in T> e IObserver<out T> para obtener
la secuencia observable personalizada y consumirla, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. Agregue una referencia al paquete NuGet de la biblioteca principal de Reactive Extensions . Consulte la
receta Convertir una colección en observable asincrónica para obtener más detalles sobre cómo
hacerlo.
3. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; usando System.Collections.Generic;
usando System.Reactive.Concurrency; usando
System.Reactive.Disposables; utilizando
System.Reactive.Linq; usando
System.Console estático; usando
System.Threading.Thread estático;
4. Agregue el siguiente fragmento de código debajo del método principal :
clase CustomObserver : IObserver<int>
{
public void OnNext(valor int) {
WriteLine($"Siguiente valor: {valor}; Id. de subproceso: {CurrentThread.
ManagedThreadId}");
}
public void OnError (Error de excepción) {
WriteLine($"Error: {error.Mensaje}");
}
public void OnCompleted() {
WriteLine("Completado");
}
}
clase CustomSequence: IObservable<int> {
privado de solo lectura IEnumerable<int> _numbers;
166
Machine Translated by Google
Capítulo 8
public CustomSequence(IEnumerable<int> números) {
_numeros = numeros;
} public IDisposable Subscribe(IObserver<int> observador) {
foreach (var número en _numbers) {
observador.OnNext(número);
} observador.OnCompleted(); volver
Desechable.Vacío;
}
}
5. Agregue el siguiente fragmento de código dentro del método principal :
var observador = new CustomObserver();
var goodObservable = new CustomSequence(new[] {1, 2, 3, 4, 5}); var badObservable = new
CustomSequence(null);
utilizando (suscripción IDisposable = goodObservable.
Suscribirse (observador)) { }
usando (suscripción IDisposable = goodObservable
.SubscribeOn(TaskPoolScheduler.Default).Subscribe(observador))
{
Dormir (TimeSpan.FromMillisegundos (100)); WriteLine("Presione
ENTER para continuar");
LeerLínea();
}
usando (suscripción IDisposable = badObservable
.SubscribeOn(TaskPoolScheduler.Default).Subscribe(observador))
{
Dormir (TimeSpan.FromMillisegundos (100)); WriteLine("Presione
ENTER para continuar");
LeerLínea();
}
6. Ejecute el programa.
167
Machine Translated by Google
Extensiones reactivas
Cómo funciona...
Aquí, primero implementamos nuestro observador simplemente imprimiendo en la consola la información
sobre el siguiente elemento de la colección observable, el error o la finalización de la secuencia. Este es
un código de consumidor muy simple y no tiene nada de especial.
La parte interesante es nuestra implementación de colección observable. Aceptamos una enumeración de
números en un constructor y no verificamos si es nulo a propósito. Cuando tenemos un observador suscriptor,
iteramos esta colección y notificamos al observador sobre cada elemento de la enumeración.
Luego, demostramos la suscripción real. Como podemos ver, la asincronía se logra llamando al método
SubscribeOn , que es un método de extensión de IObservable y contiene lógica de suscripción asíncrona.
No nos importa la asincronía en nuestra colección observable; usamos la implementación estándar de la
biblioteca Reactive Extensions.
Cuando nos suscribimos a la colección observable normal, solo obtenemos todos los elementos de ella.
Ahora es asíncrono, por lo que debemos esperar un tiempo para que se complete la operación asíncrona
y solo luego imprimir el mensaje y esperar la entrada del usuario.
Finalmente, intentamos suscribirnos a la siguiente colección observable, donde iteramos una enumeración
nula y, por lo tanto, obtenemos una excepción de referencia nula. Vemos que la excepción se manejó
correctamente y se ejecutó el método OnError para imprimir los detalles del error.
Uso de la familia de tipos de asunto
Esta receta le muestra cómo usar la familia de tipos de asunto de la biblioteca de extensiones reactivas.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No se requieren otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter8\Recipe3.
Cómo hacerlo...
Para comprender el uso de la familia de tipos de asunto de la biblioteca de extensiones reactivas,
realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. Agregue una referencia al paquete NuGet de la biblioteca principal de Reactive Extensions .
Consulte la receta Convertir una colección en observable asincrónica para obtener detalles sobre
cómo hacerlo.
168
Machine Translated by Google
Capítulo 8
3. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
usando System.Reactive.Subjects; usando
System.Console estático; usando
System.Threading.Thread estático;
4. Agregue el siguiente fragmento de código debajo del método principal :
static IDisposable OutputToConsole<T>(IObservable<T> secuencia) {
secuencia de retorno.Subscribe(
obj => WriteLine($"{obj}")
, ex => WriteLine($"Error: {ex.Message}")
, () => WriteLine("Completado")
);
}
5. Agregue el siguiente fragmento de código dentro del método principal :
WriteLine("Asunto"); var asunto =
nuevo Asunto<cadena>();
asunto.OnNext("A"); usando
(var suscripción = OutputToConsole(asunto)) {
asunto.OnNext("B");
asunto.OnNext("C");
asunto.OnNext("D");
asunto.OnCompleted();
subject.OnNext("No se imprimirá");
}
WriteLine("ReproducirAsunto"); var
ReplaySubject = new ReplaySubject<cadena>();
reproducirAsunto.OnNext("A"); usando
(var suscripción = OutputToConsole(replaySubject)) {
reproducirAsunto.OnNext("B");
reproducirAsunto.OnNext("C");
reproducirAsunto.OnNext("D");
reproducirSubject.OnCompleted();
}
169
Machine Translated by Google
Extensiones reactivas
WriteLine("Asunto de reproducción en búfer"); var
bufferedSubject = new ReplaySubject<cadena>(2);
BufferedSubject.OnNext("A");
bufferedSubject.OnNext("B");
BufferedSubject.OnNext("C"); usando (var
suscripción = OutputToConsole(bufferedSubject)) {
bufferedSubject.OnNext("D");
bufferedSubject.OnCompleted();
}
WriteLine("Ventana de tiempo ReplaySubject"); var timeSubject
= new ReplaySubject<string>(TimeSpan.
DesdeMilisegundos(200));
tiempoAsunto.OnNext("A"); Dormir
(TimeSpan.FromMillisegundos (100)); tiempoAsunto.OnNext("B");
Dormir (TimeSpan.FromMillisegundos
(100)); tiempoAsunto.OnNext("C"); Dormir
(TimeSpan.FromMillisegundos (100));
usando (var suscripción = OutputToConsole(timeSubject)) {
Dormir (TimeSpan.FromMillisegundos (300));
tiempoAsunto.OnNext("D");
timeSubject.OnCompleted();
}
WriteLine("AsuntoAsíncrono"); var
asyncSubject = new AsyncSubject<cadena>();
asyncSubject.OnNext("A"); usando (var
suscripción = OutputToConsole(asyncSubject)) {
asyncSubject.OnNext("B");
asyncSubject.OnNext("C");
asyncSubject.OnNext("D");
asyncSubject.OnCompleted();
}
WriteLine("ComportamientoAsunto"); var
BehaviorSubject = new BehaviorSubject<cadena>("Predeterminado"); usando (var suscripción =
OutputToConsole(behaviorSubject)) {
comportamientoAsunto.OnNext("B");
170
Machine Translated by Google
Capítulo 8
comportamientoAsunto.OnNext("C");
comportamientoAsunto.OnNext("D");
comportamientoAsunto.OnCompleted();
}
6. Ejecute el programa.
Cómo funciona...
En este programa, analizamos las diferentes variantes de la familia de tipos Sujeto . El tipo de asunto representa
las implementaciones de IObservable y IObserver . Esto es útil en diferentes escenarios de proxy cuando
queremos traducir eventos de múltiples fuentes a una transmisión, o viceversa, para transmitir una secuencia
de eventos a múltiples suscriptores. Los sujetos también son muy convenientes para experimentar con las
extensiones reactivas.
Comencemos con el tipo de asunto básico . Vuelve a traducir una secuencia de eventos a los suscriptores
tan pronto como se suscriben. En nuestro caso, la cadena A no se imprimirá porque la suscripción se produjo
después de que se transmitiera. Además de eso, cuando llamamos a los métodos OnCompleted o OnError en
Observable, detiene la traducción adicional de la secuencia de eventos, por lo que la última cadena tampoco
se imprimirá.
El siguiente tipo, ReplaySubject, es bastante flexible y nos permite implementar tres escenarios adicionales.
Primero, puede almacenar en caché todos los eventos desde el comienzo de su transmisión, y si nos
suscribimos más tarde, obtendremos todos los eventos anteriores primero. Este comportamiento se ilustra en
el segundo ejemplo. Aquí, tendremos las cuatro cadenas en la consola porque el primer evento se almacenará
en caché y se traducirá al último suscriptor.
Luego, podemos especificar el tamaño del búfer y el tamaño de la ventana de tiempo para ReplaySubject. En
el siguiente ejemplo, configuramos el sujeto para que tenga un búfer para dos eventos. Si se transmiten
más eventos, solo los dos últimos se volverán a traducir al suscriptor. Así que aquí no veremos la primera
cadena porque tenemos B y C en el búfer de asunto cuando nos suscribimos. Lo mismo ocurre con una ventana
de tiempo. Podemos especificar que el tipo Asunto solo almacene en caché los eventos que tuvieron lugar
hace menos de un tiempo determinado, descartando los más antiguos. Por lo tanto, en el cuarto ejemplo, solo
veremos los dos últimos eventos; los eventos más antiguos no encajan en la ventana de tiempo.
El tipo AsyncSubject es algo así como un tipo de tarea de TPL globalmente. Representa una única operación
asíncrona. Si hay varios eventos publicados, espera a que se complete la secuencia de eventos y proporciona
solo el último evento al suscriptor.
El tipo BehaviorSubject es bastante similar al tipo ReplaySubject , pero almacena en caché solo un valor y nos
permite especificar un valor predeterminado en caso de que no enviemos ninguna notificación. En nuestro último
ejemplo, veremos todas las cadenas impresas porque proporcionamos un valor predeterminado y todos los
demás eventos tienen lugar después de la suscripción. Si movemos el comportamientoSubject.
En Siguiente("B"); hacia arriba debajo del evento predeterminado , reemplazará el valor predeterminado en
la salida.
171
Machine Translated by Google
Extensiones reactivas
Crear un objeto observable
Esta receta describirá diferentes formas de crear un objeto Observable .
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No se requieren otros requisitos previos. El
código fuente de esta receta se puede encontrar en BookSamples\Chapter8\ Recipe4.
Cómo hacerlo...
Para comprender las diferentes formas de crear un objeto Observable , realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. Agregue una referencia al paquete NuGet de la biblioteca principal de Reactive Extensions . Consulte la
receta Conversión de una colección en Observable asíncrono para obtener detalles sobre cómo
hacerlo.
3. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
usando System.Reactive.Disposables; utilizando
System.Reactive.Linq; usando
System.Console estático; usando
System.Threading.Thread estático;
4. Agregue el siguiente fragmento de código debajo del método principal :
static IDisposable OutputToConsole<T>(IObservable<T> secuencia) {
secuencia de retorno.Subscribe(
obj => WriteLine("{0}", obj)
, ex => WriteLine("Error: {0}", ej.Mensaje)
, () => WriteLine("Completado")
);
}
5. Agregue el siguiente fragmento de código dentro del método principal :
IObservable<int> o = Observable.Return(0);
usando (var sub = OutputToConsole(o));
Línea de escritura(" ");
o = Observable.Empty<int>(); usando (var
sub = OutputToConsole(o));
172
Machine Translated by Google
Capítulo 8
Línea de escritura(" ");
o = Observable.Throw<int>(nueva Excepción()); usando (var sub =
OutputToConsole(o)); Línea de escritura(" ");
o = Observable.Repetir(42); usando (var
sub = OutputToConsole(o.Take(5))); Línea de escritura(" ");
o = Observable.Rango(0, 10); usando (var
sub = OutputToConsole(o)); Línea de escritura("
");
o = Observable.Create<int>(ob => { for (int i = 0; i < 10;
i++) {
ob.OnNext(i);
} return Desechable.Vacío; }); usando
(var
sub = OutputToConsole(o)) ; Línea de escritura("
");
o = Observable.Generate( 0 // estado
inicial
, i => i < 5 // mientras esto sea cierto continuamos la secuencia i => ++i // iteración i => i*2 //
, seleccionando el resultado
,
);
usando (var sub = OutputToConsole(o));
Línea de escritura(" ");
IObservable<long> ol = Observable.Interval(TimeSpan.
DeSegundos(1)); usando
(var sub = OutputToConsole(ol)) {
Dormir (TimeSpan.FromSeconds (3)); }; Línea de
escritura(" ");
ol = Observable.Timer(DateTimeOffset.Now.AddSeconds(2)); usando (var sub =
OutputToConsole(ol)) {
Dormir (TimeSpan.FromSeconds (3)); }; Línea de
escritura(" ");
6. Ejecute el programa.
173
Machine Translated by Google
Extensiones reactivas
Cómo funciona...
Aquí, recorremos diferentes escenarios de creación de objetos observables . La mayor parte de esta
funcionalidad se proporciona como métodos de fábrica estáticos del tipo Observable . Los primeros dos
ejemplos muestran cómo podemos crear un método Observable que produzca un solo valor y uno que no
produzca ningún valor. En el siguiente ejemplo, usamos Observable.Throw para construir una clase
Observable que activa el controlador OnError de sus observadores.
El método Observable.Repeat representa una secuencia sin fin. Hay diferentes sobrecargas de este
método; aquí, construimos una secuencia sin fin repitiendo 42 valores.
Luego, usamos el método Take de LINQ para tomar cinco elementos de esta secuencia. Observable.
Range representa un rango de valores, muy parecido a Enumerable.Range.
El método Observable.Create admite más escenarios personalizados. Hay muchas sobrecargas que
nos permiten usar tokens de cancelación y tareas, pero veamos la más simple. Acepta una función, que acepta
una instancia de observador y devuelve un objeto IDisposable que representa una suscripción. Si tuviéramos
algún recurso para limpiar, podríamos proporcionar la lógica de limpieza aquí, pero solo devolvemos un
desechable vacío ya que en realidad no lo necesitamos.
El método Observable.Generate es otra forma de crear una secuencia personalizada. Debemos proporcionar
un valor inicial para una secuencia y luego un predicado que determina si debemos generar más
elementos o completar la secuencia. Luego, proporcionamos una lógica de iteración, que incrementa un
contador en nuestro caso. El último parámetro es una función selectora que nos permite personalizar los
resultados.
Los dos últimos métodos se ocupan de los temporizadores. Observable.Interval comienza a producir
eventos de marca de tiempo con el período TimeSpan , y Observable.Timer también especifica el tiempo de inicio.
Uso de consultas LINQ en una colección
observable
Esta receta le muestra cómo usar LINQ para consultar una secuencia asíncrona de eventos.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No se requieren otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter8\Recipe5.
174
Machine Translated by Google
Capítulo 8
Cómo hacerlo...
Para comprender el uso de consultas LINQ en la colección observable, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. Agregue una referencia al paquete NuGet de la biblioteca principal de Reactive Extensions . Consulte la
receta Convertir una colección en observable asincrónica para obtener detalles sobre cómo hacerlo.
3. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Reactive.Linq; usando
System.Console estático;
4. Agregue el siguiente fragmento de código debajo del método principal :
static IDisposable OutputToConsole<T>(IObservable<T> secuencia, int innerLevel) {
delimitador de cadena = nivel interno == 0 ? string.Empty :
new string('',
nivelInterior*3);
secuencia de retorno.Subscribe(
obj => WriteLine($"{delimiter}{obj}")
, ex => WriteLine($"Error: {ex.Message}")
, () => WriteLine($"{delimiter}Completado")
);
}
5. Agregue el siguiente fragmento de código dentro del método principal :
IObservable<long> secuencia =
Observable.Interval( TimeSpan.FromMilliseconds(50)).Take(21);
var evenNumbers = de n en secuencia
donde n % 2 == 0
seleccionar n;
var números impares = de n en secuencia
donde n % 2 != 0
seleccionar n;
var combine = from n en evenNumbers.Concat(oddNumbers)
seleccionar n;
175
Machine Translated by Google
Extensiones reactivas
var nums = (de n en combinar
donde n % 5 == 0
seleccionar n)
.Do(n => WriteLine($"El número {n} se procesa en el método Do"));
usando (var sub = OutputToConsole(secuencia, 0)) usando (var sub2 =
OutputToConsole(combine, 1)) usando (var sub3 = OutputToConsole(nums,
2)) {
WriteLine("Presione enter para finalizar la demo");
LeerLínea();
}
6. Ejecute el programa.
Cómo funciona...
La capacidad de usar LINQ contra las secuencias de eventos observables es la principal ventaja del marco de
extensiones reactivas. También hay muchos escenarios útiles diferentes; desafortunadamente, es imposible
mostrarlos todos aquí. Traté de proporcionar un ejemplo simple, pero muy ilustrativo, que no tiene muchos detalles
complejos y muestra la esencia misma de cómo podría funcionar una consulta LINQ cuando se aplica a colecciones
observables asincrónicas.
Primero, creamos un evento Observable que genera una secuencia de números, un número cada 50 milisegundos, y
comenzamos desde el valor inicial de cero, tomando 21 de esos eventos.
Luego, redactamos consultas LINQ para esta secuencia. Primero, seleccionamos solo los números pares de la secuencia
y luego solo los números impares. Luego, concatenamos estas dos secuencias.
La consulta final nos muestra cómo usar un método muy útil, Do, que nos permite introducir efectos secundarios y, por
ejemplo, registrar cada valor de la secuencia resultante. Para ejecutar todas las consultas, creamos suscripciones
anidadas y, dado que las secuencias son inicialmente asíncronas, debemos tener mucho cuidado con la duración de la
suscripción. El ámbito externo representa una suscripción al temporizador, y las suscripciones internas se ocupan de
la consulta de secuencia combinada y la consulta de efectos secundarios, respectivamente. Si presionamos Enter
demasiado pronto, simplemente cancelamos la suscripción del temporizador y, por lo tanto, detenemos la demostración.
Cuando ejecutamos la demostración, vemos el proceso real de cómo interactúan las diferentes consultas en tiempo real.
Podemos ver que nuestras consultas son perezosas y comienzan a ejecutarse solo cuando nos suscribimos a sus
resultados. La secuencia del evento del temporizador se imprime en la primera columna. Cuando la consulta de números
pares obtiene un número par, también lo imprime usando el prefijo para distinguir el resultado de esta secuencia
del primero. Los resultados finales de la consulta se imprimen en la columna de la derecha.
176
Machine Translated by Google
Capítulo 8
Cuando se ejecuta el programa, podemos ver que la secuencia del temporizador, la secuencia de números pares y
la secuencia de efectos secundarios se ejecutan en paralelo. Solo la concatenación espera hasta que se
completa la secuencia de números pares. ¡Si no concatenamos esas secuencias, tendremos cuatro secuencias
paralelas de eventos interactuando entre sí de la manera más efectiva! Esto muestra el poder real de Reactive
Extensions y podría ser un buen comienzo para aprender esta biblioteca en profundidad.
Creación de operaciones asíncronas con Rx
Esta receta le muestra cómo crear un Observable a partir de las operaciones asincrónicas definidas en otros
patrones de programación.
preparándose
Para trabajar con esta receta, necesitará Visual Studio 2015. No se requieren otros requisitos previos. El
código fuente de esta receta se puede encontrar en BookSamples\Chapter8\Recipe6.
Cómo hacerlo...
Para comprender cómo crear operaciones asíncronas con Rx, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. Agregue una referencia al paquete NuGet de la biblioteca principal de Reactive Extensions . Consulte la
receta Convertir una colección en observable asincrónica para obtener detalles sobre cómo hacerlo.
3. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
System.Reactive; utilizando
System.Reactive.Linq; usando
System.Reactive.Threading.Tasks;
utilizando
System.Threading.Tasks; utilizando
System.Timers; usando System.Console estático; usando System.Threading.Thread estático;
4. Agregue el siguiente fragmento de código debajo del método principal :
Tarea asincrónica estática<T> AwaitOnObservable<T>(IObservable<T> observable)
{
T obj = espera observable;
WriteLine($"{obj}" ); devolver
objeto;
}
177
Machine Translated by Google
Extensiones reactivas
Tarea estática<cadena> LongRunningOperationTaskAsync(nombre de cadena) {
return Task.Run(() => LongRunningOperation(nombre));
}
IObservable estático<cadena> LongRunningOperationAsync(nombre de cadena) {
return Observable.Start(() => LongRunningOperation(nombre));
}
cadena estática LongRunningOperation(nombre de cadena) {
Dormir (TimeSpan.FromSeconds (1)); return $"La
tarea {nombre} se ha completado. Id. de subproceso {CurrentThread.
ManagedThreadId}"; }
static IDisposable OutputToConsole(IObservable<EventPattern<Elapse dEventArgs>> secuencia) {
return secuencia.Subscribe( obj =>
WriteLine($"{obj.EventArgs.SignalTime}") ex => WriteLine($"Error:
, {ex.Message}")
, () => WriteLine("Completado")
);
}
static IDisposable OutputToConsole<T>(IObservable<T> secuencia) {
secuencia de retorno.Subscribe(
obj => WriteLine("{0}", obj)
, ex => WriteLine("Error: {0}", ej.Mensaje)
, () => WriteLine("Completado")
);
}
5. Reemplace el método principal con el siguiente fragmento de código:
delegar cadena AsyncDelegate(nombre de cadena);
vacío estático principal (cadena [] argumentos)
{
IObservable<cadena> o = LongRunningOperationAsync("Tarea1"); usando (var sub =
OutputToConsole(o)) {
Dormir (TimeSpan.FromSeconds (2)); };
178
Machine Translated by Google
Capítulo 8
Línea de escritura(" ");
Task<string> t = LongRunningOperationTaskAsync("Task2"); usando (var sub =
OutputToConsole(t.ToObservable())) {
Dormir (TimeSpan.FromSeconds (2)); }; Línea de
escritura(" ");
AsyncDelegate asyncMethod = LongRunningOperation;
// marcado como obsoleto, use tareas en su lugar Func<string,
IObservable<string>> observableFactory = Observable.FromAsyncPattern<string,
string>(
asyncMethod.BeginInvoke, asyncMethod.EndInvoke);
o = fabricaobservable("Tarea3"); usando (var sub
= OutputToConsole(o)) {
Dormir (TimeSpan.FromSeconds (2)); }; Línea de
escritura(" ");
o = fabricaobservable("Tarea4");
EsperarEnObservable(o).Esperar(); Línea de
escritura(" ");
usando (var temporizador = nuevo temporizador (1000))
{
varot = Observable.
FromEventPattern<EventHandler transcurrido,
ElapsedEventArgs>( h =>
timer.Elapsed += h,
h => temporizador.Transcurrido = h);
temporizador.Inicio();
usando (var sub = OutputToConsole(ot)) {
Dormir (TimeSpan.FromSeconds (5));
}
Línea de escritura(" ");
temporizador.Stop();
}
6. Ejecute el programa.
179
Machine Translated by Google
Extensiones reactivas
Cómo funciona...
Esta receta le muestra cómo convertir diferentes tipos de operaciones asincrónicas en una clase
Observable . El primer fragmento de código usa el método Observable.Start , que es bastante similar a
Task.Run de TPL. Comienza una operación asincrónica que da un resultado de cadena y luego se
completa.
Le sugiero encarecidamente que utilice la biblioteca paralela de
tareas para operaciones asincrónicas. Reactive Extensions también
es compatible con este escenario, pero para evitar la
ambigüedad, es mucho mejor ceñirse a las tareas cuando se habla
de operaciones asíncronas separadas y usar Rx solo cuando
necesitamos trabajar con secuencias de eventos. Otra sugerencia
es convertir cada tipo de operación asincrónica separada en tareas
y solo luego convertir una tarea en una clase observable, si lo necesita.
Luego, hacemos lo mismo con las tareas y convertimos una tarea en un método Observable simplemente
llamando al método de extensión ToObservable . El siguiente fragmento de código se trata de convertir
el patrón del Modelo de programación asincrónica en Observable. Normalmente, convertiría APM en una
tarea y luego una tarea en Observable. Sin embargo, hay una conversión directa y este ejemplo
ilustra cómo ejecutar un delegado asíncrono y envolverlo en una operación Observable .
La siguiente parte del fragmento de código muestra que podemos usar el operador await en una operación
Observable . Como no podemos usar el modificador asíncrono en un método de entrada como Main,
introducimos un método separado que devuelve una tarea y espera a que esta tarea resultante se complete
dentro del método Main .
La última parte de este fragmento de código es el mismo que el código que convierte el patrón APM
en Observable, pero ahora convertimos el Patrón asíncrono basado en eventos directamente en una
clase Observable . Creamos un temporizador y consumimos sus eventos durante 5 segundos. Luego
desechamos el temporizador para limpiar los recursos.
180
Machine Translated by Google
9
Uso de E/S asíncrona
En este capítulo, revisaremos en detalle las operaciones de E/S asíncronas. Aprenderás las
siguientes recetas:
f Trabajar con archivos de forma asíncrona
f Escritura de un servidor HTTP asíncrono y un cliente f
Trabajar con una base de datos de forma asíncrona
f Llamar a un servicio WCF de forma asíncrona
Introducción
En los capítulos anteriores, ya discutimos lo importante que es usar las operaciones de E/S
asíncronas correctamente. ¿Por qué importa tanto? Para tener una comprensión sólida,
consideremos dos tipos de aplicaciones.
Cuando ejecutamos una aplicación en un cliente, una de las cosas más importantes es tener una
interfaz de usuario receptiva. Esto significa que no importa lo que suceda con la aplicación, todos los
elementos de la interfaz de usuario, como los botones y las barras de progreso, siguen ejecutándose
rápidamente y el usuario obtiene una reacción inmediata de la aplicación. ¡Esto no es fácil de lograr! Si
intenta abrir el editor de texto del Bloc de notas en Windows e intenta cargar un documento de texto
que tiene varios megabytes de tamaño, la ventana de la aplicación se congelará durante un período
de tiempo significativo porque todo el texto se carga desde el disco primero y solo entonces el programa
comienza a procesar la entrada del usuario.
181
Machine Translated by Google
Uso de E/S asíncrona
Este es un problema extremadamente importante y, en esta situación, la única solución es evitar a toda costa bloquear
el subproceso de la interfaz de usuario. Esto, a su vez, significa que para evitar el bloqueo del subproceso de la interfaz
de usuario, cada API relacionada con la interfaz de usuario debe permitir solo llamadas asincrónicas. Esta es la
razón clave detrás del rediseño de las API en el sistema operativo Windows 8 al reemplazar casi todos los métodos
con análogos asincrónicos. Pero, ¿afecta el rendimiento si nuestra aplicación utiliza varios subprocesos para lograr
este objetivo? ¡Claro que lo hace! Sin embargo, podríamos pagar el precio teniendo en cuenta que tenemos un solo
usuario. Es bueno que la aplicación utilice toda la potencia de la computadora para que sea más eficaz, ya que toda
esta potencia está destinada al único usuario que ejecuta la aplicación.
Veamos entonces el segundo caso. Si ejecutamos la aplicación en un servidor, tenemos una situación completamente
diferente. Tenemos la escalabilidad como una prioridad principal, lo que significa que un solo usuario debe consumir la
menor cantidad de recursos posible. Si empezamos a crear muchos hilos para cada usuario, simplemente no
podemos escalar bien. Es un problema muy complejo equilibrar el consumo de recursos de nuestra aplicación de
manera eficiente. Por ejemplo, en ASP.NET, que es una plataforma de aplicaciones web de Microsoft, usamos un
conjunto de subprocesos de trabajo para atender las solicitudes de los clientes. Este grupo tiene una cantidad limitada de
subprocesos de trabajo y tenemos que minimizar el tiempo de uso de cada subproceso de trabajo para lograr la
escalabilidad. Esto significa que tenemos que devolverlo a la piscina lo antes posible para que pueda atender otro
pedido. Si iniciamos una operación asíncrona que requiere computación, tendremos un flujo de trabajo muy ineficiente.
Primero, tomamos un subproceso de trabajo del grupo de subprocesos para atender una solicitud de cliente.
Luego, tomamos otro subproceso de trabajo e iniciamos una operación asíncrona en él. Ahora, tenemos dos
subprocesos de trabajo que atienden nuestra solicitud, ¡pero realmente necesitamos que el primer subproceso haga
algo útil! Desafortunadamente, la situación común es que simplemente esperamos a que se complete la operación
asincrónica y consumimos dos subprocesos de trabajo en lugar de uno. En este escenario, ¡la asincronía es en realidad
peor que la ejecución sincrónica!
No necesitamos cargar todos los núcleos de la CPU, ya que estamos sirviendo a muchos clientes y, por lo tanto,
estamos utilizando toda la potencia informática de la CPU. No necesitamos mantener el primer hilo receptivo ya que
no tenemos una interfaz de usuario. Entonces, ¿por qué deberíamos usar asincronía en aplicaciones de servidor?
La respuesta es que debemos usar asincronía cuando hay una operación de E/S asíncrona.
Hoy en día, las computadoras modernas suelen tener una unidad de disco duro que almacena archivos y una
tarjeta de red que envía y recibe datos a través de la red. Ambos dispositivos tienen sus propias microcomputadoras
que administran las operaciones de E/S en un nivel muy bajo y le indican al sistema operativo los resultados. Este
es nuevamente un tema bastante complicado; pero para mantener el concepto claro, podríamos decir que hay una
manera para que los programadores inicien una operación de E/S y proporcionen al sistema operativo un código para
devolver la llamada cuando se complete la operación. Entre el inicio de una tarea de E/S y su finalización, no hay trabajo
de CPU involucrado; se realiza en los correspondientes microordenadores controladores de disco y red. Esta forma de
ejecutar una tarea de E/S se denomina subproceso de E/S; se implementan utilizando el conjunto de subprocesos
de .NET y, a su vez, utilizan una infraestructura del sistema operativo denominada puertos de finalización de E/S.
En ASP.NET, tan pronto como se inicia una operación de E/S asíncrona desde un subproceso de trabajo, se puede
devolver inmediatamente al grupo de subprocesos. Mientras se lleva a cabo la operación, este hilo puede servir a
otros clientes. Finalmente, cuando la operación indica que se completó, la infraestructura de ASP.NET obtiene un
subproceso de trabajo libre del grupo de subprocesos (que podría ser diferente del que inició la operación) y finaliza
la operación.
182
Machine Translated by Google
Capítulo 9
Está bien; ahora entendemos cuán importantes son los subprocesos de E/S para las aplicaciones de servidor.
Desafortunadamente, es muy difícil verificar si una API determinada usa subprocesos de E/S bajo el capó.
La única forma (además de estudiar el código fuente) es simplemente saber qué biblioteca de clases de .NET Framework
aprovecha los subprocesos de E/S. En este capítulo, veremos cómo usar algunas de esas API. Aprenderá a trabajar con
archivos de forma asíncrona, cómo usar la E/S de red para crear un servidor HTTP y llamar al servicio Windows
Communication Foundation (WCF) , y cómo trabajar con una API asíncrona para consultar una base de datos.
Otro tema importante a considerar es el paralelismo. Por varias razones,
una operación de disco paralela intensiva puede tener un rendimiento
muy bajo. Tenga en cuenta que las operaciones de E/S paralelas a
menudo son muy ineficaces y podría ser razonable trabajar con E/S
secuencialmente, pero de forma asíncrona.
Trabajar con archivos de forma asíncrona
Esta receta nos muestra cómo crear un archivo y cómo leer y escribir datos de forma asíncrona.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter9\Recipe1.
Cómo hacerlo...
Para comprender cómo trabajar con archivos de forma asincrónica, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
System.IO; utilizando
System.Linq; usando
Sistema.Texto; utilizando
System.Threading.Tasks; usando
System.Console estático; usando System.Text.Encoding estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
const int BUFFER_SIZE = 4096;
Tarea asincrónica estática ProcessAsynchronousIO() {
183
Machine Translated by Google
Uso de E/S asíncrona
usando (var stream = new FileStream(
"prueba1.txt", FileMode.Create, FileAccess.ReadWrite,
FileShare.Ninguno, BUFFER_SIZE))
{
WriteLine($"1. Utiliza subprocesos de E/S: {stream.IsAsync}");
byte[] búfer = UTF8.GetBytes(CreateFileContent()); var writeTask =
Task.Factory.FromAsync(
corriente.BeginWrite, corriente.EndWrite, búfer, 0,
búfer.Longitud, nulo);
esperar escribirTarea;
}
usando (var stream = new FileStream("test2.txt", FileMode.Create,
FileAccess.ReadWrite,FileShare.None, BUFFER_SIZE, FileOptions.Asynchronous))
{
WriteLine($"2. Utiliza subprocesos de E/S: {stream.IsAsync}");
byte[] búfer = UTF8.GetBytes(CreateFileContent()); var writeTask =
Task.Factory.FromAsync(
corriente.BeginWrite, corriente.EndWrite, búfer, 0,
búfer.Longitud, nulo);
esperar escribirTarea;
}
usando (var stream = File.Create("test3.txt", BUFFER_SIZE,
FileOptions.Asynchronous)) usando (var
sw = new StreamWriter(stream)) {
WriteLine($"3. Utiliza subprocesos de E/S: {stream.IsAsync}"); esperar
sw.WriteAsync(CreateFileContent());
}
usando (var sw = new StreamWriter("test4.txt", true)) {
WriteLine($"4. Utiliza subprocesos de E/S:
{((FileStream)sw.BaseStream).IsAsync}");
esperar sw.WriteAsync(CreateFileContent());
}
WriteLine("Comenzando a analizar archivos en paralelo");
184
Machine Translated by Google
Capítulo 9
var readTasks = new Task<largo>[4]; para (int i = 0; i <
4; i++) {
string fileName = $"prueba{i + 1}.txt"; readTasks[i] =
SumFileContent(fileName);
}
long[] sums = await Task.WhenAll(readTasks);
WriteLine($"Suma en todos los archivos: {sums.Sum()}");
WriteLine("Eliminando archivos...");
Tarea[] eliminarTareas = nueva Tarea[4]; para (int i =
0; i < 4; i++) {
string fileName = $"prueba{i + 1}.txt"; deleteTasks[i] =
SimulateAsynchronousDelete(fileName);
}
espera Task.WhenAll(deleteTasks);
WriteLine("Eliminación completa.");
}
cadena estática CreateFileContent() {
var sb = nuevo StringBuilder(); para (int i = 0; i
< 100000; i++) {
sb.Append($"{nuevo Random(i).Next(0, 99999)}"); sb.AppendLine();
} devuelve sb.ToString();
}
Tarea asincrónica estática <long> SumFileContent(string fileName) {
usando (var stream = new FileStream(fileName,
FileMode.Open, FileAccess.Read,FileShare.Ninguno, BUFFER_SIZE, FileOptions.Asynchronous))
usando (var sr = new StreamReader(flujo)) {
suma larga = 0;
185
Machine Translated by Google
Uso de E/S asíncrona
while (sr. Peek() > 1)
{
cadena de línea = esperar sr.ReadLineAsync(); sum +=
long.Parse(línea);
}
suma devuelta;
}
}
Tarea estática SimulateAsynchronousDelete(string fileName) {
return Task.Run(() => File.Delete(fileName));
}
4. Agregue el siguiente fragmento de código dentro del método principal :
var t = ProcessAsynchronousIO();
t.GetAwaiter().GetResult();
5. Ejecute el programa.
Cómo funciona...
Cuando se ejecuta el programa, creamos cuatro archivos de diferentes maneras y los llenamos con datos
aleatorios. En el primer caso, usamos la clase FileStream y sus métodos, convirtiendo una API de
modelo de programación asincrónica en una tarea; en el segundo caso, hacemos lo mismo, pero
proporcionamos FileOptions.Asynchronous al constructor de FileStream .
Es muy importante utilizar la opción FileOptions.Asynchronous.
Si omitimos esta opción, aún podemos trabajar con el archivo de manera
asíncrona, ¡pero esto es solo una invocación de delegado asíncrono en un
grupo de subprocesos! Usamos la asincronía de E/S con la clase FileStream
solo si proporcionamos esta opción (o bool useAsync en otra sobrecarga del constructor).
El tercer caso utiliza algunas API simplificadas, como el método File.Create y la clase StreamWriter .
Todavía usa subprocesos de E/S, que podemos verificar usando la transmisión.
Propiedad IsAsync . El último caso ilustra que simplificar demasiado también es malo. Aquí, no aprovechamos
la asincronía de E/S imitándola con la ayuda de la invocación de delegados asíncronos.
Ahora, realizamos una lectura asincrónica paralela de archivos, sumamos su contenido y luego lo sumamos
entre sí. Finalmente, borramos todos los archivos. Como no hay un archivo de eliminación asincrónica en
ninguna aplicación de la tienda que no sea de Windows, simulamos la asincronía usando el método de
fábrica Task.Run .
186
Machine Translated by Google
Capítulo 9
Escribir un servidor y un cliente HTTP asíncronos
Esta receta le muestra cómo crear un servidor HTTP asíncrono simple.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No se requieren otros requisitos previos. El
código fuente de esta receta se puede encontrar en BookSamples\Chapter9\Recipe2.
Cómo hacerlo...
Los siguientes pasos demuestran cómo crear un servidor HTTP asíncrono simple:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. Agregue una referencia a la biblioteca del marco System.Net.Http .
3. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.IO;
utilizando System.Net;
utilizando System.Net.Http;
utilizando System.Threading.Tasks; usando
System.Console estático;
4. Agregue el siguiente fragmento de código debajo del método principal :
Tarea asincrónica estática GetResponseAsync (URL de cadena) {
usando (var cliente = nuevo HttpClient()) {
HttpResponseMessage responseMessage = esperar
cliente.GetAsync(url);
string responseHeaders = mensaje de respuesta.Headers.ToString(); respuesta de cadena =
esperar mensaje de
respuesta.Contenido.ReadAsStringAsync();
WriteLine("Cabeceras de respuesta:");
WriteLine(responseHeaders);
WriteLine("Cuerpo de la respuesta:");
WriteLine(respuesta);
}
}
187
Machine Translated by Google
Uso de E/S asíncrona
clase AsyncHttpServer {
solo lectura HttpListener _listener; const string
RESPONSE_TEMPLATE =
"<html><head><title>Prueba</title></
head><body><h2>Página de prueba</h2>" +
"<h4>Hoy es: {0}</h4> </cuerpo></html>";
AsyncHttpServer público (número de puerto int) {
_escucha = new HttpListener();
_listener.Prefixes.Add($"http://localhost:{portNumber}/");
}
inicio de tarea asincrónica pública () {
_listener.Start();
mientras (verdadero)
{
var ctx = esperar _listener.GetContextAsync(); WriteLine("Cliente
conectado..."); var respuesta =
string.Format(RESPONSE_TEMPLATE, DateTime.Now);
usando (var sw = new StreamWriter(ctx.Response.OutputStream)) {
esperar sw.WriteAsync(respuesta); esperar
sw.FlushAsync();
}
}
}
parada de tarea asíncrona pública () {
_listener.Abort();
}
}
5. Agregue el siguiente fragmento de código dentro del método principal :
servidor var = nuevo AsyncHttpServer (1234); var t =
Tarea.Ejecutar(() => servidor.Iniciar()); WriteLine("Escuchando
en el puerto 1234. Abra http://localhost:1234 en su navegador.");
188
Machine Translated by Google
Capítulo 9
WriteLine("Intentando conectar:");
Línea de escritura();
GetResponseAsync("http://localhost:1234").GetAwaiter().
ObtenerResultado();
Línea de
escritura(); WriteLine("Presione Enter para detener el servidor.");
LeerLínea();
servidor.Stop().GetAwaiter().GetResult();
6. Ejecute el programa.
Cómo funciona...
Aquí, implementamos un servidor web muy simple utilizando la clase HttpListener . También hay una
clase TcpListener para las operaciones de E/S del socket TCP. Configuramos nuestro oyente para
aceptar conexiones desde cualquier host a la máquina local en el puerto 1234. Luego, iniciamos el
oyente en un hilo de trabajo separado para que podamos controlarlo desde el hilo principal.
La operación de E/S asíncrona ocurre cuando usamos el método GetContextAsync .
Desafortunadamente, no acepta CancellationToken para escenarios de cancelación; entonces, cuando
queremos detener el servidor, simplemente llamamos al método _listener.Abort , que abandona la
conexión y detiene el servidor.
Para realizar una solicitud asíncrona en este servidor, usamos la clase HttpClient ubicada en el
ensamblado System.Net.Http y el mismo espacio de nombres. Usamos el método GetAsync para emitir
una solicitud HTTP GET asíncrona . También hay métodos para otras solicitudes HTTP como POST,
DELETE y PUT . HttpClient tiene muchas otras opciones, como serializar y deserializar un objeto usando
diferentes formatos, como XML y JSON, especificando una dirección de servidor proxy, credenciales, etc.
Cuando ejecuta el programa, puede ver que el servidor se ha iniciado. En el código del servidor,
usamos el método GetContextAsync para aceptar nuevas conexiones de clientes. Este método regresa
cuando se conecta un nuevo cliente, y simplemente generamos un lenguaje HTML muy básico
con la fecha y hora actual de la respuesta. Luego, solicitamos al servidor e imprimimos los
encabezados y el contenido de la respuesta. También puede abrir su navegador y navegar a http://
localhost:1234/. Aquí, verá la misma respuesta que se muestra en la ventana del navegador.
189
Machine Translated by Google
Uso de E/S asíncrona
Trabajar con una base de datos de forma asíncrona
Esta receta nos guía a través del proceso de crear una base de datos, llenarla con datos y leer datos de forma
asíncrona.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No se requieren otros requisitos previos. El
código fuente de esta receta se puede encontrar en BookSamples\Chapter9\Recipe3.
Cómo hacerlo...
Para comprender el proceso de creación de una base de datos, llenarla con datos y leer datos de forma
asíncrona, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Data;
utilizando System.Data.SqlClient; utilizando
System.IO; usando
System.Reflection; utilizando
System.Threading.Tasks; usando
System.Console estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
Tarea asincrónica estática ProcessAsynchronousIO(string dbName) {
intentar
cadena const cadena de conexión =
@"Fuente de datos=(LocalDB)\MSSQLLocalDB;Inicial
Catálogo=maestro;" +
"Seguridad Integrada=Verdadero";
cadena de carpeta de salida = Path.GetDirectoryName (
Asamblea.GetExecutingAssembly().Ubicación);
string dbFileName = Path.Combine(outputFolder, $"{dbName}.mdf");
string dbLogFileName =
Path.Combine(outputFolder, $"{dbName}_log.ldf");
190
Machine Translated by Google
Capítulo 9
cadena dbConnectionString =
@"Fuente de datos=(LocalDB)\MSSQLLocalDB;" +
$"AttachDBFileName={dbFileName};Seguridad integrada=Verdadero;";
usando (conexión var = nueva SqlConnection (cadena de conexión)) {
esperar conexión.OpenAsync();
if (Archivo.Existe(dbFileName))
{
WriteLine("Separando la base de datos...");
var detachCommand = new SqlCommand("sp_detach_db", conexión);
detachCommand.CommandType = CommandType.StoredProcedure;
detachCommand.Parameters.AddWithValue("@dbname", dbName);
espera detachCommand.ExecuteNonQueryAsync();
WriteLine("La base de datos se separó con éxito."); WriteLine("Eliminando la base de
datos...");
if(File.Exists(dbLogFileName)) File.Delete(dbLogFileName); Archivo.Eliminar(dbFileName);
WriteLine("La base de datos fue eliminada con exito.");
}
WriteLine("Creando la base de datos..."); string createCommand
= $"CREAR BASE DE DATOS
{dbName} ON (NOMBRE = N'{dbName}',
NOMBRE DE ARCHIVO =
" +
$"'{dbFileName}')";
var cmd = new SqlCommand(createCommand, conexión);
espera cmd.ExecuteNonQueryAsync(); WriteLine("La
base de datos fue creada exitosamente");
}
usando (var conexión = nueva SqlConnection(dbConnectionString)) {
esperar conexión.OpenAsync();
191
Machine Translated by Google
Uso de E/S asíncrona
var cmd = new SqlCommand("SELECT newid()", conexión); var resultado = esperar
cmd.ExecuteScalarAsync();
WriteLine($"Nuevo GUID de la base de datos: {resultado}");
cmd = new SqlCommand( @"CREAR
TABLA [dbo].[CustomTable]( [ID] [int] IDENTIDAD(1,1) NO
NULO, " +
"[Nombre] [nvarchar](50) NO NULO, RESTRICCIÓN [PK_ID] CLAVE PRINCIPAL
AGRUPADOS " +
" ([ID] ASC) ON [PRIMARIO]) ON [PRIMARIO]", conexión);
espera cmd.ExecuteNonQueryAsync();
WriteLine("La tabla fue creada exitosamente.");
cmd = nuevo comando Sql (
@"INSERT INTO [dbo].[CustomTable] (Name) VALUES ('John'); INSERT INTO [dbo].
[CustomTable] (Name) VALUES ('Peter'); INSERT INTO [dbo].[CustomTable] ( Nombre)
VALORES ('James');
INSERTAR EN [dbo].[CustomTable] (Nombre) VALORES ('Eugene');", conexión);
espera cmd.ExecuteNonQueryAsync();
WriteLine("Datos insertados con exito"); WriteLine("Leyendo datos
de la tabla...");
cmd = new SqlCommand(@"SELECT * FROM [dbo].[CustomTable]", conexión); usando (lector
SqlDataReader =
esperar cmd.
ExecuteReaderAsync())
{
while (esperar lector.ReadAsync()) {
var id = lector.GetFieldValue<int>(0); var nombre =
lector.GetFieldValue<cadena>(1);
WriteLine("Fila de la tabla: Id {0}, Nombre {1}", id, nombre);
}
}
192
Machine Translated by Google
Capítulo 9
} catch(excepción ex) {
WriteLine("Error: {0}", ej.Mensaje);
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
const string dataBaseName = "CustomDatabase"; var t =
ProcessAsynchronousIO(dataBaseName); t.GetAwaiter().GetResult();
Console.WriteLine("Presione Enter para salir"); Consola.ReadLine();
5. Ejecute el programa.
Cómo funciona...
Este programa funciona con un software llamado SQL Server 2014 LocalDb. Está instalado con Visual Studio
2015 y debería funcionar bien. Sin embargo, en caso de errores, es posible que desee reparar este
componente desde el asistente de instalación.
Comenzamos con la configuración de rutas a nuestros archivos de base de datos. Colocamos los archivos de
la base de datos en la carpeta de ejecución del programa. Habrá dos archivos: uno para la propia
base de datos y otro para el archivo de registro de transacciones. También configuramos dos cadenas de
conexión que definen cómo nos conectamos a nuestras bases de datos. La primera es conectarnos al motor
LocalDb para desvincular nuestra base de datos; si ya existe, elimínelo y vuelva a crearlo. Aprovechamos la
asincronía de E/S al abrir la conexión y al ejecutar los comandos SQL mediante los métodos OpenAsync y
ExecuteNonQueryAsync , respectivamente.
Una vez completada esta tarea, adjuntamos una base de datos recién creada. Aquí, creamos una nueva
tabla e insertamos algunos datos en ella. Además de los métodos mencionados anteriormente, usamos
ExecuteScalarAsync para obtener de forma asíncrona un valor escalar del motor de la base de datos y usamos
el método SqlDataReader.ReadAsync para leer una fila de datos de la tabla de la base de datos de forma
asíncrona.
Si tuviéramos una tabla grande con valores binarios grandes en sus filas en nuestra base de datos, usaríamos la
enumeración CommandBehavior.SequentialAcess para crear el lector de datos y el método GetFieldValueAsync
para obtener valores de campo grandes del lector de forma asíncrona.
193
Machine Translated by Google
Uso de E/S asíncrona
Llamar a un servicio WCF de forma asíncrona
Esta receta describirá cómo crear un servicio WCF, cómo alojarlo en una aplicación de consola, cómo
hacer que los metadatos del servicio estén disponibles para los clientes y cómo consumirlos de forma asíncrona.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter9\Recipe4.
Cómo hacerlo...
Para comprender cómo trabajar con un servicio WCF, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. Agregue referencias a la biblioteca System.ServiceModel . Haga clic derecho en el
Carpeta Referencias en el proyecto y seleccione la opción de menú Añadir referencia… .
Agregue referencias a la biblioteca System.ServiceModel . Puede usar la función de búsqueda en el
cuadro de diálogo del administrador de referencias, como se muestra en la siguiente captura de pantalla:
194
Machine Translated by Google
Capítulo 9
3. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando System.ServiceModel;
utilizando System.ServiceModel.Descripción; utilizando
System.Threading.Tasks; usando
System.Console estático;
4. Agregue el siguiente fragmento de código debajo del método principal :
const string SERVICE_URL = "http://localhost:1234/HelloWorld";
Tarea asincrónica estática RunServiceClient() {
var endpoint = new EndpointAddress(SERVICE_URL); var channel =
ChannelFactory<IHelloWorldServiceClient> .CreateChannel(new BasicHttpBinding(),
punto final);
var saludo = esperar canal.GreetAsync("Eugene"); WriteLine(saludo);
[ServiceContract(Espacio de nombres = "Paquete", Nombre =
"HelloWorldServiceContract")] interfaz
pública IHelloWorldService {
[ContratoOperación] string
Saludo(string nombre);
}
[ServiceContract(Espacio de nombres = "Paquete", Nombre =
"HelloWorldServiceContract")] interfaz
pública IHelloWorldServiceClient {
[ContratoOperación] string
Saludo(string nombre);
[Contrato de operación]
Tarea<cadena> GreetAsync(nombre de cadena);
}
clase pública HelloWorldService : IHelloWorldService {
public string Saludo(nombre de la cadena) {
return $"¡Saludos, {nombre}!";
}
}
195
Machine Translated by Google
Uso de E/S asíncrona
5. Agregue el siguiente fragmento de código dentro del método principal :
ServiceHost anfitrión = nulo;
intentar
host = nuevo ServiceHost(tipode(HelloWorldService), nuevo
Uri(URL_SERVICIO)); var
metadatos =
host.Descripción.Comportamientos.Find<ServiceMetadataBehavior>()
?? nuevo ComportamientoMetadataServicio();
metadatos.HttpGetEnabled = verdadero;
metadata.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
host.Descripción.Comportamientos.Agregar(metadatos);
host.AddServiceEndpoint(ServiceMetadataBehavior.MexContractName,
MetadataExchangeBindings.CreateMexHttpBinding(), "mex");
var endpoint = host.AddServiceEndpoint(typeof
(IHelloWorldService), nuevo BasicHttpBinding(), SERVICE_URL);
host.Faulted += (remitente, e) => WriteLine("¡Error!");
host.Open();
WriteLine("El servicio de saludos se está ejecutando y escuchando en:");
WriteLine($"{endpoint.Address} ({endpoint.Binding.Name})");
var cliente = RunServiceClient();
cliente.GetAwaiter().GetResult();
WriteLine("Presione Enter para salir");
LeerLínea();
} catch (excepción ex) {
WriteLine($"Error en el bloque catch: {ex}");
} finalmente
{
si (nulo! = host) {
196
Machine Translated by Google
Capítulo 9
if (host.State == CommunicationState.Faulted)
{
anfitrión.Abortar();
}
demás
{
anfitrión.Cerrar();
}
}
}
6. Ejecute el programa.
Cómo funciona...
WCF es un marco que nos permite llamar a servicios remotos de diferentes maneras. Uno de ellos, que
fue muy popular hace un tiempo, se usaba para llamar a servicios remotos a través de HTTP utilizando un
protocolo basado en XML llamado Simple Object Access Protocol (SOAP). Es bastante común cuando una
aplicación de servidor llama a otro servicio remoto, y esto también se puede hacer usando subprocesos de
E/S.
Visual Studio 2015 tiene una gran compatibilidad con los servicios WCF; por ejemplo, puede agregar
referencias a dichos servicios con la opción de menú Agregar referencia de servicio . También podría hacer
esto con nuestro servicio porque proporcionamos metadatos de servicio.
Para crear dicho servicio, necesitamos usar una clase ServiceHost que alojará nuestro servicio.
Describimos qué servicio alojaremos al proporcionar un tipo de implementación de servicio y el URI base
mediante el cual se abordará el servicio. Luego, configuramos el punto final de metadatos y el punto final de
servicio. Finalmente, manejamos el evento Faulted en caso de errores y ejecutamos el servicio de host.
Tenga en cuenta que necesitamos tener privilegios de administrador para
ejecutar el servicio, ya que utiliza enlaces HTTP, que a su vez usan
http.sys y, por lo tanto, requieren permisos especiales para ser creados. Puede
ejecutar Visual Studio con un administrador o ejecutar el siguiente comando
en el símbolo del sistema elevado para agregar los permisos necesarios:
netsh http agregar urlacl url=http://+:1234/HelloWorld usuario=máquina\usuario
197
Machine Translated by Google
Uso de E/S asíncrona
Para consumir este servicio, creamos un cliente, y aquí es donde ocurre el truco principal. En el
lado del servidor, tenemos un servicio con el método síncrono habitual llamado Greet. Este método
se define en el contrato de servicio, IHelloWorldService. Sin embargo, si queremos aprovechar
una E/S de red asíncrona, debemos llamar a este método de forma asíncrona. Podemos
hacerlo creando un nuevo contrato de servicio con un espacio de nombres y un nombre de
servicio coincidentes, donde definimos los métodos sincrónicos y asincrónicos basados en
tareas. A pesar de que no tenemos una definición de método asíncrono en el lado del servidor,
seguimos la convención de nomenclatura y la infraestructura de WCF entiende que queremos
crear un método de proxy asíncrono.
Por lo tanto, cuando creamos un canal proxy IHelloWorldServiceClient y WCF enruta
correctamente una llamada asíncrona al método síncrono del lado del servidor, si deja la
aplicación ejecutándose, puede abrir el navegador y acceder al servicio usando su URL, es
decir, http : //localhost:1234/HolaMundo. Se abrirá una descripción del servicio y podrá buscar
los metadatos XML que nos permiten agregar una referencia de servicio de Visual Studio 2012.
Si intenta generar la referencia, verá un código un poco más complicado, pero se genera
automáticamente y es fácil. usar.
198
Machine Translated by Google
Programación en paralelo
10
Patrones
En este capítulo, revisaremos los problemas comunes a los que se enfrenta un programador al intentar implementar
un flujo de trabajo paralelo. Aprenderás las siguientes recetas:
f Implementación de estados compartidos evaluados por Lazy
f Implementando Parallel Pipeline con BlockingCollection f Implementando Parallel
Pipeline con TPL DataFlow f Implementando Map/Reduce con PLINQ
Introducción
Los patrones en programación significan una solución concreta y estándar para un problema dado. Por lo general, los
patrones de programación son el resultado de personas que acumulan experiencia, analizan los problemas comunes y
brindan soluciones a estos problemas.
Dado que la programación paralela existe desde hace mucho tiempo, existen muchos patrones diferentes que se utilizan
para programar aplicaciones paralelas. Incluso existen lenguajes de programación especiales para facilitar la programación
de algoritmos paralelos específicos. Sin embargo, aquí es donde las cosas empiezan a complicarse cada vez más. En
este capítulo, le proporcionaré un punto de partida desde el cual podrá seguir estudiando la programación paralela.
Revisaremos patrones muy básicos, pero muy útiles, que son bastante útiles para muchas situaciones comunes en la
programación paralela.
Primero, usaremos un objeto de estado compartido de varios subprocesos. Me gustaría enfatizar que debes evitarlo
tanto como sea posible. Como discutimos en capítulos anteriores, un estado compartido es realmente malo cuando
escribes algoritmos paralelos, pero en muchas ocasiones es inevitable.
Descubriremos cómo retrasar el cálculo real de un objeto hasta que sea necesario y cómo implementar diferentes
escenarios para lograr la seguridad de subprocesos.
199
Machine Translated by Google
Patrones de programación en paralelo
Luego, le mostraremos cómo crear un flujo de datos paralelo estructurado. Revisaremos un caso concreto de un patrón productor/
consumidor, que se llama Parallel Pipeline. Vamos a implementarlo simplemente bloqueando la colección primero, y
luego veremos cuán útil es otra biblioteca de Microsoft para la programación paralela: TPL DataFlow.
El último patrón que estudiaremos es el patrón Map/Reduce . En el mundo moderno, este nombre podría significar
cosas muy diferentes. Algunas personas consideran Map/Reduce no como un enfoque común para cualquier problema,
sino como una implementación concreta para grandes cálculos de clústeres distribuidos. Descubriremos el significado detrás
del nombre de este patrón y revisaremos algunos ejemplos de cómo podría funcionar en casos de pequeñas aplicaciones
paralelas.
Implementación de estados compartidos evaluados por Lazy
Esta receta muestra cómo programar un objeto de estado compartido seguro para subprocesos y evaluado por Lazy.
preparándose
Para comenzar esta receta, deberá ejecutar Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter10\Recipe1.
Cómo hacerlo...
Para implementar estados compartidos evaluados por Lazy, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Threading;
utilizando System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
Tarea asincrónica estática ProcessAsynchronously() {
var unsafeState = new UnsafeState();
Tarea[] tareas = nueva Tarea[4];
para (int i = 0; i < 4; i++) {
tareas[i] = Tarea.Ejecutar(() => Trabajador(estado inseguro));
}
200
Machine Translated by Google
Capítulo 10
esperar Task.WhenAll(tareas); Línea de
escritura(" ");
var firstState = new DoubleCheckedLocking(); para (int i = 0; i < 4; i++) {
tareas[i] = Tarea.Ejecutar(() => Trabajador(primerEstado));
}
esperar Task.WhenAll(tareas); Línea de
escritura(" ");
var segundoEstado = new BCLDoubleChecked(); para (int i = 0; i < 4;
i++) {
tareas[i] = Tarea.Ejecutar(() => Trabajador(segundoEstado));
}
esperar Task.WhenAll(tareas); Línea de
escritura(" ");
var lazy = new Lazy<ValueToAccess>(Compute); var tercerEstado = new
LazyWrapper(lazy); para (int i = 0; i < 4; i++) {
tareas[i] = Tarea.Ejecutar(() => Trabajador(tercer Estado));
}
esperar Task.WhenAll(tareas);
Línea de escritura(" ");
var cuartoEstado = new BCLThreadSafeFactory(); para (int i = 0; i < 4; i++) {
tareas[i] = Tarea.Ejecutar(() => Trabajador(cuartoEstado));
}
esperar Task.WhenAll(tareas); Línea de
escritura(" ");
Trabajador vacío estático (estado IHasValue)
{
201
Machine Translated by Google
Patrones de programación en paralelo
WriteLine($"Worker se ejecuta en el id de subproceso {CurrentThread.
ManagedThreadId}");
WriteLine($"Valor del estado: {estado.Valor.Texto}");
}
Valor estático para acceder a la computación ()
{
WriteLine("El valor se construye en un hilo" +
$"id {CurrentThread.ManagedThreadId}"); Dormir
(TimeSpan.FromSeconds (1));
devuelve un nuevo valor de acceso (
$"Construido en la identificación del subproceso {CurrentThread.
ManagedThreadId}"); }
clase ValueToAccess
{
cadena privada de solo lectura _texto; public
ValueToAccess(cadena de texto) {
_texto = texto;
}
cadena pública Texto => _texto;
}
clase UnsafeState: IHasValue
{
ValueToAccess privado _valor;
ValueToAccess público Valor =>_value ?? (_valor = Calcular());
}
clase DoubleCheckedLocking : IHasValue {
objeto privado de solo lectura _syncRoot = nuevo objeto(); ValueToAccess
volátil privado _value;
Valor público de acceso al valor {
conseguir
si (_valor == nulo) {
202
Machine Translated by Google
Capítulo 10
bloquear (_syncRoot) {
if (_valor == nulo) _valor = Calcular();
}
} devuelve _valor;
}
}
}
clase BCLDoubleChecked: IHasValue
{
objeto privado _syncRoot = nuevo objeto(); ValueToAccess
privado _valor; bool privado _inicializado;
ValueToAccess público Valor => LazyInitializer.EnsureInitialized(ref _value, ref _initialized, ref
_syncRoot, Compute);
}
clase BCLThreadSafeFactory: IHasValue {
ValueToAccess privado _valor;
ValueToAccess público Valor => LazyInitializer.
Asegúrese de inicializar (ref _value, Compute); }
clase LazyWrapper: IHasValue {
privado solo lectura Lazy<ValueToAccess> _value;
public LazyWrapper(Lazy<ValueToAccess> valor) {
_valor = valor;
}
Public ValueToAccess Value => _value.Value;
}
interfaz IHasValue
{
ValueToAccess Valor { obtener; }
}
203
Machine Translated by Google
Patrones de programación en paralelo
4. Agregue el siguiente fragmento de código dentro del método principal :
var t = ProcessAsynchronously();
t.GetAwaiter().GetResult();
5. Ejecute el programa.
Cómo funciona...
El primer ejemplo muestra por qué no es seguro usar el objeto UnsafeState con múltiples subprocesos de
acceso. Vemos que el método Construct fue llamado varias veces, y diferentes subprocesos usan diferentes
valores, lo que obviamente no es correcto. Para arreglar esto, podemos usar un candado al leer el valor, y si no
está inicializado, crearlo primero. Esto funcionará, pero usar un bloqueo con cada operación de lectura no es
eficiente. Para evitar el uso de bloqueos cada vez, podemos usar un enfoque tradicional llamado patrón de
bloqueo de verificación doble . Verificamos el valor por primera vez, y si no es nulo, evitamos bloqueos
innecesarios y solo usamos el objeto compartido.
Sin embargo, si no se construyó, usamos el bloqueo y luego verificamos el valor por segunda vez porque podría
inicializarse entre nuestra primera verificación y la operación de bloqueo. Si todavía no está inicializado, solo
entonces calculamos el valor. Podemos ver claramente que este enfoque funciona con el segundo ejemplo: solo
hay una llamada al método Construct , y el primer subproceso llamado define el estado del objeto compartido.
Tenga en cuenta que si la implementación del objeto evaluado por Lazy es segura para
subprocesos, no significa automáticamente que todas sus propiedades también sean seguras
para subprocesos.
Si agrega, por ejemplo, una propiedad pública int al objeto ValueToAccess,
no será seguro para subprocesos; todavía tiene que usar construcciones entrelazadas o
bloqueo para garantizar la seguridad de los subprocesos.
Este patrón es muy común, y es por eso que hay varias clases en la Biblioteca de clases base para ayudarnos.
Primero, podemos usar el método LazyInitializer.EnsureInitialized , que implementa el patrón de bloqueo de doble
verificación en el interior. Sin embargo, la opción más cómoda es usar la clase Lazy<T> , que nos permite tener un
estado compartido seguro para subprocesos, evaluado por Lazy, listo para usar. Los siguientes dos ejemplos
nos muestran que son equivalentes al segundo, y el programa se comporta de la misma manera. La única
diferencia es que dado que LazyInitializer es una clase estática, no tenemos que crear una nueva instancia
de una clase, como hacemos en el caso de Lazy<T>, y por lo tanto, el rendimiento en el primer caso puede ser
mejor en algunos escenarios raros.
La última opción es evitar el bloqueo en absoluto si no nos importa el método Construct . Si es seguro para
subprocesos y no tiene efectos secundarios ni impactos graves en el rendimiento, podemos ejecutarlo varias
veces pero usar solo el primer valor construido. El último ejemplo muestra el comportamiento descrito, y podemos
lograr este resultado utilizando otra sobrecarga del método LazyInitializer.EnsureInitialized .
204
Machine Translated by Google
Capítulo 10
Implementando Parallel Pipeline con
BlockingCollection
Esta receta describirá cómo implementar un escenario específico de un patrón de productor/consumidor, que se
denomina canalización paralela, utilizando la estructura de datos estándar de BlockingCollection .
preparándose
Para comenzar esta receta, deberá ejecutar Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter10\Recipe2.
Cómo hacerlo...
Para comprender cómo implementar Parallel Pipeline usando BlockingCollection, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando System.Collections.Concurrent;
utilizando System.Globalization; utilizando
System.Linq; utilizando
System.Threading; utilizando
System.Threading.Tasks; usando
System.Console estático; usando
System.Threading.Thread estático;
3. Agregue el siguiente fragmento de código debajo del método principal :
privado const int CollectionNumber = 4; privado const int
Cuenta = 5;
static void CreateInitialValues(BlockingCollection<int>[] sourceArrays,
CancellationTokenSource cts) {
Parallel.For(0, sourceArrays.Length*Count, (j, estado) => {
si (cts.Token.IsCancellationRequested) {
estado.Detener();
}
205
Machine Translated by Google
Patrones de programación en paralelo
número int = ObtenerNúmeroAleatorio(j); int k =
BlockingCollection<int>.TryAddToAny(sourceArrays,
j);
si (k >= 0) {
Línea de escritura(
$"añadió {j} a los datos de origen en el subproceso " $"id +
{CurrentThread.ManagedThreadId}");
Dormir (TimeSpan.FromMilliseconds (número));
}
});
foreach (arr var en sourceArrays) {
arr.CompleteAdding();
}
}
static int GetRandomNumber(int seed) {
devuelve nuevo Random(seed).Next(500);
}
clase PipelineWorker<TInput, TOutput> {
Func<TInput, TOutput> _procesador;
Acción<TInput> _outputProcessor;
BlockingCollection<TInput>[] _input;
CancelaciónToken _token;
Aleatorio _rnd;
PipelineWorker público (
BlockingCollection<TInput>[] entrada,
Func<TInput, TOutput> procesador,
Token CancellationToken, nombre de
cadena)
{
_entrada = entrada;
Salida = new BlockingCollection<TOutput>[_input.Length]; for (int i = 0; i < Output.Length; i+
+)
Salida[i] = nulo == entrada[i] ? nulo
: new BlockingCollection<TOutput>(Recuento);
_procesador = procesador;
_ficha = ficha;
206
Machine Translated by Google
Capítulo 10
Nombre = nombre;
_rnd = new Random(DateTime.Now.Millisecond);
}
PipelineWorker público (
BlockingCollection<TInput>[] entrada,
Procesador Action<TInput>,
Token de cancelación,
nombre de cadena)
{
_entrada = entrada;
_outputProcessor = renderizador; _ficha
= ficha; Nombre =
nombre; Salida =
nulo; _rnd = new
Random(DateTime.Now.Millisecond); }
Public BlockingCollection<TOutput>[] Salida { get; conjunto privado;
}
cadena pública Nombre { obtener; conjunto privado; }
Ejecutar vacío público ()
{
WriteLine($"{Nombre} se está ejecutando");
while (!_input.All(bc => bc.IsCompleted) && !
_token.IsCancellationRequested)
{
Elemento recibido de entrada;
int i = BlockingCollection<TInput>.TryTakeFromAny(
_entrada, fuera artículo recibido, 50, _token);
si (yo >= 0) {
si (salida! = nulo) {
TOutput elemento de salida = _procesador (elemento recibido);
BlockingCollection<TOutput>.AddToAny( Output,
outputItem); WriteLine($"{Name}
" +
envió {outputItem} al siguiente, en $"thread id
{CurrentThread.ManagedThreadId}");
Sleep(TimeSpan.FromMilliseconds(_rnd.Next(200))); }
demás
207
Machine Translated by Google
Patrones de programación en paralelo
{
_outputProcessor(elemento recibido);
}
}
demás
{
Dormir (TimeSpan.FromMillisegundos (50));
}
} si (salida! = nulo) {
foreach (var bc en Salida) bc.CompleteAdding();
}
}
}
4. Agregue el siguiente fragmento de código dentro del método principal :
var cts = new CancellationTokenSource();
Tarea.Ejecutar(() => {
if (ReadKey().KeyChar == 'c') cts.Cancel(); }, cts.Token);
var sourceArrays = new BlockingCollection<int>[CollectionsNumber];
for (int i = 0; i < sourceArrays.Length; i++) {
sourceArrays[i] = new BlockingCollection<int>(Recuento);
}
var convertToDecimal = nuevo PipelineWorker<int, decimal>
(
arreglos de fuentes,
n => Convertir.ADecimal(n*100),
cts.token,
"Convertidor decimal"
);
var stringifyNumber = new PipelineWorker<decimal, cadena>
(
convertToDecimal.Output,
208
Machine Translated by Google
Capítulo 10
s => $"{s.ToString("C", CultureInfo.GetCultureInfo("en us"))}", cts.Token, "String Formatter" );
var outputResultToConsole = new PipelineWorker<cadena, cadena> (
stringifyNumber.Output, s =>
WriteLine($"El resultado final es {s} en el subproceso " $"id +
{CurrentThread.ManagedThreadId}"), cts.Token, "Console Output");
intentar
{
Parallel.Invoke(
() =>
CreateInitialValues(sourceArrays, cts), () => convertToDecimal.Run(),
() => stringifyNumber.Run(), () =>
outputResultToConsole.Run()
);
} captura (AgregateException ae) {
foreach (var ex en ae.InnerExceptions)
WriteLine(ex.Message + ex.StackTrace);
}
si (cts.Token.IsCancellationRequested) {
WriteLine("¡La operación ha sido cancelada! Presione ENTER para salir.");
}
demás
{
WriteLine("Presione ENTER para salir.");
}
LeerLínea();
5. Ejecute el programa.
209
Machine Translated by Google
Patrones de programación en paralelo
Cómo funciona...
En el ejemplo anterior, implementamos uno de los escenarios de programación paralela más comunes. Imagine
que tenemos algunos datos que tienen que pasar por varias etapas de cálculo, lo que lleva una cantidad de
tiempo significativa. El último cálculo requiere los resultados del primero, por lo que no podemos ejecutarlos en
paralelo.
Si tuviéramos un solo elemento para procesar, no habría muchas posibilidades para mejorar el rendimiento.
Sin embargo, si ejecutamos muchos elementos a través del mismo conjunto de etapas de cálculo, podemos usar
una técnica de canalización paralela. Esto significa que no tenemos que esperar a que todos los elementos
pasen por la primera etapa de cálculo para pasar a la siguiente. Basta con tener un solo elemento que termine
la etapa; lo movemos a la siguiente etapa, y mientras tanto, el siguiente elemento está en la etapa anterior, y así
sucesivamente. Como resultado, casi tenemos un procesamiento paralelo desplazado por el tiempo requerido para
que el primer elemento pase por la primera etapa de cálculo.
Aquí, usamos cuatro colecciones para cada etapa de procesamiento, lo que ilustra que también podemos
procesar cada etapa en paralelo. El primer paso que hacemos es brindar la posibilidad de cancelar todo el proceso
presionando la tecla C. Creamos un token de cancelación y ejecutamos una tarea separada para monitorear la
clave C. Luego, definimos nuestro pipeline. Consta de tres etapas principales.
La primera etapa es donde colocamos los números iniciales en las primeras cuatro colecciones que sirven
como fuente de elementos para la última canalización. Este código está dentro del bucle Parallel.For del
método CreateInitialValues , que a su vez está dentro de la instrucción Parallel.Invoke , ya que ejecutamos
todas las etapas en paralelo; la etapa inicial también corre en paralelo.
La siguiente etapa es definir nuestros elementos de canalización. La lógica se define dentro de la
clase PipelineWorker . Inicializamos el trabajador con la colección de entrada, proporcionamos una función
de transformación y luego ejecutamos el trabajador en paralelo con los otros trabajadores. Así, definimos dos
trabajadores, o filtros, porque filtran la secuencia inicial. Uno de ellos convierte un número entero en un valor decimal
y el segundo convierte un decimal en una cadena.
Finalmente, el último trabajador simplemente imprime cada cadena entrante en la consola. En todos los lugares,
proporcionamos una ID de subproceso en ejecución para ver cómo funciona todo. Además de esto, agregamos
demoras artificiales, por lo que el procesamiento del elemento será más natural, ya que realmente usamos cálculos pesados.
Como resultado, vemos exactamente el comportamiento esperado. Primero, se crean algunos artículos en las
colecciones iniciales. Luego, vemos que el primer filtro comienza a procesarlos y, a medida que se procesan, el
segundo filtro comienza a funcionar. Finalmente, el elemento va al último trabajador que lo imprime en la consola.
Implementación de canalización paralela con TPL
Flujo de datos
Esta receta muestra cómo implementar un patrón de canalización paralela con la ayuda de la biblioteca TPL
DataFlow.
210
Machine Translated by Google
Capítulo 10
preparándose
Para comenzar esta receta, deberá ejecutar Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter10\Recipe3.
Cómo hacerlo...
Para comprender cómo implementar Parallel Pipeline con TPL DataFlow, realice los siguientes
pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. Agregue referencias al paquete Microsoft TPL DataFlow NuGet. Siga estos pasos para
hazlo:
1. Haga clic derecho en la carpeta Referencias en el proyecto y seleccione Administrar
Paquetes NuGet... opción de menú.
2. Ahora, agregue sus referencias preferidas al paquete Microsoft TPL DataFlow NuGet.
Puede usar la opción de búsqueda en el cuadro de diálogo Administrar paquetes NuGet
de la siguiente manera:
211
Machine Translated by Google
Patrones de programación en paralelo
3. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Globalization; utilizando
System.Threading; utilizando
System.Threading.Tasks; utilizando
System.Threading.Tasks.Dataflow; usando System.Console
estático; usando System.Threading.Thread
estático;
4. Agregue el siguiente fragmento de código debajo del método principal :
Tarea asincrónica estática ProcessAsynchronously() {
var cts = new CancellationTokenSource(); Random _rnd = new
Random(DateTime.Now.Millisecond);
Tarea.Ejecutar(() =>
{
if (ReadKey().KeyChar == 'c') cts.Cancel(); },
cts.Token);
var inputBlock = new BufferBlock<int>( new DataflowBlockOptions
{ BoundedCapacity = 5, CancellationToken = cts.Token });
var convertToDecimalBlock = new TransformBlock<int, decimal>(
norte =>
{
resultado decimal = Convert.ToDecimal(n * 100); WriteLine($"Decimal
Converter envió {resultado} a la siguiente etapa en
" +
$"id del subproceso {CurrentThread.ManagedThreadId}");
Dormir(TimeSpan.FromMilliseconds(_rnd.Next(200))); resultado devuelto;
}
, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 4, CancellationToken = cts.Token });
var stringifyBlock = new TransformBlock<decimal, cadena>(
norte =>
{
resultado de cadena = $"{n.ToString("C", CultureInfo.
GetCultureInfo("eses"))}";
212
Machine Translated by Google
Capítulo 10
WriteLine($"String Formatter envió {resultado} a la siguiente etapa en el id de subproceso
{CurrentThread.ManagedThreadId}");
Dormir(TimeSpan.FromMilliseconds(_rnd.Next(200))); resultado devuelto;
}
, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 4, CancellationToken = cts.Token });
var outputBlock = new ActionBlock<cadena>(
s =>
{
WriteLine($"El resultado final es {s} en la identificación del subproceso
{CurrentThread.ManagedThreadId}");
}
, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 4, CancellationToken = cts.Token });
inputBlock.LinkTo(convertToDecimalBlock, nuevas opciones de enlace de flujo de datos
{PropagateCompletion = true});
convertToDecimalBlock.LinkTo(stringifyBlock, new DataflowLinkOptions
{ PropagateCompletion = true });
stringifyBlock.LinkTo(outputBlock, new DataflowLinkOptions {
PropagateCompletion = true });
intentar
Parallel.For(0, 20, new ParallelOptions { MaxDegreeOfParallelism =
4, CancellationToken = cts.Token }
, yo =>
{
WriteLine($"agregado {i} a los datos de origen en la identificación del hilo
{CurrentThread.ManagedThreadId}");
inputBlock.SendAsync(i).GetAwaiter().GetResult(); }); inputBlock.Complete();
espera outputBlock.Completion;
WriteLine("Presione ENTER para salir.");
} captura (Excepción Cancelada por Operación) {
WriteLine("¡La operación ha sido cancelada! Presione ENTER para salir."); }
LeerLínea();
}
213
Machine Translated by Google
Patrones de programación en paralelo
5. Agregue el siguiente fragmento de código dentro del método principal :
var t = ProcessAsynchronously();
t.GetAwaiter().GetResult();
6. Ejecute el programa.
Cómo funciona...
En la receta anterior, implementamos un patrón de canalización paralela para procesar elementos a través de
etapas secuenciales. Es un problema bastante común, y una de las formas propuestas para programar dichos
algoritmos es usar una biblioteca TPL DataFlow de Microsoft. Se distribuye a través de NuGet y es fácil de
instalar y usar en su aplicación.
La biblioteca TPL DataFlow contiene diferentes tipos de bloques que se pueden conectar entre sí de
diferentes maneras y formar procesos complicados que pueden ser parcialmente paralelos y secuenciales
cuando sea necesario. Para ver parte de la infraestructura disponible, implementemos el escenario anterior con
la ayuda de la biblioteca TPL DataFlow.
Primero, definimos los diferentes bloques que estarán procesando nuestros datos. Tenga en cuenta que
estos bloques tienen diferentes opciones que se pueden especificar durante su construcción; pueden ser muy
importantes. Por ejemplo, pasamos el token de cancelación a cada bloque que definimos, y cuando
señalamos la cancelación, todos dejan de funcionar.
Comenzamos nuestro proceso con BufferBlock, limitamos su capacidad a 5 elementos como máximo. Este
bloque contiene elementos para pasarlos a los siguientes bloques del flujo. Lo restringimos a la capacidad de
cinco artículos, especificando el valor de la opción BoundedCapacity . Esto significa que cuando haya cinco
elementos en este bloque, dejará de aceptar nuevos elementos hasta que uno de los elementos existentes
pase a los siguientes bloques.
El siguiente tipo de bloque es TransformBlock. Este bloque está destinado a un paso de transformación de datos.
Aquí, definimos dos bloques de transformación; uno de ellos crea decimales a partir de números enteros y el
segundo crea una cadena a partir de un valor decimal. Podemos usar la opción MaxDegreeOfParallelism para este
bloque, especificando el máximo de subprocesos de trabajo simultáneos.
El último bloque es del tipo ActionBlock . Este bloque ejecutará una acción específica en cada elemento
entrante. Usamos este bloque para imprimir nuestros elementos en la consola.
Ahora, vinculamos estos bloques con la ayuda de los métodos LinkTo . Aquí, tenemos un flujo de datos secuencial
fácil, pero es posible crear esquemas que son más complicados.
Aquí, también proporcionamos DataflowLinkOptions con la propiedad PropagateCompletion establecida en
verdadero. Esto significa que cuando se complete el paso, automáticamente propagará sus resultados y
excepciones a la siguiente etapa. Luego, comenzamos a agregar elementos al bloque de búfer en
paralelo, llamando al método Complete del bloque , cuando terminamos de agregar nuevos elementos.
Luego, esperamos a que se complete el último bloque. En el caso de una cancelación, manejamos
OperationCancelledException y cancelamos todo el proceso.
214
Machine Translated by Google
Capítulo 10
Implementando Map/Reduce con PLINQ
Esta receta describirá cómo implementar el patrón Map/Reduce mientras se usa PLINQ.
preparándose
Para comenzar esta receta, deberá ejecutar Visual Studio 2015. No hay otros requisitos previos.
El código fuente de esta receta se puede encontrar en BookSamples\Chapter10\Recipe4.
Cómo hacerlo...
Para comprender cómo implementar Map/Reduce con PLINQ, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el sistema;
usando System.Collections.Generic; utilizando
System.IO; utilizando
System.Linq; utilizando
System.Net.Http; usando
Sistema.Texto; utilizando
System.Threading.Tasks;
usando Newtonsoft.Json;
usando System.Console estático;
3. Agregue referencias al paquete Newtonsoft.Json NuGet y System.Net.
Ensamblaje HTTP .
4. Agregue el siguiente fragmento de código debajo del método principal :
delimitadores estáticos char[] = { ' ', ',', ';', ':', '\"', '.' };
async static Task<cadena> ProcessBookAsync(
string bookContent, string title, HashSet<string> palabras vacías)
{
usando (var lector = new StringReader(bookContent)) {
var consulta = lector.EnumLines()
.ComoParalelo()
.SelectMany(línea => línea.Dividir(delimitadores))
.Mapa reducido(
215
Machine Translated by Google
Patrones de programación en paralelo
palabra => nuevo[] { palabra.ToLower() }, clave =>
clave, g =>
nuevo[] { nuevo { Palabra = g.Clave, Contador = g.Contador()
} }
)
.Listar();
var palabras =
consulta .Dónde(elemento
=> !cadena.IsNullOrWhiteSpace(elemento.Palabra) && !
stopwords.Contains(elemento.Palabra))
.OrderByDescending(elemento => elemento.Cuenta);
var sb = nuevo StringBuilder();
sb.AppendLine($"'{title}' estadísticas del libro");
sb.AppendLine("Diez palabras más usadas en este libro: "); foreach (var w en
palabras.Take(10)) {
sb.AppendLine($"Word: '{w.Word}', veces usadas: '{w.
Contar}'");
}
sb.AppendLine($"Palabras únicas utilizadas: {query.Count()}");
volver sb.ToString();
}
}
async static Task<string> DownloadBookAsync(string bookUrl) {
usando (var cliente = nuevo HttpClient()) {
volver esperar cliente.GetStringAsync(bookUrl);
}
}
asíncrono estático Task<HashSet<string>> DownloadStopWordsAsync() {
URL de cadena =
"https://raw.githubusercontent.com/6/stopwords/master/
stopwordsall.json";
usando (var cliente = nuevo HttpClient())
216
Machine Translated by Google
Capítulo 10
{
intentar
{
var contenido = espera cliente.GetStringAsync(url);
var palabras =
JsonConvert.DeserializeObject
<Diccionario<cadena, cadena[]>>(contenido);
devolver nuevo HashSet<cadena>(palabras["en"]);
}
atrapar
{
devolver nuevo HashSet<cadena>();
}
}
}
5. Agregue el siguiente fragmento de código dentro del método principal :
var listaLibros = new Diccionario<cadena, cadena>() {
["Moby Dick; O, La ballena de Herman Melville"] = "http://www.gutenberg.org/
cache/epub/2701/pg2701.txt",
["Las aventuras de Tom Sawyer de Mark Twain"] = "http://
www.gutenberg.org/cache/epub/74/pg74.txt",
["La isla del tesoro de Robert Louis Stevenson"] = "http://
www.gutenberg.org/cache/epub/120/pg120.txt",
["El retrato de Dorian Gray de Oscar Wilde"] = "http://www.gutenberg.org/
cache/epub/174/pg174.txt"
};
HashSet<string> stopwords = DownloadStopWordsAsync().GetAwaiter().
ObtenerResultado();
var salida = nuevo StringBuilder();
Parallel.ForEach(booksList.Keys, key => {
var bookContent = DownloadBookAsync(booksList[clave])
.GetAwaiter().GetResult();
217
Machine Translated by Google
Patrones de programación en paralelo
resultado de cadena = ProcessBookAsync(contenidolibro, clave, palabras vacías)
.GetAwaiter().GetResult();
salida.Append(resultado);
salida.AppendLine(); });
Escribir (salida. ToString ()); LeerLínea();
6. Agregue el siguiente fragmento de código después de la definición de clase de programa :
Extensiones de clase estática
{
pública estática ParallelQuery<TResult> MapReduce<TSource, TMapped,
TClave, TResultado>(
esta fuente ParallelQuery<TSource>,
Func<TSource, IEnumerable<TMapped>> mapa,
Func<TMapped, TKey> keySelector,
Func<IGrouping<TKey, TMapped>, IEnumerable<TResult>> reduce)
{
volver source.SelectMany(mapa)
.GroupBy(keySelector)
.SelectMany(reducir);
}
public static IEnumerable<string> EnumLines(este lector de StringReader) {
mientras (verdadero)
{
línea de cadena = lector.ReadLine(); if (nulo ==
línea) rendimiento de ruptura;
línea de retorno de rendimiento;
}
}
}
7. Ejecute el programa.
218
Machine Translated by Google
Capítulo 10
Cómo funciona...
Las funciones Map/Reduce son otro importante patrón de programación paralela. Son adecuados para un
programa pequeño y grandes cálculos multiservidor. El significado de este patrón es que tiene dos funciones
especiales para aplicar a sus datos. La primera de ellas es la función Mapa . Toma un conjunto de datos
iniciales en forma de lista de clave/valor y produce otra secuencia de clave/valor, transformando los datos a un
formato cómodo para su posterior procesamiento. Luego, usamos otra función, llamada Reducir. La función Reduce
toma el resultado de la función Map y lo transforma en el conjunto de datos más pequeño posible que realmente
necesitamos. Para entender cómo funciona este algoritmo, veamos la receta anterior.
Aquí vamos a analizar el texto de cuatro libros clásicos. Vamos a descargar los libros del sitio del proyecto
Gutenberg (www.gutenberg.org), que puede solicitar un captcha si emite muchas solicitudes de red y, por lo
tanto, rompe la lógica del programa de esta muestra. Si ve elementos HTML en la salida del programa, abra
una de las URL del libro en el navegador y complete el captcha. Lo siguiente que debemos hacer es cargar una
lista de palabras en inglés que vamos a saltar al analizar el texto. En este ejemplo, intentamos cargar una lista de
palabras codificadas en JSON desde GitHub y, en caso de falla, solo obtenemos una lista vacía.
Ahora, prestemos atención a nuestra implementación Map/Reduce como un método de extensión PLINQ en la clase
PLINQExtensions . Usamos SelectMany para transformar la secuencia inicial en la secuencia que necesitamos
aplicando la función Map . Esta función produce varios elementos nuevos a partir de un elemento de secuencia. Luego,
elegimos cómo agrupamos la nueva secuencia con la función keySelector , y usamos GroupBy con esta clave
para producir una secuencia intermedia de clave/valor. Lo último que hacemos es aplicar Reduce a la
secuencia agrupada resultante para obtener el resultado.
Luego, ejecutamos el procesamiento de todos nuestros libros en paralelo. Cada subproceso de trabajo de
procesamiento genera la información resultante en una cadena y, una vez que todos los trabajadores están
completos, imprimimos esta información en la consola. Hacemos esto para evitar la salida simultánea de la consola,
cuando el texto de cada trabajador se superpone y hace que la información resultante sea ilegible. En cada
proceso de trabajo, dividimos el texto del libro en una secuencia de líneas de texto, cortamos cada línea en
secuencias de palabras y le aplicamos nuestra función MapReduce . Usamos la función Map para transformar cada
palabra en minúsculas y usarla como clave de agrupación. Luego, definimos la función Reducir como una
transformación del elemento de agrupación en un par de valores clave, que tiene el elemento Palabra que contiene
una palabra única que se encuentra en el texto y el elemento Contar , que tiene información sobre cuántas veces
se ha repetido esta palabra. usado. El paso final es la materialización de nuestra consulta con la llamada al método
ToList , ya que necesitamos procesar esta consulta dos veces. Luego, usamos nuestra lista de palabras vacías para
eliminar palabras comunes de nuestras estadísticas y crear un resultado de cadena con el título del libro, las 10
palabras principales utilizadas en el libro y la frecuencia de una palabra única en el libro.
219
Machine Translated by Google
Machine Translated by Google
Hay más
11
En este capítulo, veremos un nuevo paradigma de programación en el sistema operativo Windows 10.
Además, aprenderá a ejecutar programas .NET en OS X y Linux.
En este capítulo aprenderá las siguientes recetas:
f Uso de un temporizador en una aplicación de la Plataforma universal de Windows
f Uso de WinRT desde aplicaciones habituales f Uso
de BackgroundTask en aplicaciones de la plataforma universal de Windows f Ejecución de una
aplicación .NET Core en OS X
f Ejecutar una aplicación .NET Core en Ubuntu Linux
Introducción
Microsoft lanzó la primera versión beta pública de Windows 8 en la conferencia Build el 13 de septiembre de 2011.
El nuevo sistema operativo trató de abordar casi todos los problemas que tenía Windows mediante la introducción de
funciones como una interfaz de usuario receptiva adecuada para dispositivos de tableta con toque, menor consumo de
energía , un nuevo modelo de aplicación, nuevas API asincrónicas y mayor seguridad.
El núcleo de las mejoras de la API de Windows fue un nuevo sistema de componentes multiplataforma, WinRT,
que es un desarrollo lógico de COM. Con WinRT, un programador puede usar código C++ nativo, C# y .NET, e incluso
JavaScript y HTML para desarrollar aplicaciones. Otro cambio es la introducción de una tienda de aplicaciones
centralizada, que antes no existía en la plataforma Windows.
Al ser una nueva plataforma de aplicaciones, Windows 8 tenía compatibilidad con versiones anteriores y nos permitía
ejecutar las aplicaciones habituales de Windows. Esto condujo a una situación en la que había dos clases principales de
aplicaciones: las aplicaciones de la Tienda Windows, donde los nuevos programas se distribuyen a través de la Tienda
Windows, y las aplicaciones clásicas habituales que no habían cambiado desde la versión anterior de Windows.
221
Machine Translated by Google
Hay más
Sin embargo, Windows 8 fue solo el primer paso hacia el nuevo modelo de aplicación. Microsoft recibió muchos
comentarios de los usuarios y quedó claro que las aplicaciones de Windows Store eran demasiado diferentes de lo que
la gente estaba acostumbrada. Además de eso, había un sistema operativo de teléfono inteligente separado, Windows 8
Phone, que tenía una tienda de aplicaciones diferente y un conjunto de API ligeramente diferente. Esto hizo que un
desarrollador de aplicaciones creara dos aplicaciones separadas para plataformas de escritorio y teléfonos inteligentes.
Para mejorar la situación, se presentó el nuevo sistema operativo Windows 10 como una plataforma unificada para
todos los dispositivos con Windows. Existe una única tienda de aplicaciones que admite todas las familias de
dispositivos y, ahora, es posible crear una aplicación que funcione en teléfonos, tabletas y computadoras de
escritorio. Por lo tanto, las aplicaciones de la Tienda Windows ahora se denominan aplicaciones de la Plataforma
universal de Windows (aplicaciones UWP). Esto, por supuesto, significa muchas limitaciones para su aplicación: no
debe usar ninguna API específica de la plataforma y, como programador, debe cumplir con reglas específicas. El
programa tiene que responder en un tiempo limitado para iniciarse o finalizar, manteniendo la capacidad de respuesta
de todo el sistema operativo y otras aplicaciones. Para ahorrar batería, sus aplicaciones ya no se ejecutan en
segundo plano de forma predeterminada; en lugar de eso, se suspenden y de hecho dejan de ejecutarse.
Las nuevas API de Windows son asincrónicas y solo puede usar funciones de API incluidas en la lista blanca en
su aplicación. Por ejemplo, ya no puede crear una nueva instancia de clase Thread . En su lugar, debe usar un grupo
de subprocesos administrado por el sistema. Muchas de las API habituales ya no se pueden usar y debe estudiar
nuevas formas de lograr los mismos objetivos que antes.
Pero esto no es todo. Microsoft comenzó a comprender que también es importante admitir sistemas operativos
distintos de Windows. Y ahora, puede escribir aplicaciones multiplataforma utilizando un nuevo subconjunto de .NET
llamado .NET Core. Su fuente se puede encontrar en GitHub y es compatible con plataformas como OS X y Linux.
Puede usar cualquier editor de texto, pero le sugiero que eche un vistazo a Visual Studio Code, un nuevo editor
de código liviano y multiplataforma que se ejecuta en OS X y Linux y comprende bien la sintaxis de C#.
En este capítulo, veremos en qué se diferencia una aplicación de la Plataforma universal de Windows de la aplicación
habitual de Windows y cómo podemos utilizar algunos de los beneficios de WinRT de las aplicaciones habituales.
También veremos un escenario simplificado de una aplicación de la Plataforma universal de Windows con notificaciones
en segundo plano. También aprenderá a ejecutar un programa .NET en OS X y Linux.
222
Machine Translated by Google
Capítulo 11
Uso de un temporizador en una ventana universal
Aplicación de plataforma
Esta receta le muestra cómo usar un temporizador simple en las aplicaciones de la plataforma universal de Windows.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015 y el sistema operativo Windows 10. No se requieren otros
requisitos previos. El código fuente de esta receta se puede encontrar en BookSamples\Chapter11\Recipe1.
Cómo hacerlo...
Para comprender cómo usar un temporizador en una aplicación de la Tienda Windows, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto C# Blank App (Universal Windows) en la carpeta
Windows\Universal .
223
Machine Translated by Google
Hay más
2. Si se le solicita que habilite el modo desarrollador para Windows 10, debe habilitarlo en el panel de control.
3. Luego, confirme que está seguro de que desea activar el modo desarrollador.
4. En el archivo MainPage.xaml , agregue el atributo Name al elemento Grid :
<Grid Name="Grid" Background="{Recurso estático
AplicaciónPáginaFondoTemaBrush}">
5. En el archivo MainPage.xaml.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
Windows.UI.Xaml; utilizando
Windows.UI.Xaml.Controls; utilizando Windows.UI.Xaml.Navigation;
6. Agregue el siguiente fragmento de código encima de la definición del constructor de MainPage :
privado de solo lectura DispatcherTimer _timer; _ticks de int
privado;
224
Machine Translated by Google
Capítulo 11
7. Reemplace el constructor MainPage() con el siguiente fragmento de código:
página principal pública ()
{
InicializarComponente(); _timer =
new DispatcherTimer(); _marcas = 0;
8. Agregue el método OnNavigatedTo() en la definición del constructor MainPage :
invalidación protegida void OnNavigatedTo(NavigationEventArgs e) { }
9. Agregue el siguiente fragmento de código dentro del método OnNavigatedTo :
base.OnNavigatedTo(e);
Cuadrícula.Niños.Borrar();
var commonPanel = nuevo StackPanel
{
Orientación = Orientación.Vertical, HorizontalAlignment
= HorizontalAlignment.Center };
var buttonPanel = new StackPanel
{
Orientación = Orientación.Horizontal, HorizontalAlignment
= HorizontalAlignment.Center };
var bloque de texto = nuevo bloque de texto
{
Text = "Aplicación de temporizador de muestra",
FontSize = 32,
HorizontalAlignment = HorizontalAlignment.Center, Margin = new
Thickness(40) };
var timerTextBlock = nuevo TextBlock
{
Text = "0",
FontSize = 32,
HorizontalAlignment = HorizontalAlignment.Center, Margin = new
Thickness(40) };
225
Machine Translated by Google
Hay más
var timerStateTextBlock = nuevo TextBlock
{
Texto = "El temporizador está habilitado",
Tamaño de fuente = 32,
HorizontalAlignment = HorizontalAlignment.Center, Margen = nuevo Grosor
(40) };
var startButton = nuevo botón { Contenido = "Inicio",
Tamaño de fuente = 32};
var stopButton = nuevo botón { Contenido = "Detener",
Tamaño de fuente = 32};
buttonPanel.Children.Add(startButton);
buttonPanel.Children.Add(stopButton);
commonPanel.Children.Add(textBlock);
commonPanel.Children.Add(timerTextBlock);
commonPanel.Children.Add(timerStateTextBlock);
commonPanel.Children.Add(buttonPanel);
_timer.Interval = TimeSpan.FromSeconds(1); _timer.Tick +=
(remitente, eventArgs) => {
timerTextBlock.Text = _ticks.ToString(); _garrapatas++; }; _temporizador.Inicio();
startButton.Click += (remitente, eventArgs) => {
timerTextBlock.Text = "0";
_temporizador.Inicio();
_marcas = 1;
timerStateTextBlock.Text = "El temporizador está habilitado"; };
stopButton.Click += (remitente, eventArgs) => {
_timer.Stop();
timerStateTextBlock.Text = "El temporizador está deshabilitado"; };
Grid.Children.Add(commonPanel);
226
Machine Translated by Google
Capítulo 11
10. Haga clic con el botón derecho en el proyecto en Visual Studio Solution Explorer y seleccione Implementar.
11. Ejecute el programa.
Cómo funciona...
Cuando se ejecuta el programa, crea una instancia de una clase MainPage . Aquí, creamos una instancia de
DispatcherTimer en el constructor e inicializamos el contador de marcas en 0. Luego, en el controlador de eventos
OnNavigatedTo , creamos nuestros controles de interfaz de usuario y vinculamos los botones de inicio y detención a las
expresiones lambda correspondientes, que contienen las lógicas de inicio y detención .
Como puede ver, el controlador de eventos del temporizador funciona directamente con los controles de la interfaz
de usuario. Esto está bien porque DispatcherTimer se implementa de tal manera que los controladores del evento Tick
del temporizador son ejecutados por el subproceso de la interfaz de usuario. Sin embargo, si ejecuta el programa y luego
cambia a otra cosa y luego cambia al programa después de un par de minutos, puede notar que el contador de segundos
está muy por detrás de la cantidad de tiempo real que pasó. Esto sucede porque las aplicaciones de la plataforma universal
de Windows tienen ciclos de vida completamente diferentes.
Tenga en cuenta que las aplicaciones de la plataforma universal de Windows se comportan
de forma muy parecida a las aplicaciones de las plataformas de teléfonos inteligentes y
tabletas. En lugar de ejecutarse en segundo plano, se suspenden después de un
tiempo, lo que significa que en realidad se congelan hasta que el usuario vuelve a cambiar a ellos.
Tiene un tiempo limitado para guardar el estado actual de la aplicación antes de que
se suspenda y puede restaurar el estado cuando las aplicaciones se ejecutan
nuevamente.
Si bien este comportamiento podría ahorrar energía y recursos de la CPU, crea dificultades significativas para las
aplicaciones del programa que se supone que deben realizar algún procesamiento en segundo plano. Windows 10 tiene
un conjunto de API especiales para programar este tipo de aplicaciones. Pasaremos por tal escenario más adelante en
este capítulo.
Uso de WinRT desde aplicaciones habituales
Esta receta le muestra cómo crear una aplicación de consola que podrá usar la API de WinRT.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015 y el sistema operativo Windows 10. No hay otros requisitos
previos. El código fuente de esta receta se puede encontrar en BookSamples\Chapter11\Recipe2.
227
Machine Translated by Google
Hay más
Cómo hacerlo...
Para comprender cómo usar WinRT desde las aplicaciones habituales, realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto de aplicación de consola C#.
2. Haga clic derecho en el proyecto creado en Visual Studio Solution Explorer y seleccione el
Opción de menú Descargar Proyecto… .
3. Haga clic derecho en el proyecto descargado y seleccione el menú Edit ProjectName.csproj
opción.
4. Agregue el siguiente código XML debajo del elemento <TargetFrameworkVersion> :
<TargetPlatformVersion>10.0</TargetPlatformVersion>
5. Guarde el archivo .csproj , haga clic con el botón derecho en el proyecto descargado en Visual Studio Solution
Explorer y seleccione la opción de menú Recargar proyecto .
6. Haga clic con el botón derecho en el proyecto y seleccione Agregar referencia de la biblioteca principal en
Ventanas. Luego, haga clic en el botón Examinar .
7. Vaya a C:\Archivos de programa (x86)\Windows Kits\10\UnionMetadata
y haga clic en Windows.winmd.
8. Navegue a C:\Program Files\Reference Assemblies\Microsoft\ Framework\.NETCore\v4.5 y haga clic
en System.Runtime.
Archivo WindowsRuntime.dll .
9. En el archivo Program.cs , agregue las siguientes directivas using :
utilizando el
sistema; utilizando
System.IO; utilizando System.Threading.Tasks;
utilizando Windows.Almacenamiento;
10. Agregue el siguiente fragmento de código debajo del método principal :
tarea asincrónica estática Procesamiento asincrónico () {
carpeta StorageFolder = KnownFolders.DocumentsLibrary;
if (carpeta en espera.DoesFileExistAsync("test.txt")) {
var fileToDelete = await folder.GetFileAsync( "test.txt"); aguardar
archivoParaEliminar.DeleteAsync(
StorageDeleteOption.PermanentDelete);
}
var archivo = esperar carpeta.CreateFileAsync("test.txt",
CreationCollisionOption.ReplaceExisting);
228
Machine Translated by Google
Capítulo 11
usando (var stream = esperar archivo.OpenAsync(FileAccessMode.
Leer escribir))
usando (var escritor = new StreamWriter(stream.AsStreamForWrite())) {
esperar escritor.WriteLineAsync("Contenido de prueba"); espera
escritor.FlushAsync();
}
usando (var stream = await file.OpenAsync(FileAccessMode.Read)) usando (var reader = new
StreamReader(stream.AsStreamForRead())) {
contenido de cadena = espera lector.ReadToEndAsync();
Console.WriteLine(contenido);
}
Console.WriteLine("Enumeración de la estructura de carpetas:");
var itemsList = espera carpeta.GetItemsAsync(); foreach (elemento var en
lista de elementos) {
if (elemento es StorageFolder) {
Console.WriteLine("{0} carpeta", elemento.Nombre);
}
demás
{
Console.WriteLine(elemento.Nombre);
}
}
}
11. Agregue el siguiente fragmento de código al método principal :
var t = procesamiento asincrónico ();
t.GetAwaiter().GetResult(); Consola.WriteLine();
Console.WriteLine("Presione
ENTER para continuar");
Consola.ReadLine();
12. Agregue el siguiente fragmento de código debajo de la definición de clase de programa :
Extensiones de clase estática
{
Tarea asincrónica estática pública <bool> DoesFileExistAsync (este
Carpeta StorageFolder, cadena fileName)
{
intentar
229
Machine Translated by Google
Hay más
esperar carpeta.GetFileAsync(fileName);
devolver verdadero;
} captura (Excepción de archivo no encontrado)
{
falso retorno;
}
}
}
13. Ejecute el programa.
Cómo funciona...
Aquí, usamos una forma bastante complicada de consumir la API de WinRT desde una aplicación de consola .NET
común. Desafortunadamente, no todas las API disponibles funcionarán en este escenario, pero aun así, podría ser útil
para trabajar con sensores de movimiento, servicios de ubicación GPS, etc.
Para hacer referencia a WinRT en Visual Studio, editamos manualmente el archivo .csproj , especificando la plataforma
de destino para la aplicación como Windows 10. Luego, hacemos referencia manualmente a Windows.winmd para obtener
acceso a las API de Windows 10 y System.Runtime.WindowsRuntime.dll para aproveche la implementación del método
de extensión GetAwaiter para las operaciones asincrónicas de WinRT. Esto nos permite usar await en las API de WinRT
directamente. También hay una conversión hacia atrás. Cuando creamos una biblioteca de WinRT, tenemos que exponer
la familia de interfaces IAsyncOperation nativas de WinRT para operaciones asincrónicas, de modo que puedan consumirse
desde JavaScript y C++ de forma independiente del lenguaje.
Las operaciones de archivo en WinRT son bastante autodescriptivas; aquí, tenemos operaciones asíncronas de creación
y eliminación de archivos. Aún así, las operaciones de archivos en WinRT contienen restricciones de seguridad, lo que lo
alienta a usar carpetas especiales de Windows para su aplicación y no le permite trabajar con cualquier ruta de archivo
en su unidad de disco.
Usando BackgroundTask en Universal
Aplicaciones de la plataforma Windows
Esta receta lo guía a través del proceso de creación de una tarea en segundo plano en una aplicación de la
Plataforma universal de Windows, que actualiza el mosaico activo de la aplicación en un escritorio.
preparándose
Para seguir esta receta, necesitará Visual Studio 2015 y el sistema operativo Windows 10. No hay otros requisitos
previos. El código fuente de esta receta se puede encontrar en BookSamples\Chapter11\Recipe3.
230
Machine Translated by Google
Capítulo 11
Cómo hacerlo...
Para comprender cómo usar BackgroundTask en las aplicaciones de la plataforma universal de Windows,
realice los siguientes pasos:
1. Inicie Visual Studio 2015. Cree un nuevo proyecto C# Blank App (Universal Windows)
en la carpeta Windows\Universal . Si necesita habilitar el modo de desarrollador de Windows 10,
consulte la receta Uso de un temporizador en una aplicación de la Tienda Windows para obtener
instrucciones detalladas.
2. Abra el archivo Package.appxmanifest . En la pestaña Declaraciones , agregue Tareas en segundo
plano a Declaraciones admitidas. En Propiedades, verifique las propiedades admitidas
Evento del sistema y Temporizador y establezca el nombre del Punto de entrada en
YourNamespace.TileSchedulerTask. YourNamespace debe ser el espacio de nombres de
su aplicación.
231
Machine Translated by Google
Hay más
3. En el archivo MainPage.xaml , inserte el siguiente código XAML en el elemento Grid :
<Margen del panel de pila="50">
<TextBlock Name="Reloj"
Texto="HH:mm"
AlineaciónHorizontal="Centro"
VerticalAlignment="Centro"
Estilo = "{StaticResource HeaderTextBlockStyle}"/>
</StackPanel>
4. En el archivo MainPage.xaml.cs , agregue las siguientes directivas using :
utilizando el sistema;
utilizando System.Diagnostics; utilizando
System.Globalization; utilizando System.Linq;
utilizando System.Xml.Linq;
utilizando
Windows.ApplicationModel.Background; utilizando
Windows.Data.Xml.Dom; utilizando
Windows.System.UserProfile; usando
Windows.UI.Notificaciones; utilizando
Windows.UI.Xaml; usando
Windows.UI.Xaml.Controls; utilizando
Windows.UI.Xaml.Navigation;
5. Agregue el siguiente fragmento de código sobre la definición del constructor de MainPage :
cadena de const privada TASK_NAME_USERPRESENT =
"TileSchedulerTask_UserPresent"; cadena const
privada TASK_NAME_TIMER =
"TileSchedulerTask_Timer";
privado de solo lectura CultureInfo _cultureInfo; privado de solo lectura
DispatcherTimer _timer;
6. Reemplace el constructor de MainPage con el siguiente fragmento de código:
página principal pública ()
{
InicializarComponente();
cadena idioma = GlobalizationPreferences.Languages.First(); _cultureInfo = new CultureInfo(idioma);
_timer = new DispatcherTimer(); _timer.Interval =
TimeSpan.FromSeconds(1); _timer.Tick += (remitente, e) =>
UpdateClockText(); }
232
Machine Translated by Google
Capítulo 11
7. Agregue el siguiente fragmento de código encima del método OnNavigatedTo :
Vacío privado UpdateClockText() {
Clock.Text =
DateTime.Now.ToString( _cultureInfo.DateTimeFormat.FullDateTimePattern);
}
asíncrono estático privado void CreateClockTask() {
Resultado de BackgroundAccessStatus = esperar a
BackgroundExecutionManager.RequestAccessAsync();
if (resultado == BackgroundAccessStatus.
AllowedMayUseActiveRealTimeConnectivity || resultado == Estado de
acceso en segundo plano.
PermitidoConAlwaysOnRealTimeConnectivity)
{
TileSchedulerTask.CreateSchedule();
Asegúrese de que el usuario presente la tarea ();
AsegurarTareaTemporizador();
}
}
vacío estático privado GarantizarUserPresentTask () {
foreach (tarea var en BackgroundTaskRegistration.AllTasks)
if (tarea.Valor.Nombre == NOMBRE_TAREA_USUARIOPRESENTE)
devolver;
var builder = new BackgroundTaskBuilder(); builder.Name =
TASK_NAME_USERPRESENT; constructor.TaskEntryPoint =
(typeof(TileSchedulerTask)).FullName;
constructor.SetTrigger(nuevo SystemTrigger(
SystemTriggerType.UserPresent, falso));
constructor.Registrar();
}
vacío estático privado GarantizarTarea de tiempo () {
foreach (tarea var en BackgroundTaskRegistration.AllTasks)
if (tarea.Valor.Nombre == NOMBRE_TAREA_TEMPORIZADOR)
devolver;
233
Machine Translated by Google
Hay más
var builder = new BackgroundTaskBuilder(); constructor.Nombre =
NOMBRE_TAREA_TEMPORIZADOR;
builder.TaskEntryPoint =
(typeof( TileSchedulerTask)).FullName;
constructor.SetTrigger(nuevo TimeTrigger(180, falso)); constructor.Registrar();
8. Agregue el siguiente fragmento de código al método OnNavigatedTo :
_temporizador.Inicio();
ActualizarTextoReloj();
CrearTareaReloj();
9. Agregue el siguiente fragmento de código debajo de la definición de la clase MainPage :
clase pública sellada TileSchedulerTask: IBackgroundTask {
public void Ejecutar (IBackgroundTaskInstance taskInstance) {
var aplazamiento = taskInstance.GetDeferral(); CrearPrograma();
aplazamiento.Complete();
vacío estático público CreateSchedule () {
var tileUpdater = TileUpdateManager.
CreateTileUpdaterForApplication();
var planificadoActualizado = tileUpdater.
GetScheduledTileNotifications();
FechaHora ahora = FechaHora.Ahora; Fecha
y hora planTill = now.AddHours(4);
DateTime updateTime = new DateTime(ahora.Año, ahora.Mes, ahora.Día, ahora.Hora,
ahora.Minuto, 0).AddMinutes(1); if (plannedUpdated.Count > 0) updateTime =
plannedUpdated.Select(x =>
x.DeliveryTime.DateTime).Union(new[] { updateTime
}).Máximo();
XmlDocument documentNow = GetTilenotificationXml(ahora);
tileUpdater.Update(new TileNotification(documentNow) { ExpirationTime =
now.AddMinutes(1) });
234
Machine Translated by Google
Capítulo 11
for (var startPlanning = updateTime;
startPlanning <planTill; empezar a planificar =
startPlanning.AddMinutes(1))
{
Debug.WriteLine(iniciarPlanificación);
Debug.WriteLine(planTill);
intentar
Documento XmlDocument = GetTilenotificationXml(
comenzar a planificar);
var notificación programada = nuevo
ScheduledTileNotification(documento,
nuevo DateTimeOffset (startPlanning))
{
ExpirationTime = startPlanning.AddMinutes(1) };
tileUpdater.AddToSchedule(notificaciónprogramada);
} catch (excepción ex) {
Depurar.WriteLine("Error: " + ej.Mensaje);
}
}
}
privado estático XmlDocument GetTilenotificationXml(
fecha y hora fecha y hora)
{
cadena idioma =
GlobalizationPreferences.Languages.First();
var cultureInfo = new CultureInfo(idioma);
cadena fechabreve = fechaHora.ToString(
culturaInfo.DateTimeFormat.ShortTimePattern); cadena longDate =
dateTime.ToString(
culturaInfo.DateTimeFormat.LongDatePattern);
var document = XElement.Parse(string.Format(@"<tile> <visual>
<plantilla de enlace=""TileSquareText02"">
<texto id=""1"">{0}</texto> <texto
id=""2"">{1}</texto>
235
Machine Translated by Google
Hay más
</binding>
<binding template=""TileWideText01"">
<texto id=""1"">{0}</texto> <texto
id=""2"">{1}</texto> <texto id=""3""></
texto> <texto id=""4""></texto> </
enlace> </visual>
</tile>", fechacorta, fechalarga));
devolver documento.ToXmlDocument();
}
}
public static class DocumentExtensions {
XmlDocument estático público ToXmlDocument (este
XElemento xDocumento)
{
var xmlDocumento = new XmlDocumento();
xmlDocumento.LoadXml(xDocumento.ToString()); devolver documento
xml;
}
}
10. Ejecute el programa.
Cómo funciona...
El programa anterior muestra cómo crear una tarea basada en el tiempo en segundo plano y cómo mostrar las
actualizaciones de esta tarea en un mosaico en vivo en el menú de inicio de Windows 10. La programación de
aplicaciones de la plataforma universal de Windows es una tarea bastante desafiante en sí misma: debe preocuparse
por la suspensión/restauración de una aplicación y muchas otras cosas. Aquí nos vamos a concentrar en nuestra
tarea principal, dejando atrás las cuestiones secundarias.
Nuestro objetivo principal es ejecutar algún código cuando la aplicación en sí no está en primer plano. Primero, creamos
una implementación de la interfaz IBackgroundTask . Este es nuestro código, y se llamará al método Run cuando
obtengamos una señal de activación. Es importante que si el método Run contiene código asíncrono con await , tenemos
que usar un objeto de aplazamiento especial como se muestra en la receta para especificar explícitamente cuándo
comenzamos y finalizamos la ejecución del método Run . En nuestro caso, la llamada al método es síncrona, pero para
ilustrar este requisito, trabajamos con el objeto de aplazamiento.
236
Machine Translated by Google
Capítulo 11
Dentro de nuestra tarea en el método Run , creamos un conjunto de actualizaciones de mosaicos cada minuto
durante 4 horas y lo registramos en TileUpdateManager con la ayuda de la clase ScheduledTaskNotification . Un
mosaico utiliza un formato XML especial para especificar exactamente cómo debe colocarse el texto en él. Cuando
activamos nuestra tarea desde el sistema, programa actualizaciones de mosaicos de un minuto para las próximas 4
horas. Luego, necesitamos registrar nuestra tarea en segundo plano. Hacemos esto dos veces; un registro
proporciona un activador UserPresent , lo que significa que esta tarea se activará cuando un usuario inicie sesión.
El siguiente activador es un activador de tiempo, que ejecuta la tarea una vez cada 3 horas.
Cuando el programa se ejecuta, crea un temporizador, que se ejecuta cuando la aplicación está en
primer plano. Al mismo tiempo, intenta registrar tareas en segundo plano; para registrar estas tareas, el
programa necesita el permiso del usuario y mostrará un diálogo solicitando permisos del usuario. Ahora, hemos
programado actualizaciones de mosaicos en vivo para las próximas 4 horas. Si cerramos nuestra aplicación,
el mosaico en vivo seguirá mostrando la nueva hora cada minuto. En las próximas 3 horas, el activador de tiempo
ejecutará nuestra tarea en segundo plano una vez más y programaremos otra actualización de mosaico en vivo.
Ejecución de una aplicación .NET Core en OS X
Esta receta muestra cómo instalar una aplicación .NET Core en OS X y cómo compilar y ejecutar una aplicación
de consola .NET.
preparándose
Para seguir esta receta, necesitará un sistema operativo Mac OS X. No hay otros requisitos previos. El código
fuente de esta receta se puede encontrar en BookSamples\Chapter11\ Recipe4.
Cómo hacerlo...
Para comprender cómo ejecutar aplicaciones .NET Core, realice los siguientes pasos:
1. Instale .NET Core en su máquina OS X. Puede visitar http://dotnet.github. io/primeros pasos/ y siga las
instrucciones de instalación allí. Dado que .NET Core se encuentra en la etapa de prelanzamiento, los
escenarios de instalación y uso podrían cambiar antes de que se publique este libro. Consulte las
instrucciones del sitio en ese caso.
2. Una vez que haya descargado el archivo .pkg , mantenga presionada la tecla Control mientras lo abre. Va a
desbloqueará el archivo y le permitirá instalarlo.
3. Una vez que haya instalado el paquete, deberá instalar OpenSSL. La forma más fácil es instalar primero
el administrador de paquetes homebrew. Abra la ventana del terminal y ejecute el siguiente comando:
/usr/bin/ruby e "$(curl fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
237
Machine Translated by Google
Hay más
4. Luego, puede instalar OpenSSL escribiendo lo siguiente:
instalación de cerveza abre SSL
5. También existe el pequeño problema de que .NET Core en el momento de escribir esto necesita aumentar
el límite de archivos abiertos. Esto se puede lograr escribiendo lo siguiente:
sudo sysctl w kern.maxfiles=20480
sudo sysctl w kern.maxfilesperproc=18000
sudo ulimit S n 2048
6. Ahora ha instalado .NET Core y está listo para comenzar. Para crear una aplicación Hello World de muestra,
puede crear un directorio y crear una aplicación vacía:
mkdir hola mundo
cd hola mundo
dotnet nuevo
7. Verifiquemos si la aplicación predeterminada funciona. Para ejecutar el código, tenemos que
restaurar dependencias y compilar y ejecutar la aplicación. Para lograr esto, escriba los siguientes
comandos:
restauración de dotnet
ejecutar dotnet
8. Ahora, intentemos ejecutar un código asíncrono. En el archivo Program.cs , cambie
el código a lo siguiente:
utilizando el sistema;
utilizando System.Threading.Tasks; usando
System.Console estático; espacio de nombres
OSXConsoleApplication {
programa de clase {
vacío estático principal (cadena [] argumentos) {
WriteLine(.Aplicación NET Core en OS X");
RunCodeAsync().GetAwaiter().GetResult();
} tarea asincrónica estática RunCodeAsync() {
intentar
resultado de cadena = esperar GetInfoAsync ("Async 1");
WriteLine(resultado);
resultado = esperar GetInfoAsync("Async 2");
WriteLine(resultado);
238
Machine Translated by Google
Capítulo 11
} catch (excepción ex) {
WriteLine(ex);
}
} Tarea asincrónica estática<cadena> GetInfoAsync(nombre de la cadena) {
WriteLine($"¡Tarea {nombre} iniciada!"); esperar
Task.Delay(TimeSpan.FromSeconds(2)); si (nombre == "Asíncrono 2")
lanzar una nueva excepción ("¡Boom!");
devolver
$"¡Tarea {nombre} completada con éxito!"
// + $"Id. de subproceso {System.Threading.Thread.CurrentThread.
ID de subproceso administrado}".
;
}
}
}
9. Ejecute el programa con el comando dotnet run .
Cómo funciona...
Aquí, descargamos un archivo .pkg con el paquete de instalación de .NET Core del sitio y lo instalamos.
También instalamos la biblioteca OpenSSL usando el administrador de paquetes homebrew (que también
se instala). Además de eso, aumentamos el límite de archivos abiertos en OS X para poder restaurar las
dependencias de .NET Core.
Luego, creamos una carpeta separada para la aplicación .NET Core, creamos una aplicación de
consola en blanco y verificamos si todo funciona bien con la restauración de dependencias y la
ejecución del código.
Finalmente, creamos un código asíncrono simple e intentamos ejecutarlo. Debería funcionar bien, mostrando
los mensajes de que la primera tarea se completó con éxito. La segunda tarea provocó una excepción,
que se manejó correctamente. Pero si intenta descomentar una línea que pretende mostrar la información
específica del subproceso, el código no se compilará, ya que .NET Core no es compatible con las API de
subprocesos.
239
Machine Translated by Google
Hay más
Ejecución de una aplicación .NET Core en Ubuntu
Linux
Esta receta muestra cómo instalar una aplicación .NET Core en Ubuntu y cómo compilar y ejecutar una aplicación de
consola .NET.
preparándose
Para seguir esta receta, necesitará un sistema operativo Ubuntu Linux 14.04. No hay otros requisitos previos. El código
fuente de esta receta se puede encontrar en BookSamples\Capítulo11\Recipe5.
Cómo hacerlo...
Para comprender cómo ejecutar aplicaciones .NET Core, realice los siguientes pasos:
1. Instale .NET Core en su máquina Ubuntu. Puede visitar http://dotnet.github. io/primeros pasos/ y siga las
instrucciones de instalación allí. Dado que .NET Core se encuentra en la etapa de prelanzamiento, los
escenarios de instalación y uso podrían cambiar para cuando se publique este libro. Consulte las
instrucciones del sitio en ese caso.
2. Primero, abra una ventana de terminal y ejecute los siguientes comandos:
sudo sh c 'echo "deb [arch=amd64] http://aptmo.trafficmanager.net/repos/dotnet/ trusty main" > /etc/
apt/sources.list.d/
dotnetdev.list'
sudo aptkey adv keyserver aptmo.trafficmanager.net recvkeys 417A0893
sudo aptobtener actualización
3. Luego, puede instalar .NET Core escribiendo lo siguiente en la ventana del terminal:
sudo aptget install dotnet=1.0.0.0013311
4. Ahora, ha instalado .NET Core y está listo para comenzar. Para crear una aplicación Hello World de
muestra, puede crear un directorio y crear una aplicación vacía:
mkdir hola mundo
cd hola mundo
dotnet nuevo
5. Verifiquemos si la aplicación predeterminada funciona. Para ejecutar el código, tenemos que
restaurar dependencias y compilar y ejecutar la aplicación. Para lograr esto, escriba los siguientes
comandos:
restauración de dotnet
ejecutar dotnet
240
Machine Translated by Google
Capítulo 11
6. Ahora, intentemos ejecutar un código asíncrono. En el archivo Program.cs , cambie el
código a lo siguiente:
utilizando el sistema;
utilizando System.Threading.Tasks; usando
System.Console estático; espacio de nombres
OSXConsoleApplication {
programa de clase {
vacío estático principal (cadena [] argumentos) {
WriteLine(.Aplicación NET Core en Ubuntu");
RunCodeAsync().GetAwaiter().GetResult();
} tarea asincrónica estática RunCodeAsync() {
intentar
resultado de cadena = esperar GetInfoAsync ("Async 1");
WriteLine(resultado); resultado
= esperar GetInfoAsync("Async 2");
WriteLine(resultado);
} catch (excepción ex) {
WriteLine(ex);
}
} Tarea asincrónica estática<cadena> GetInfoAsync(nombre de la cadena) {
WriteLine($"¡Tarea {nombre} iniciada!"); esperar
Task.Delay(TimeSpan.FromSeconds(2)); si (nombre == "Asíncrono 2")
lanzar una nueva excepción ("¡Boom!");
devolver
$"¡Tarea {nombre} completada con éxito!"
// + $"Id. de subproceso {System.Threading.Thread.CurrentThread.
ID de subproceso administrado}".
;
}
}
}
7. Ejecute el programa con el comando dotnet run .
241
Machine Translated by Google
Hay más
Cómo funciona...
Aquí, comenzamos con la configuración de la fuente aptget que aloja los paquetes de .NET Core que
necesitamos. Esto es necesario ya que, en el momento de escribir este artículo, es posible que .NET Core para
Linux no se haya lanzado. Por supuesto, cuando se produzca el lanzamiento, entrará en los feeds aptget
normales y no tendrá que agregarle feeds personalizados. Después de completar esto, usamos aptget para
instalar la versión actualmente en funcionamiento de .NET Core.
Luego, creamos una carpeta separada para la aplicación .NET Core, creamos una aplicación de consola en
blanco y verificamos si todo funciona bien con la restauración de dependencias y la ejecución del código.
Finalmente, creamos un código asíncrono simple e intentamos ejecutarlo. Debería funcionar bien, mostrando
mensajes de que la primera tarea se completó con éxito y la segunda tarea provocó una excepción, que se
manejó correctamente. Pero si intenta descomentar una línea que pretende mostrar la información específica del
subproceso, el código no se compilará, ya que .NET Core no es compatible con las API de subprocesos.
242
Machine Translated by Google
Índice
Obtención de resultados de tareas
simbolos asincrónicas , con operador de espera 9698
Aplicación .NET Core en desventajas de los
ejecución, en OS X 237239 en operadores asíncronos
ejecución, en Ubuntu Linux 240242 Grupo 95 método vacío
de subprocesos .NET 48 asíncrono trabajando con
111114 operaciones
A atómicas
alrededor de 28 realizando 2831
capa de abstracción 67 Construcción AutoResetEvent
Conversión de
usando 3436
patrón APM , a la tarea 7578
esperar desventajas
Método AsOrdered 151
del operador 95 tipo
funciones asíncronas sobre
dinámico, usando con 118122 usado,
94, 95 creando
para obtener resultados de tareas asincrónicas
94 servidor 9698
HTTP asíncrono y escritura de cliente 187189
utilizado, para la ejecución de tareas
aplicaciones de asincrónicas en paralelo 102104
operaciones de E/S asíncronas
usando, en expresión lambda 98, 99 usando,
181183 colección
con las consiguientes tareas asincrónicas
Observable asíncrona , 100102
convirtiendo a 162165 operaciones
asíncronas
creando, Rx usó 177180
B
excepciones, manejando 104107 Tarea de fondo
publicando, en grupo de subprocesos 52, 53 usando, en aplicaciones de la Plataforma
generalización de procesamiento Universal de Windows 230236
asíncrono , BlockingCollection utilizado Componente BackgroundWorker usando
136139 6366
implementando, ConcurrentQueue usó Construcción de
127129 barrera usando
ordenar, cambiar, ConcurrentStack usado 3941 ejecución de
130132 operaciones básicas , con tarea 7072
Modelo de programación asíncrona (APM) 49
243
Machine Translated by Google
BlockingCollection diseño de tipo disponible
alrededor de personalizado 114117
124 usados, para generalizar el procesamiento Observable personalizado
asíncrono 136139 usados, escritura 165168
para implementar Parallel
Tubería 205210 D
base de datos
C
trabajar con, asincrónicamente 190193 paralelismo
C# de datos 142 partición de
subproceso, creando 26 datos
devolución de llamada gestión en consulta PLINQ 153157 grado de
registrando 58 paralelismo
opción de cancelación y el grupo de subprocesos
implementando el uso del 5456
contexto de sincronización capturado 5658 , delega
evitando 107111 alrededor de 48 invocando, en el grupo
Palabra clave de bloqueo de subprocesos 4951 patrón de bloqueo verificado
de C# utilizada, para bloquear 1921 dos veces 204
mecánica de cierre 53 tipo dinámico usando, con operador de espera 118121
colección 127 de bloqueo de grano
grueso mi
conversión, a asíncrono
Observable 162165 Conversión de
Tiempo de ejecución de lenguaje común (CLR) 48 patrones EAP , a la tarea 79, 80
Comparar e intercambiar (CAS) 124 Método de puesta en cola 124
ConcurrentBag Patrón asíncrono basado en eventos (EAP) 65 controladores
de eventos 65
utilizado, para crear un rastreador escalable 132136
eventos 65
Diccionario concurrente
alrededor de 124 manejo de
usando 125127 excepciones 2426
Cola simultánea manejo, en operaciones asíncronas
usado, para implementar el procesamiento 104107 manejo, en
asíncrono 127129 consulta PLINQ 151153
pila concurrente
utilizado, para cambiar el procesamiento asíncrono F
pedido 130132
archivos
tareas asincrónicas consiguientes
trabajar con, asincrónicamente 183186
operador de espera, usando con el interruptor
técnica de bloqueo de grano fino 127
de contexto 100102 28
Primero en entrar, primero en salir (FIFO) 124
continuación 75
Construcción CountDownEvent
GRAMO
utilizando 38, 39
creación de agregador Enlace al sitio
personalizado , para consulta PLINQ 157159 web de Gutenberg 219
244
Machine Translated by Google
H PAG
construcciones híbridas 28 ejecución de tareas asincrónicas
paralelas , operador en espera utilizado 102104
I clase paralela
usando 143145
Subprocesos de E/S Extensiones de Marco Paralelo (PFX) 141
48 iteradores 95
Parallel Pipeline
alrededor de
k 200 implementando, con
BlockingColección 205210
construcciones en modo kernel 28
implementando, con el paso de parámetros TPL DataFlow
210214 , al
L subproceso 1618
expresión lambda sobre PLINQ
50 operador usado, para implementar
de espera, usando 98, 99 Mapa/Reducir 215219
Último en entrar, primero en salir (LIFO) 124 Consulta PLINQ
Estados compartidos con evaluación agregador personalizado, creando 157160
perezosa que implementan 200204 particiones de datos, administrando 153157
LazyInitializer.EnsureInitialized método 204 Consultas LINQ excepciones, manejando 151153
usando, contra la parámetros, ajustando 148151 agrupando
colección observable 174176 47 enfoque
Paralelización de basado en pull 161 enfoque basado
consultas LINQ en push 162
145148
R
METRO
Extensiones reactivas (Rx)
Construcción ManualResetEventSlim usando alrededor de 161, 162
3638 utilizadas, para crear operaciones
Mapear/Reducir patrón asincrónicas 177180
alrededor de Construcción ReaderWriterLockSlim
200 implementando, con construcción de monitor usando 4143
PLINQ 215219
bloqueo con 2224 S
construcción de exclusión mutua
rastreador escalable
usando 31, 32
creando, ConcurrentBag usado 132136
Construcción SemaphoreSlim usando
O 3234 objeto de
colección observable estado compartido 199
Consultas LINQ, usando contra 174176 Protocolo simple de acceso a objetos (SOAP) 197
Objeto observable que Construcción SpinWait
crea 172174 Aplicación usando 44, 45
OS paralelismo estructurado 142
X .NET Core, que ejecuta 237239 Tipo de sujetos
usando 168171
245
Machine Translated by Google
T tiempo de
espera usando, con grupo de subprocesos 5961
tarea temporizador
alrededor de 68 usando 6163
Patrón APM, convirtiendo a 7578 usando, en la aplicación Universal Windows
operaciones básicas, realizando con 7072 Platform 223227
combinando 7275 Flujo de datos TPL
creando 69, 70 alrededor de 200
patrón EAP, conversión a 79, 80 manejo usado, para implementar, Paralelo
de excepciones 83, 85 ejecución, Tubería 210214
en paralelo 8587 ajuste de Método TryDequeue 124
ejecución de Método TryPeek 124
tareas , con TaskScheduler 8791 paralelismo
de tareas 141 tu
Biblioteca paralela de tareas (TPL) 51
programador de tareas 70 ubuntu linux
hilo Aplicación .NET Core, ejecutando 240242
cancelar 810 Aplicación de plataforma universal de Windows
segundo plano 14, 15 BackgroundTask, usando 230236
cancelar opción, implementar 5658 crear, en C# 25 temporizador, usando
primer plano 14, 15 223227 paralelismo no estructurado
142 construcciones de modo de usuario 28
hacer, esperar 7, 8
parámetros, pasar
1618 pausar 6, 7 prioridad 1214 W
estado,
use el controlador
determinar 10 ,
de espera , con el grupo de subprocesos 5961
grupo de 11 subprocesos y grado
Fundación de comunicación de Windows
de paralelismo
(WCF) servicio
5456 operación asíncrona, publicación
alrededor de 183
52, 53 delegado, invocación 4951 tiempo de
llamando, asincrónicamente 194198
espera, uso de 5961 controlador
WinRT
de espera, uso de 5961
sobre 221
utilizando, de las aplicaciones habituales 227230
sincronización de subprocesos
Método WithExecutionMode 151
sobre 27
Construcción AutoResetEvent 3436 Método WithMergeOptions 151 subproceso
de trabajo 48
Construcción de barrera 3941
CountDownEvent construcción 38, 39
ManualResetEventSlim construcción 3638
Construcción mutex 31, 32
ReaderWriterLockSlim construcción 4143
Construcción SemaphoreSlim 3234
Construcción SpinWait 44, 45
246
Machine Translated by Google