Está en la página 1de 264

Machine Translated by Google

Machine Translated by Google

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  978­1­78588­125­1

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 /  7­Zip  para  Windows

f  Zipeg /  iZip /  UnRarX  para  Mac  f  7­Zip /  

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/submit­errata,  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/7­Zip  para  Windows  f  Zipeg/

iZip /  UnRarX  para  Mac  f  7­Zip/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  20­30  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  padre­hijo.  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[i­1]  =  
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[i­1],
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  %  

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  

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  
rx­main  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  Rx­XAML  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("es­es"))}­­";

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/
stopwords­all.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://apt­mo.trafficmanager.net/repos/dotnet/  trusty  main"  > /etc/
apt/sources.list.d/
dotnetdev.list'

sudo  apt­key  adv  ­­keyserver  apt­mo.trafficmanager.net  ­­recv­keys  417A0893

sudo  apt­obtener  actualización

3.  Luego,  puede  instalar .NET  Core  escribiendo  lo  siguiente  en  la  ventana  del  terminal:

sudo  apt­get  install  dotnet=1.0.0.001331­1

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  apt­get  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  apt­get  
normales  y  no  tendrá  que  agregarle  feeds  personalizados.  Después  de  completar  esto,  usamos  apt­get  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  96­98
Aplicación .NET  Core  en   desventajas  de  los  
ejecución,  en  OS  X  237­239  en   operadores  asíncronos  
ejecución,  en  Ubuntu  Linux  240­242  Grupo   95  método  vacío  
de  subprocesos .NET  48 asíncrono  trabajando  con  
111­114  operaciones  
A atómicas  
alrededor  de  28  realizando  28­31
capa  de  abstracción  67 Construcción  AutoResetEvent
Conversión  de  
usando  34­36  
patrón  APM ,  a  la  tarea  75­78
esperar  desventajas  
Método  AsOrdered  151  
del  operador  95  tipo  
funciones  asíncronas  sobre  
dinámico,  usando  con  118­122  usado,  
94,  95  creando  
para  obtener  resultados  de  tareas  asincrónicas  
94  servidor   96­98
HTTP  asíncrono  y  escritura  de  cliente  187­189  
utilizado,  para  la  ejecución  de  tareas  
aplicaciones  de   asincrónicas  en  paralelo  102­104
operaciones  de  E/S  asíncronas  
usando,  en  expresión  lambda  98,  99  usando,  
181­183  colección  
con  las  consiguientes  tareas  asincrónicas  
Observable  asíncrona ,   100­102
convirtiendo  a  162­165  operaciones  
asíncronas
creando,  Rx  usó  177­180  
B
excepciones,  manejando  104­107   Tarea  de  fondo
publicando,  en  grupo  de  subprocesos  52,  53 usando,  en  aplicaciones  de  la  Plataforma  
generalización  de  procesamiento   Universal  de  Windows  230­236
asíncrono ,  BlockingCollection  utilizado   Componente  BackgroundWorker  usando  
136­139 63­66
implementando,  ConcurrentQueue  usó   Construcción  de  
127­129 barrera  usando  
ordenar,  cambiar,  ConcurrentStack  usado   39­41  ejecución  de  
130­132 operaciones  básicas ,  con  tarea  70­72
Modelo  de  programación  asíncrona  (APM)  49

243
Machine Translated by Google

BlockingCollection   diseño  de  tipo  disponible  
alrededor  de   personalizado  114­117
124  usados,  para  generalizar  el  procesamiento   Observable  personalizado
asíncrono  136­139  usados,   escritura  165­168
para  implementar  Parallel
Tubería  205­210 D
base  de  datos
C
trabajar  con,  asincrónicamente  190­193  paralelismo  
C# de  datos  142  partición  de  
subproceso,  creando  2­6   datos
devolución  de  llamada gestión  en  consulta  PLINQ  153­157  grado  de  
registrando  58   paralelismo
opción  de  cancelación y  el  grupo  de  subprocesos  
implementando  el  uso  del   54­56  
contexto  de  sincronización  capturado  56­58 ,   delega  
evitando  107­111 alrededor  de  48  invocando,  en  el  grupo  
Palabra  clave  de  bloqueo   de  subprocesos  49­51  patrón  de  bloqueo  verificado  
de  C#  utilizada,  para  bloquear  19­21 dos  veces  204  
mecánica  de  cierre  53 tipo  dinámico  usando,  con  operador  de  espera  118­121
colección  127  de  bloqueo  de  grano  
grueso mi
conversión,  a  asíncrono
Observable  162­165 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  132­136
eventos  65  
Diccionario  concurrente
alrededor  de  124 manejo  de  

usando  125­127 excepciones  24­26  

Cola  simultánea manejo,  en  operaciones  asíncronas  

usado,  para  implementar  el  procesamiento   104­107  manejo,  en  

asíncrono  127­129 consulta  PLINQ  151­153
pila  concurrente
utilizado,  para  cambiar  el  procesamiento  asíncrono F
pedido  130­132
archivos
tareas  asincrónicas  consiguientes
trabajar  con,  asincrónicamente  183­186
operador  de  espera,  usando  con  el  interruptor  
técnica  de  bloqueo  de  grano  fino  127
de  contexto  100­102  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  157­159 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  102­104
I clase  paralela
usando  143­145
Subprocesos  de  E/S   Extensiones  de  Marco  Paralelo  (PFX)  141
48  iteradores  95
Parallel  Pipeline  
alrededor  de  
k 200  implementando,  con
BlockingColección  205­210
construcciones  en  modo  kernel  28
implementando,  con  el  paso  de  parámetros  TPL  DataFlow  
210­214 ,  al  
L subproceso  16­18

expresión  lambda  sobre   PLINQ  
50  operador   usado,  para  implementar

de  espera,  usando  98,  99 Mapa/Reducir  215­219

Último  en  entrar,  primero  en  salir  (LIFO)  124 Consulta  PLINQ

Estados  compartidos  con  evaluación   agregador  personalizado,  creando  157­160  

perezosa  que  implementan  200­204 particiones  de  datos,  administrando  153­157  

LazyInitializer.EnsureInitialized  método  204  Consultas  LINQ   excepciones,  manejando  151­153  

usando,  contra  la   parámetros,  ajustando  148­151  agrupando  

colección  observable  174­176   47  enfoque  
Paralelización  de   basado  en  pull  161  enfoque  basado  

consultas  LINQ   en  push  162

145­148
R
METRO
Extensiones  reactivas  (Rx)  
Construcción  ManualResetEventSlim  usando   alrededor  de  161,  162  

36­38 utilizadas,  para  crear  operaciones  

Mapear/Reducir  patrón   asincrónicas  177­180
alrededor  de   Construcción  ReaderWriterLockSlim

200  implementando,  con  construcción  de  monitor   usando  41­43
PLINQ  215­219
bloqueo  con  22­24 S
construcción  de  exclusión  mutua
rastreador  escalable
usando  31,  32
creando,  ConcurrentBag  usado  132­136
Construcción  SemaphoreSlim  usando  
O 32­34  objeto  de  
colección  observable estado  compartido  199

Consultas  LINQ,  usando  contra  174­176 Protocolo  simple  de  acceso  a  objetos  (SOAP)  197

Objeto  observable  que   Construcción  SpinWait  

crea  172­174  Aplicación   usando  44,  45  
OS   paralelismo  estructurado  142

X .NET  Core,  que  ejecuta  237­239 Tipo  de  sujetos  
usando  168­171

245
Machine Translated by Google

T tiempo  de  

espera  usando,  con  grupo  de  subprocesos  59­61  
tarea temporizador
alrededor  de  68 usando  61­63  
Patrón  APM,  convirtiendo  a  75­78   usando,  en  la  aplicación  Universal  Windows  
operaciones  básicas,  realizando  con  70­72   Platform  223­227
combinando  72­75   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  210­214
en  paralelo  85­87  ajuste  de   Método  TryDequeue  124
ejecución  de   Método  TryPeek  124
tareas ,  con  TaskScheduler  87­91  paralelismo  
de  tareas  141 tu
Biblioteca  paralela  de  tareas  (TPL)  51  
programador  de  tareas  70 ubuntu  linux
hilo Aplicación .NET  Core,  ejecutando  240­242  
cancelar  8­10   Aplicación  de  plataforma  universal  de  Windows  
segundo  plano  14,  15   BackgroundTask,  usando  230­236  
cancelar  opción,  implementar  56­58  crear,  en  C#  2­5   temporizador,  usando  
primer  plano  14,  15   223­227  paralelismo  no  estructurado  
142  construcciones  de  modo  de  usuario  28
hacer,  esperar  7,  8  
parámetros,  pasar  
16­18  pausar  6,  7  prioridad  12­14   W
estado,  
use  el  controlador  
determinar  10 ,  
de  espera ,  con  el  grupo  de  subprocesos  59­61
grupo  de  11  subprocesos  y  grado  
Fundación  de  comunicación  de  Windows
de  paralelismo  
(WCF)  servicio  
54­56  operación  asíncrona,  publicación  
alrededor  de  183
52,  53  delegado,  invocación  49­51  tiempo  de  
llamando,  asincrónicamente  194­198
espera,  uso  de  59­61  controlador  
WinRT
de  espera,  uso  de  59­61
sobre  221
utilizando,  de  las  aplicaciones  habituales  227­230
sincronización  de  subprocesos  
Método  WithExecutionMode  151
sobre  27
Construcción  AutoResetEvent  34­36 Método  WithMergeOptions  151  subproceso  
de  trabajo  48
Construcción  de  barrera  39­41
CountDownEvent  construcción  38,  39
ManualResetEventSlim  construcción  36­38
Construcción  mutex  31,  32
ReaderWriterLockSlim  construcción  41­43
Construcción  SemaphoreSlim  32­34
Construcción  SpinWait  44,  45

246
Machine Translated by Google

También podría gustarte