Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf ·...

79

Transcript of Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf ·...

Page 1: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales
Page 2: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Programación Paralela con OmniThreadLibrary

Primož Gabrijelčič and Juan Antonio Castillo H.

This book is for sale at http://leanpub.com/omnithreadlibrary-es

This version was published on 2013-04-22

This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process. LeanPublishing is the act of publishing an in-progress ebook using lightweight tools and many iterations to getreader feedback, pivot until you have the right book and build traction once you do.

©2012 - 2013 Primož Gabrijelčič and Juan Antonio Castillo H.

Page 3: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Tweet This Book!Please help Primož Gabrijelčič and Juan Antonio Castillo H. by spreading the word about this book on Twitter!

The suggested hashtag for this book is #omnithreadlibraryes.

Find out what other people are saying about the book by clicking on this link to search for this hashtag onTwitter:

https://twitter.com/search/#omnithreadlibraryes

Page 4: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Also By Primož Gabrijelčič

Parallel Programming with OmniThreadLibrary

A Smart Book

Параллельное программирование с OmniThreadLibrary

Page 5: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Índice general

Libro de Muestra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i

Créditos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ii

Traducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iiiAcerca de jachguate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iv

Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vConvenciones de Formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . viTrabajo en progreso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . viiAnuncios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . viii

Notas de la publicación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ix

1 Introducción a OmniThreadLibrary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11.1 Requerimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.2 Licencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.3 Instalación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

1.3.1 Instalación del Paquete de tiempo de Diseño . . . . . . . . . . . . . . . . . . . . . . . 51.4 ¿Por qué utilizar OmniThreadLibrary? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71.5 Tareas vs. Hilos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

2 Multi-hilos de alto nivel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92.1 Async . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

2.1.1 Manejo de Excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122.2 Async/Await . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142.3 For Each . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

2.3.1 Cooperación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162.3.2 Iterando sobre … . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

2.3.2.1 … Rangos de Números . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162.3.2.2 … Colecciones Enumerables . . . . . . . . . . . . . . . . . . . . . . . . . . . 172.3.2.3 … Colecciones enumerables multi-hilo . . . . . . . . . . . . . . . . . . . . . 182.3.2.4 … Colecciones con Bloqueo . . . . . . . . . . . . . . . . . . . . . . . . . . . 192.3.2.5 … Cualquier cosa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

2.3.3 Proporcionar una Entrada Externa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202.3.4 IOmniParallelLoop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222.3.5 Preservar el orden de salida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

Page 6: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

ÍNDICE GENERAL

2.3.6 Agregación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262.3.7 Cancelación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292.3.8 Inicialización y Finalización de Tarea . . . . . . . . . . . . . . . . . . . . . . . . . . . 292.3.9 Manejo de excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 312.3.10 Interioridades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31

2.3.10.1 Proveedor de datos de origen . . . . . . . . . . . . . . . . . . . . . . . . . . 352.3.10.2 Gestor de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372.3.10.3 Cola local . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 392.3.10.4 Ordenación de la salida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

2.3.11 Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44

3 Sincronización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 453.1 Critical Sections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46

3.1.1 IOmniCriticalSection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463.1.2 TOmniCS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473.1.3 Locked . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

3.1.3.1 ¿Por qué no utilizar TMonitor? . . . . . . . . . . . . . . . . . . . . . . . . . 51

4 How-to . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 534.1 Async/Await . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 554.2 QuickSort y Max Paralelo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59

4.2.1 QuickSort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 594.2.2 Max Paralelo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61

5 Aplicaciones Demo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63

Page 7: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Libro de MuestraEsta es solamente una muestra del libro real. Incluye partes seleccionadas de algunos capítulos.

Para verificar el estado del libro completo, en Inglés, visita el blog The Delphi Geek¹ y en Español, visita elblog de jachguate²

Puedes comprar el libro en LeanPub³.

¹http://thedelphigeek.org²http://jachguate.wordpress.com³http://leanpub.com/omnithreadlibrary-es

i

Page 8: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

CréditosEl autor desea agradecer a Gorazd Jernejc (alias GJ) por su contribución al proyecto OmniThreadLibrary.Gorazd, ¡OTL no sería lo mismo sin ti!

Parte del código ha sido proveído por Anton Alisov y Lee Nover. ¡Gracias!

Los siguientes programadores (en orden alfabético) han ayudado reportando y corrigiendo errores: ajasja,andi, Anton Alisov, dottor_jeckill, geskill, Hallvard Vassbotn, Jamie, M.J. Brandt, Mason Wheeler, Mayjest,meishier, morlic, Passella, Qmodem, Unspoken, Zarko.

Muchas gracias a Pierre le Riche, que escribió el hermoso manejador de memoria FastMM⁴ y me ha permitidoincluirlo en la distribución.

Página de cubierta (c) Dave Gingrich, http://www.flickr.com/photos/ndanger/2744507570/

⁴http://sourceforge.net/projects/fastmm/

ii

Page 9: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

TraducciónEste libro fue traducido por Juan Antonio Castillo Hernández⁵, también conocido como jachguate.

Un aspecto importante de la versión en Español, es que se ha conservado algunas palabras en Inglés, debido a

• la dificultad para expresar el mismo concepto en una o dos palabras en Español,• que tradicionalmente los programadores de habla hispana han adoptado el nombre en Inglés ycomprenden bien su significado, por lo que traducirlo al Español solo ocasionaría confusiones,

• que son nombres de abstracciones paralelas no solo en Delphi/OTL sino en otros entornos y aún no seha observado un concenso sobre la forma de llamar a estas en la literatura de habla hispana.

Entre estas palabras encontramos, en orden alfabético, a:

• Critical section: sección crítica es una primitiva de bloqueo utilizada en programas multi-hilo. http://es.wikipedia.org/wiki/Sección_crítica

• Deadlock: abrazo mortal o interbloqueo hace referencia a la situación en la qué dos o más accionescompetidoras están cada una esperando a que la otra termine, y por tanto, ninguna lo hace. http://es.wikipedia.org/wiki/Bloqueo_mutuo

• Data-aware: asociado a datos hace referencia al conjunto de controles asociados a datos de la VCL quese conectan a una fuente de datos a través de un TDataSource, y que usualmente llevan el prefijo DBen su nombre, tales como TDBGrid, TDBEdit, etc.

• Fork/Join: Bifurcar/Unir es una abstracción paralela tratada en su capítulo correspondiente.• Frame:marco hace referencia a una instancia de la clase TFrame o uno de sus descendientes, el popularcontenedor de la VCL. http://docwiki.embarcadero.com/Libraries/XE3/en/Vcl.Forms.TFrame.

• Framework: marco de trabajo http://es.wikipedia.org/wiki/Framework.• Pipeline: tubería es una abstracción tratada en su capítulo correspondiente.• … Pool: se refiere a una colección de … un recurso, que, previamente creado/configurado, se reutiliza alo largo de la vida del sistema para aumentar su eficiencia, pues dicha creación/configuración consumeun tiempo significativo en comparación con el tiempo de uso de cada llamada. A lo largo del libro, eltérmino se utiliza para varios tipos de recurso:

– computation pool: pool computacional véase la abstracción Fork/Join.– connection pool: pool de conexiones http://en.wikipedia.org/wiki/Thread_pool_pattern.– thread pool: pool de hilos http://es.wikipedia.org/wiki/Connection_pool.

• Thread-safe: seguro para hilos, lo que, dicho de un componente, biblioteca, función o fragmento decódigo, significa que producirá un resultado correcto si se ejecuta/utiliza concurrentemente desdevarios hilos. http://es.wikipedia.org/wiki/Seguridad_en_hilos.

• Token: señal, se refiere sobre todo al token de cancelación de la(s) tarea(s) de OTL, un mecanismo quepermite señalar o informar a una tarea que debe abortar su proceso.

⁵http://jachguate.wordpress.com

iii

Page 10: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Traducción iv

Acerca de jachguate

Juan Antonio Castillo Hernández es un consultor y capacitador de Delphi y otras herramientas originario deGuatemala, con 18+ años de experiencia en Tecnologías de la Información y desarrollo de software, nombradoEmbarcadero MVP (Profesional más valioso) en el 2012.

Ha sido moderador de los foros del Club Delphi, una activa comunidad de desarrolladores de habla hispana delas herramientas de Embarcadero, y fundador de la propuesta para crear StackOverflow, el sitio de preguntasy respuestas para programadores, en Español.

Desde su compañía, Castillo IT, brinda servicios de consultoría, asesoría y capacitación en diferentes temasrelacionados con IT. Es invitado de manera regular como expositor en eventos presenciales y en líneaorganizados por Embarcadero o sus asociados en América Latina.

Page 11: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

IntroducciónEste es un libro que trata sobre OmniThreadLibrary⁶, una biblioteca multi-hilos para el entorno de desarrolloEmbarcadero Delphi.

El libro asume que el lector tiene cierto entendimiento sobre la programación multi-hilos. Si eres nuevo enmulti-hilos, una introducción excelente es Multithreading - The Delphi Way⁷ de Martin Harvey. Ese libro esveterano, pero brillante.

Una vista más actualizada de las capacidades multi-hilos de Delphi ha sido publicada en Delphi XE2Foundations, Part 3⁸ de Chris Rolliston, la cual se encuentra ahora disponible en Amazon.

⁶http://otl.17slon.com⁷http://thaddy.co.uk/threads/⁸http://delphifoundations.com/

v

Page 12: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Introducción vi

Convenciones de Formato

Este libro cubre la última entrega oficial de OmniThreadLibrary (es decir, la entrega más reciente en GoogleCode⁹).

Cuando una parte del libro cubre alguna otra versión, una [etiqueta de versión] en superíndice indicará la versión oversiones relevantes. Se usa números de versión (ej. 2.1) para entregas antiguas y se usa números de revisiónde SVN (ej. r1184) para funcionalidad que se ha añadido después de la última entrega oficial.

Un número de revisión simple (ej. [r1184]) indica que el tema en cuestión se introdujo en esa versión y que aúnestá soportado en la entrega actual.

Un rango de dos versiones (ej. [1.0-1.1]) indica que el tema se introdujo en la primera versión (1.0 en esteejemplo) y que fue soportado hasta la segunda versión (1.1). Después de eso, el soporte para este tema fueremovido o ha sido cambiado de tal manera que se ha añadido una nueva sección para describir la nuevafuncionalidad.

⁹https://code.google.com/p/omnithreadlibrary/downloads/list

Page 13: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Introducción vii

Trabajo en progreso

Este libro es un trabajo en progreso — se va publicando a medida que se escribe. Cuando compras este libro, nosolo estas comprándolo ‘como está’ — en concordancia con el Manifiesto Lean Publishing¹⁰ también recibirástodas las versiones por venir del ebook, por siempre.

Para comprobar el estado del libro y para influir en el proceso de escritura votando sobre la importancia detemas concretos, visita el blog The Delphi Geek¹¹.

¹⁰http://leanpub.com/manifesto¹¹http://thedelphigeek.com

Page 14: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Introducción viii

Anuncios

Doy capacitación sobre diferentes temas de Delphi y Smart Mobile Studio. También estoy disponible paraconsultorías sobre Delphi o Smart Mobile Studio. Más información en http://www.glagolite.si/training.

Page 15: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Notas de la publicación2013-04-21

• Se ha completado la traducción del capítulo de sincronización, publicado en la versión en inglés endiciembre de 2012.

2012-10-12

• Se publica la primera traducción en Español, actualizada a la versión en Inglés publicada el 8 de octubrede 2012 (¡solo 4 días después!).

• Adaptado a la versión 3.02 de OTL.• Algunos textos del libro son generados en Inglés por LeanPub.com, estamos trabajando con ellos paralograr que el libro se produzca 100% en Español, incluyendo las partes generadas por el sistema depublicación.

ix

Page 16: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

1 Introducción a OmniThreadLibraryOmniThreadLibrary¹ es una biblioteca multi-hilos para Delphi, escrita casi en su totalidad por el autor deeste libro (mira los Créditos para una lista completa de colaboradores). OmniThreadLibrary se puede dividirclaramente en tres partes. En primer lugar, están las piezas básicas que pueden utilizarse tanto con lasconstrucciones de hilos de OmniThreadLibrary como con cualquier otro enfoque de hilado (ej. con TThreadde Delphi o con AsyncCalls²). Muchas de estas piezas básicas se describen en el capítuloMisceláneos, mientrasalgunas partes se encuentran en otros sitios del libro (Colecciones libres de Bloqueo, Colecciones bloqueables,Sincronización).

En segundo lugar, OmniThreadLibrary aporta un framework de multi-hilos a bajo nivel, que puede ser vistocomo un andamiaje que envuelve a la clase TThread. Este framework simplifica el paso de mensajes hacia ydesde hilos en segundo plano, iniciar tareas en segundo plano, utilizar pools de hilos y más.

En tercer lugar, OmniThreadLibrary introduce el concepto de multi-hilos de alto nivel. El framework de altonivel contiene soluciones prefabricadas (también llamadas abstracciones; ej. for paralelo, pipeline, fork/join…) que pueden ser utilizadas en tu código. La idea es que el usuario solo elija la abstracción apropiada y escribael código que hace el trabajo, mientras OmniThreadLibrary proporciona el framework que implementa laspartes difíciles de multi-hilos, se encarga de la sincronización, etc.

¹http://otl.17slon.com²http://andy.jgknet.de/blog/?page_id=100

1

Page 17: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Introducción a OmniThreadLibrary 2

1.1 Requerimientos

OmniThreadLibrary requiere como mínimo Delphi 2007 y no funciona con FreePascal. Esto se debe a quemuchas partes de OmniThreadLibrary hace uso de construcciones del lenguaje que aún no están disponiblesen el compilador de FreePascal.

El framework de multi-hilos de alto nivel requiere como mínimo Delphi 2009.

Actualmente, OmniThreadLibrary está dirigido solamente a instalaciones Windows. Se soporta tanto laplataforma de 32 bits como la de 64 bits.

Page 18: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Introducción a OmniThreadLibrary 3

1.2 Licencia

OmmiThradLibrary es una biblioteca de código abierto con licencia OpenBSD

..

NdelT El texto de la licencia no se traduce para evitar cambios en su significado, puedes encontrar másinformación sobre esta licencia en Español en Wikipedia/Licencia_BSD

http://es.wikipedia.org/wiki/Licencia_BSD

This software is distributed under the BSD license.

Copyright (c) 2012, Primoz Gabrijelcic

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permittedprovided that the following conditions are met:

• Redistributions of source code must retain the above copyright notice, this list of conditionsand the following disclaimer.

• Redistributions in binary form must reproduce the above copyright notice, this list ofconditions and the following disclaimer in the documentation and/or other materialsprovided with the distribution.

• The name of the Primoz Gabrijelcic may not be used to endorse or promote products derivedfrom this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “ASIS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULARPURPOSEAREDISCLAIMED. INNOEVENT SHALL THECOPYRIGHTOWNERORCONTRI-BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENTOF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESSINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHERIN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDINGNEGLIGENCEOROTHERWISE)ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THEPOSSIBILITY OF SUCH DAMAGE.

En pocas palabras, esto significa que:

1. Puedes usar la biblioteca en cualquier proyecto, libre, de código abierto o comercial, sin tener quemencionar mi nombre o el nombre de la biblioteca en algún lugar del proyecto, documentación o en elsitio web.

Page 19: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Introducción a OmniThreadLibrary 4

2. Puedes cambiar los fuentes para tu propio uso. También puedes poner una versión modificada en laweb, pero no debes remover mi nombre ni la licencia del código fuente.

3. No soy culpable si el software te explota en la cara. Recuerda, obtuviste OmniThreadLibrary gratis.

En caso que tu compañía quiera obtener un contrato de soporte para OmniThreadLibrary, por favorcontáctame.

Page 20: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Introducción a OmniThreadLibrary 5

1.3 Instalación

1. Descarga la última versión estable de Google Code³ o haz checkout del SVN trunk⁴. Por lo general esseguro seguir el trunk, ya que se hace commit solamente de código probado. [Al decir esto, debo admitirque de vez en cuando se cuela un bug o dos, pero estos son exterminados rápidamente].

2. Si has descargado la edición estable, desempácala en una carpeta.3. Agrega la carpeta donde [desempacaste la edición estable/has hecho checkout del trunk de SVN] a la

Library path de Delphi. También agrega la subcarpeta src al Library path. En caso que también usesunidades de mi proyecto GpDelphiUnits⁵, puedes ignorar las copias que se encuentran en la carpeta srcy usar las de GpDelphiUnits

4. Agrega las unidades necesarias a la clausula uses y ¡comienza a usar la biblioteca!

1.3.1 Instalación del Paquete de tiempo de Diseño

OmniThreadLibrary incluye un componente de tiempo de diseño (TOmniEventMonitor) que puede serutilizado para recibir mensajes enviados desde las tareas en segundo plano y para el monitoreo de lacreación/destrucción de hilos. Este se utiliza en algunas de las demos

Para compilar e instalar el paquete que contiene este componente, ha que seguir estos pasos:

• Desde Delphi, abrir la subcarpeta packages de la instalación de OmniThreadLibrary y seleccionar elarchivo OmniThreadLibraryPackages{VER}.groupproj (donde {VER} se corresponde con la versión deDelphi que usas; al escribir esto {VER} puede ser 2007, 2009, 2010, XE, XE2 o XE3).

• En la ventana del Project Manager encontrarás dos proyectos – OmniThreadLibraryRuntime{VER}.bply OmniThreadLibraryDesigntime{VER}.bpl. Si no está visible esta ventana, selecciona View, ProjectManager en el menú.

• Clic derecho en OmniThreadLibraryRuntime{VER}.bpl y selecciona Build en el menú emergente.• Clic derecho en OmniThreadLibraryDesigntime{VER}.bpl y selecciona Build en el menú emergente.

³http://code.google.com/p/omnithreadlibrary/downloads/list⁴http://code.google.com/p/omnithreadlibrary/source/checkout⁵http://code.google.com/p/gpdelphiunits/

Page 21: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Introducción a OmniThreadLibrary 6

• De nuevo clic derecho en OmniThreadLibraryDesigntime{VER}.bpl y selecciona Install en el menúemergente.

• Delphi notificará que el componente TOmniEventMonitor ha sido instalado

• Cierra el grupo de proyectos con File, Close All. Si Delphi pregunta si quieres guardar los archivosmodificados, elige No.

Debes repetir estos pasos siempre que la instalación de OmniThreadLibrary sea actualizada.

Page 22: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Introducción a OmniThreadLibrary 7

1.4 ¿Por qué utilizar OmniThreadLibrary?

OmniThreadLibrary aborda el problema de multi-hilos desde una perspectiva distinta a TThread. Mientrasel enfoque nativo de Delphi se orienta a la creación y gestión de hilos a muy bajo nivel, la guía de diseñoprincipal tras OmniThreadLibrary es: “Permitir al programador trabajar con hilos de manera tan fluida comosea posible.” El código idealmente te liberaría de toda la carga asociada comúnmente con multi-hilos.

OmniThreadLibrary fue diseñada para convertirse en una “VCL para multi-hilos” — una biblioteca queconvertiría las tareas típicas de multi-hilos en realmente sencillas, pero aún permitiría ir más allá y metertecon el código de multi-hilos a nivel del sistema operativo. A la vez que permite estos refinamientos a bajonivel, OmniThreadLibrary permite trabajar a un alto nivel de abstracción la mayor parte del tiempo.

Hay dos puntos importantes de distinción entre TThread y OmniThreadLibrary, ambos se explican másadelante en este capítulo. Uno es que OmniThreadLibrary se enfoca en tareas, no hilos y el otro es que enOmniThreadLibrary la mensajería trata de remplazar el bloqueo siempre que sea posible.

Al mover la mayoría del código crítico de multi-hilos a componentes re-utilizables (clases y abstracciones dealto nivel), OmniThreadLibrary permite escribir mejor código multi-hilos más rápido.

Page 23: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Introducción a OmniThreadLibrary 8

1.5 Tareas vs. Hilos

En OmniThreadLibrary no se crean hilos sino tareas. Una tarea puede ejecutarse en un nuevo hilo o en unhilo existente, tomado desde un pool de hilos.

Una tarea se crea utilizando la función CreateTask, que toma como parámetro un procedimiento global, unmétodo, una instancia de la clase TOmniWorker (o, comúnmente, un descendiente de esa clase) o un métodoanónimo (en Delphi 2009 o superior). CreateTask devuelve una interfaz IOmniTaskControl que puede serutilizada para controlar la tarea. Todas las tareas creadas están en estado suspendida y debe llamarse a Run

para activarla (o programarla vía Schedule para su ejecución en un pool de hilos).

La tarea tiene acceso a la interfaz IOmniTask y puede usarla para comunicarse con su propietario (la parte delprograma que la inició). Ambas interfaces se explican a detalle en el capítulo Multi-hilos a bajo nivel.

Las diferencias entre la Tarea y el Hilo se pueden resumir en unas pocas palabras.

La Tarea es la parte del código que se ha de ejecutar

El Hilo es el entorno de ejecución.

Tu te haces cargo de la Tarea, OmniThreadLibrary se hace cargo del Hilo.

Page 24: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

2 Multi-hilos de alto nivelHay que enfrentarlo — la programación multi-hilos es difícil. Es difícil diseñar un programa multi-hilos, esdifícil escribirlo y probarlo y es locamente difícil depurarlo. Para aliviar este problema, OmniThreadLibraryintroduce una serie de soluciones multi-hilo pre-fabricadas; llamadas abstracciones.

La idea detrás de las abstracciones de alto nivel es que el usuario solo debe elegir la abstracción apropiada yescribir el código que hace el trabajo, mientras OmniThreadLibrary provee el framework que implementa laspartes complicadas del multiproceso, se encarga de la sincronización, etc.

9

Page 25: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 10

2.1 Async

Async (de asíncrono) es la más simple entre las abstracciones de alto nivel y suele utilizarse en escenarioslanza y olvida. Para crear una tarea Async se llama a Parallel.Async.

..

Cuando se llama a Parallel.Async, el código se inicia en un nuevo hilo (indicado por la línea vertical gruesa)y tanto el hilo principal como el hilo en segundo plano continúan su ejecución. En algún momento, la tarea defondo completa su ejecución y desaparece.

Ver también demo 46_Async.

Page 26: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 11

Ejemplo:

1 Parallel.Async(

2 procedure

3 begin

4 MessageBeep($FFFFFFFF);

5 end);

Este simple programa crea una tarea en segundo plano con el único propósito de hacer algún ruido desdeella. La tarea ha sido codificada como un método anónimo, pero también podría utilizarse un método oprocedimiento normales para el código de la tarea.

La clase Parallel define dos sobrecargas de Async. La primera acepta una tarea en segundo plano sinparámetros y un bloque de configuración de tarea opcional y la segunda acepta una tarea en segundo planocon un parámetro de tipo IOmniTask y un bloque de configuración de tarea opcional.

1 type

2 TOmniTaskDelegate = reference to procedure(const task: IOmniTask);

3

4 Parallel = class

5 class procedure Async(task: TProc;

6 taskConfig: IOmniTaskConfig = nil); overload;

7 class procedure Async(task: TOmniTaskDelegate;

8 taskConfig: IOmniTaskConfig = nil); overload;

9 ...

10 end;

La segunda forma es útil si el código que se ejecutará en segundo plano requiere acceso a la interfaz IOmniTask,por ejemplo para enviar mensajes al propietario o para ejecutar código en el hilo del propietario (generalmenteesto será en el hilo principal).

El ejemplo siguiente utiliza una tarea asíncrona para obtener el contenido de una página web (llamando a lafunciónmisteriosa HttpGet) y entonces utiliza Invoke para ejecutar código que registra el tamaño del resultadoen el hilo principal.

1 Parallel.Async(

2 procedure (const task: IOmniTask)

3 var

4 page: string;

5 begin

6 HttpGet('otl.17slon.com', 80, 'tutorials.htm', page, '');

7 task.Invoke(

8 procedure

9 begin

10 lbLogAsync.Items.Add(Format('Async GET: %d ms; page length = %d',

Page 27: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 12

11 [time, Length(page)]))

12 end);

13 end);

El mismo resultado puede alcanzarse enviando un mensaje del hilo en segundo plano al hilo principal. Elbloque TaskConfig es utilizado para configurar el manejador de mensaje.

1 const

2 WM_RESULT = WM_USER;

3

4 procedure LogResult(const task: IOmniTaskControl; const msg: TOmniMessage);

5 begin

6 lbLogAsync.Items.Add(Format('Async GET: %d ms; page length = %d',

7 [time, Length(page)]))

8 end;

9

10 Parallel.Async(

11 procedure (const task: IOmniTask)

12 var

13 page: string;

14 begin

15 HttpGet('otl.17slon.com', 80, 'tutorials.htm', page, '');

16 task.Comm.Send(WM_RESULT, page);

17 end,

18 TaskConfig.OnMessage(WM_RESULT, LogResult)

19 );

Debo advertir que en los casos en que se desea devolver un resultado de una tarea en segundo plano, laabstracción Async no es la más apropiada. Sería mejor utilizar un Futuro.

2.1.1 Manejo de Excepciones

Si el código de fondo lanza una excepción que no es manejada, OmniThreadLibrary atrapará esta excepcióny la re-lanzará en el manejador OnTerminated. De esta manera la excepción viajará del hilo en segundo planoal hilo propietario, donde puede ser procesada.

Dado que el manejador OnTerminated ocurre en un momento no especificado mientras Windows procesamensajes, no hay una buena forma de atrapar este mensaje en un bloque try..except. En su lugar, el códigoque ha realizado la llamada debe instalar su propio manejador OnTerminated y manejar la excepción allí.

El ejemplo siguiente usa el manejador OnTerminated para separar una excepción fatal de la tarea, registrarlos detalles de la misma y destruir el objeto de excepción.

Page 28: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 13

1 Parallel.Async(

2 procedure

3 begin

4 Sleep(1000);

5 raise Exception.Create('Exception in Async');

6 end,

7 Parallel.TaskConfig.OnTerminated(

8 procedure (const task: IOmniTaskControl)

9 var

10 excp: Exception;

11 begin

12 if assigned(task.FatalException) then begin

13 excp := task.DetachException;

14 Log('Caught async exception %s:%s',[excp.ClassName, excp.Message]);

15 FreeAndNil(excp);

16 end;

17 end

18 ));

Si no se instala un manejador OnTerminated, la excepción será manejada por el filtro a nivel de la aplicación,lo que, por defecto, causará que aparezca un mensaje de error.

Véase también demo 48_OtlParallelExceptions.

Page 29: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 14

2.2 Async/Await

Async/Await es una versión simplificada de la abstracción Async que imita al mecanismo Async/Await de.NET¹.²

En pocas palabras, Async/Await recibe dos métodos anónimos sin parámetros. El primero se ejecuta en un hiloen segundo plano, y el segundo es ejecutado en el hilo principal luego de que el hilo de fondo ha completadosu trabajo.

Véase también la demo 53_AsyncAwait.

Al utilizar Async/Await se puede, por ejemplo, crear una operación de fondo que es lanzada por un clic y quere-habilita el botón luego de que la tarea en segundo plano ha sido completada.

1 procedure TForm1.Button1Click(Sender: TObject);

2 var

3 button: TButton;

4 begin

5 button := Sender as TButton;

6 button.Caption := 'Trabajando ...';

7 button.Enabled := false;

8 Async(

9 // se ejecuta en un hilo en segundo plano

10 procedure begin

11 Sleep(5000);

12 end).

13 Await(

14 // se ejecuta en el hilo principal después

15 // de que el método anónimo pasado a

16 // Async complete su 'trabajo'

17 procedure begin

18 button.Enabled := true;

19 button.Caption := 'Terminado!';

20 end);

21 end;

Hay un único pequeño cambio entre Async y Async/Await. El primero se invoca llamando aParallel.Async y el último llamando a Async sin el prefijo.

Actualmente, las excepciones en la parte Async no son manejadas por OmniThreadLibrary.

¹http://blogs.msdn.com/b/pfxteam/archive/2012/04/12/10293335.aspx²Más información en http://www.thedelphigeek.com/2012/07/asyncawait-in-delphi.html.

Page 30: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 15

2.3 For Each

La abstracción For Each crea un bucle for paralelo que itera sobre un rango de datos (rango numérico,lista, cola, conjunto de datos…) en múltiples hilos. Para crear un bucle for paralelo debe llamarse aParallel.ForEach.

..

Cuando se utiliza Parallel.ForEach, OmniThreadLibrary inicia múltiples tareas en segundo plano y las conectaa la fuente a través de un mecanismo de serialización. Opcionalmente, la salida puede ser ordenada en el ordende la entrada. Por defecto, ForEach espera a que todos los hilos en segundo plano se completen antes de retornarel control a la rutina que lo ha llamado.

NDelT Se ha usado la palabra serialización al considerar que es la más aceptada para el término original — serialization — auncuándo la RAE no la incluye en el diccionario. Véase es.wikipedia.org/wiki/Serialización.

Véase también los demos 35_ParallelFor y 36_OrderedFor.

Page 31: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 16

Ejemplo:

1 PrimeCount.Value := 0;

2 Parallel.ForEach(1, 1000000).Execute(

3 procedure (const value: integer)

4 begin

5 if IsPrime(value) then

6 PrimeCount.Increment;

7 end;

8 end);

Este sencillo programa calcula el número de números primos en el rango de uno a un millón. El objetoPrimeCount debe ser capaz de realizar un incremento atómico (un incremento thread-safe), que se lografácilmente usando un registro TGp4AlignedInt. La tarea ForEach se ha codificado como un método anónimo,pero también podría utilizarse un método o procedimiento normales para el código de la tarea.

2.3.1 Cooperación

El punto principal de la abstracción ForEach es la cooperación entre tareas paralelas. ForEach hace todo loposible para minimizar posibles conflictos entre hilos cuando acceden a los datos. Con excepción de ocasionesespeciales (rango de números, IOmniBlockingCollection), los datos no son thread-safe y debe utilizarsebloqueos para su acceso.

Para minimizar el bloqueo, la asignación de los datos a las tareas que hacen el trabajo ocurre en bloques.ForEach crea un objeto proveedor (source provider) que será quién accederá a los datos de manera segura(thread-safe). Cuándo una tareas se queda sin datos para procesar, este proveedor se asegura de retornar unbloque de tamaño apropiado de datos (el tamaño depende del número de tareas, el tipo de datos de origen yotros factores).

Dado que la fuente de datos es asignada en bloques, existe la posibilidad de que una de las tareas se quedesin trabajo mientras las otras aún están ocupadas. En este caso, una tarea robará datos de alguna de las otrastareas. Este enfoque hace que todas las tareas estén tan ocupadas como sea posible a la vez que minimiza lacontención.

Los detalles de este proceso se discuten más adelante en la sección Interioridades.

2.3.2 Iterando sobre …

La clase Parallel define varias sobrecargas de ForEach, cada una soportando un distinto tipo de contenedor.Veremos estos en detalle en las secciones siguientes.

2.3.2.1 … Rangos de Números

Para iterar sobre un rango, se pasa el índice del primero (first) y el último (last) en la llamada a ForEach.Opcionalmente puede pasarse un parámetro salto (step), cuyo valor por defecto es 1. Entonces, ForEachiterará desde el primero hasta el último en incrementos de salto.

Page 32: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 17

1 class function ForEach(low, high: integer; step: integer = 1):

2 IOmniParallelLoop<integer>; overload;

El pseudocódigo para un ForEach numérico podría ser escrito así:

1 i := low;

2 while ((step > 0) and (i <= high)) or

3 ((step < 0) and (i >= high)) do

4 begin

5 // process 'i' in parallel

6 if low < high then Inc(i, step)

7 else Dec(i, step);

8 end;

2.3.2.2 … Colecciones Enumerables

Si se desea iterar sobre una colección (por decir, un TStringList), se tiene dos opciones.

Una es utilizar un equivalente de for i := 0 to sl.Count-1 do Something(sl[i]).

1 Parallel.ForEach(0, sl.Count-1).Execute(

2 procedure (const value: integer)

3 begin

4 Something(sl[value]);

5 end);

La otra es utilizar un equivalente de for s in sl do Something(s).

1 Parallel.ForEach(sl).Execute(

2 procedure (const value: TOmniValue)

3 begin

4 Something(value);

5 end);

En el segundo ejemplo, el valor es pasado a la función de la tarea como un parámetro TOmniValue. En el ejemploanterior, este será convertido automáticamente a un string, pero hay ocasiones en que esta conversión debehacerse manualmente, llamando a value.AsString (o la función de moldeado de tipo apropiada cuándo seitera sobre un contenedor distinto).

Una variación del segundo enfoque es indicar a ForEach que se trata de un contenedor de strings yOmniThreadLibrary se encargará de la conversión.

Page 33: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 18

1 Parallel.ForEach<string>(sl).Execute(

2 procedure (const value: string)

3 begin

4 Something(value);

5 end);

Podríamos preguntarnos cuál de estos enfoques es mejor. La respuesta depende de si se puede acceder demanera simultánea a diferentes elementos del contenedor desde diferentes hilos al mismo tiempo. En otraspalabras, debe saberse si el contenedor soporta la lectura multi-hilo. Por fortuna, todos los contenedoresimportantes en Delphi (TList, TObjectList, TStringList) pertenecen a esta categoría.

Si el contenedor soporta la lectura multi-hilo, entonces el enfoque numérico (ForEach(0, sl.Count-1)) esmucho más rápido que el enfoque for..in (ForEach(sl)). La diferencia de velocidad viene de los bloqueos.En el primer ejemplo, ForEach nunca bloquea nada, y en el segundo, se utiliza bloqueos para sincronizar elacceso al contenedor.

Sin embargo, si el contenedor no soporta la lectura multi-hilo, debe utilizarse el último enfoque.

Hay tres maneras de iterar sobre contenedores enumerables. Se puede pasar una interfaz IEnumerable en lallamada a ForEach, con una interfaz IEnumerator o con directamente con una colección enumerable. En elúltimo caso, OmniThreadLibrary utiliza RTTI para tener acceso al enumerador de la colección. Para que estotrabaje, el enumerador mismo debe estar implementado como un objeto, no como un registro o interfaz. Porsuerte, muchos, si no todos, los enumeradores de la VCL han sido implementados de esta manera.

1 class function ForEach(const enumerable: IEnumerable):

2 IOmniParallelLoop; overload;

3 class function ForEach(const enum: IEnumerator):

4 IOmniParallelLoop; overload;

5 class function ForEach(const enumerable: TObject):

6 IOmniParallelLoop; overload;

7 class function ForEach<T>(const enumerable: IEnumerable):

8 IOmniParallelLoop<T>; overload;

9 class function ForEach<T>(const enum: IEnumerator):

10 IOmniParallelLoop<T>; overload;

11 class function ForEach<T>(const enumerable: TEnumerable<T>):

12 IOmniParallelLoop<T>; overload;

13 class function ForEach<T>(const enum: TEnumerator<T>):

14 IOmniParallelLoop<T>; overload;

15 class function ForEach<T>(const enumerable: TObject):

16 IOmniParallelLoop<T>; overload;

2.3.2.3 … Colecciones enumerables multi-hilo

La enumeración de colecciones usa bloqueos para sincronizar el acceso al enumerador de la colección, loque ralentiza el proceso de enumeración. En algunos casos especiales, la colección puede enumerarse sinrecurrir a ellos. Para enumerar una colección tal, ésta debe implementar las interfaces IOmniValueEnumerabley IOmniValueEnumerator, que se encuentran definidas en la unidad OtlCommon.

Page 34: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 19

1 class function ForEach(const enumerable: IOmniValueEnumerable):

2 IOmniParallelLoop; overload;

3 class function ForEach(const enum: IOmniValueEnumerator):

4 IOmniParallelLoop; overload;

5 class function ForEach<T>(const enumerable: IOmniValueEnumerable):

6 IOmniParallelLoop<T>; overload;

7 class function ForEach<T>(const enum: IOmniValueEnumerator):

8 IOmniParallelLoop<T>; overload;

2.3.2.4 … Colecciones con Bloqueo

Para simplificar la enumeración sobre colecciones con bloqueo, la clase Parallel implementa dos sobrecargasde ForEach que aceptan un parámetro de este tipo. Internamente, la colección con bloqueo es enumerada conla interfaz IOmniValueEnumerable.

1 class function ForEach(const source: IOmniBlockingCollection):

2 IOmniParallelLoop; overload;

3 class function ForEach<T>(const source: IOmniBlockingCollection):

4 IOmniParallelLoop<T>; overload;

2.3.2.5 … Cualquier cosa

Como último recurso, la clase Parallel implementa tres sobrecargas de ForEach que podrán (con algunaayuda del programador) iterar sobre los datos que sea.

La manera de TOmniSourceProvider es poderosa, pero complicada.

1 class function ForEach(const sourceProvider: TOmniSourceProvider):

2 IOmniParallelLoop; overload;

Debe implementarse un descendiente de la clase TOmniSourceProvider. Todos los métodos deben ser thread-safe. Para más información sobre los proveedores de datos, véase la sección Interioridades más adelante.

1 TOmniSourceProvider = class abstract

2 public

3 function Count: int64; virtual; abstract;

4 function CreateDataPackage: TOmniDataPackage; virtual; abstract;

5 function GetCapabilities: TOmniSourceProviderCapabilities;

6 virtual; abstract;

7 function GetPackage(dataCount: integer; package: TOmniDataPackage):

8 boolean; virtual; abstract;

9 function GetPackageSizeLimit: integer; virtual; abstract;

10 end;

Dado que este enfoque no es para los débiles de corazón, OmniThreadLibrary ofrece una versión más lenta,pero mucho más simple.

Page 35: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 20

1 class function ForEach(enumerator: TEnumeratorDelegate):

2 IOmniParallelLoop; overload;

3 class function ForEach<T>(enumerator: TEnumeratorDelegate<T>):

4 IOmniParallelLoop<T>; overload;

En este caso, debe proveerse una función que retornará el dato siguiente cada vez que ForEach lo requiera.

1 TEnumeratorDelegate = reference to function(var next: TOmniValue): boolean;

2 TEnumeratorDelegate<T> = reference to function(var next: T): boolean;

OmniThreadLibrary proporcionará la sincronización (bloqueos) de tal manera que se puede estar seguro queeste método se solamente llamará de un hilo a la vez. Como podría esperarse, esto ralentizará las cosas, perola paralelización aún puede dar un incremento razonable en el desempeño si la carga de trabajo de ForEaches sustancial (es decir, si el método que se ejecuta en el bucle ForEach toma algún tiempo para ejecutarse).

La función TEnumeratorDelegate también puede usarse como un generador; es decir que puede calcular losvalores que serán procesados en el bucle for paralelo.

2.3.3 Proporcionar una Entrada Externa

En ocasiones, especialmente cuándo se está tratando con conjuntos de datos, el acceso sincronizado alcontenedor no será suficiente. Cuándo se trabaja con conexiones a base de datos, conjuntos de datos, etc.se puede caer fácilmente en problemas de afinidad de hilos – la incapacidad de algunos componentes paratrabajar adecuadamente si se les llama de un hilo diferente de aquel en el que fueron creados.

[Inicializa siempre las conexiones a bases de datos y conjuntos de datos en el hilo que lasutilizará. El código podría trabajar sin tomar esa precaución, pero a menos que se haya probadoextensivamente los componentes de base de datos en múltiples hilos, no debe asumirse que estosfuncionarán correctamente a menos que se cumpla esa condición (inicialización y utilización enel mismo hilo).]

En ese caso, lo mejor es proporcionar la entrada directamente desde el hilo principal. Hay varias maneras delograr esto.

1. Re-empacar los datos en otra colección que pueda ser consumida fácilmente en ForEach (TObjectList,TStringList, TOmniBlockingCollection).

2. Ejecutar el ForEach en modo NoWait, y luego escribir los datos en la cola de entrada y cuándo estos seterminen, esperar a que termine el bucle ForEach. Este enfoque es también útil cuando se quiere enviara ForEach a un segundo plano y facilitarle los datos desde algún manejador de eventos asíncrono.

Un ejemplo del segundo enfoque ayudará a aclarar la idea.

Page 36: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 21

1 uses

2 OtlCommon,

3 OtlCollections,

4 OtlParallel;

5

6 procedure Test;

7 var

8 i : integer;

9 input: IOmniBlockingCollection;

10 loop : IOmniParallelLoop<integer>;

11 wait : IOmniWaitableValue;

12 begin

13 // creación del contenedor

14 input := TOmniBlockingCollection.Create;

15 // creación de la señal de 'fin del trabajo'

16 wait := CreateWaitableValue;

17 loop := Parallel.ForEach<integer>(input);

18 // método de terminación que lanzará la señal de 'fin del trabajo'

19 loop.OnStop(

20 procedure

21 begin

22 wait.Signal;

23 end);

24 // Inicio del bucle en modo NoWait

25 loop.NoWait.Execute(

26 procedure (const value: integer)

27 begin

28 // Hacer algo con el valor de entrada

29 OutputDebugString(PChar(Format('%d', [value])));

30 end

31 );

32 // Proveer los datos al bucle for paralelo

33 for i := 1 to 1000 do

34 input.Add(i);

35 // señalar al bucle que no hay más datos para procesar

36 input.CompleteAdding;

37 // Esperar a que el bucle for paralelo pare

38 wait.WaitFor;

39 // Destrucción del bucle for paralelo

40 loop := nil;

41 end;

Page 37: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 22

2.3.4 IOmniParallelLoop

Parallel.ForEach devuelve una interface IOmniParallelLoop que es utilizada para configurar y ejecutar elbucle for paralelo.

1 IOmniParallelLoop = interface

2 function Aggregate(defaultAggregateValue: TOmniValue;

3 aggregator: TOmniAggregatorDelegate): IOmniParallelAggregatorLoop;

4 function AggregateSum: IOmniParallelAggregatorLoop;

5 procedure Execute(loopBody: TOmniIteratorDelegate); overload;

6 procedure Execute(loopBody: TOmniIteratorTaskDelegate); overload;

7 function CancelWith(const token: IOmniCancellationToken):

8 IOmniParallelLoop;

9 function Initialize(taskInitializer: TOmniTaskInitializerDelegate):

10 IOmniParallelInitializedLoop;

11 function Into(const queue: IOmniBlockingCollection):

12 IOmniParallelIntoLoop; overload;

13 function NoWait: IOmniParallelLoop;

14 function NumTasks(taskCount : integer): IOmniParallelLoop;

15 function OnMessage(eventDispatcher: TObject):

16 IOmniParallelLoop; overload; deprecated 'use TaskConfig';

17 function OnMessage(msgID: word; eventHandler: TOmniTaskMessageEvent):

18 IOmniParallelLoop; overload; deprecated 'use TaskConfig';

19 function OnMessage(msgID: word; eventHandler: TOmniOnMessageFunction):

20 IOmniParallelLoop; overload; deprecated 'use TaskConfig';

21 function OnTaskCreate(taskCreateDelegate: TOmniTaskCreateDelegate):

22 IOmniParallelLoop; overload;

23 function OnTaskCreate(taskCreateDelegate:

24 TOmniTaskControlCreateDelegate): IOmniParallelLoop; overload;

25 function OnStop(stopCode: TProc): IOmniParallelLoop;

26 function PreserveOrder: IOmniParallelLoop;

27 function TaskConfig(const config: IOmniTaskConfig): IOmniParallelLoop;

28 end;

ForEach<T> devuelve una interfaz IOmniParallelLoop<T>, que es exactamente lomismo que IOmniParallelLoopcon la salvedad de que cada método devuelve la versión <T> apropiada de la interfaz.

Se utiliza a Aggregate y AggregateSum para implementar agregación. Véase más adelante la secciónAgregación

Execute acepta el bloque de código que será ejecutado para cada valor en el contenedor de entrada. Se soportados firmas de método, ambas con su variante <T>. Una recibe un solo valor de iteración y la otra recibe unparámetro IOmniTask.

Page 38: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 23

1 TOmniIteratorDelegate = reference to procedure(const value: TOmniValue);

2 TOmniIteratorDelegate<T> = reference to procedure(const value: T);

3 TOmniIteratorTaskDelegate =

4 reference to procedure(const task: IOmniTask; const value: TOmniValue);

5 TOmniIteratorTaskDelegate<T> =

6 reference to procedure(const task: IOmniTask; const value: T);

CancelWith activa el mecanismo de cancelación.

Con Initialize y OnTaskCreate se puede inicializar datos-por-tarea antes del inicio de la ejecución de latarea. Véase más adelante la sección Inicialización de Tarea

Into configura la cola de salida, véase Perservar el Orden de Salida.

Si se llama a la función NoWait, el for paralelo iniciará en segundo plano y el control retornará inmediatamenteal hilo principal. Si no se llama a NoWait, Execute retornará hasta que todas las tareas hayan parado de trabajar.

Al llamar a NumTasks se puede configurar el número de tareas de trabajo. Por defecto, al número de tareas se leasigna el [número de núcleos disponibles al proceso] - 1 si se utiliza los modificadores NoWait o PreserveOrdery se le asigna el [número de núcleos disponibles al proceso] en los otros casos.

Las funciones OnMessage son obsoletas, en su lugar se utiliza TaskConfig.

OnStop configura un manejador de terminación que será llamado luego de que todas las tareas paralelas hancompletado su trabajo. Si se llama a la función NoWait, OnStop será llamado desde uno de los hilos trabajadores.Si, por el contrario, no se ha llamado a NoWait, OnStop será llamado desde el hilo que creo la abstracciónForEach. Este comportamiento dificulta la ejecución de código VCL dentro de OnStop, por ello la versión 3.02introduce otra variación que acepta un delegado con un parámetro IOmniTask.

1 TOmniTaskStopDelegate = reference to procedure (const task: IOmniTask);

2 IOmniParallelLoop = interface

3 function OnStop(stopCode: TOmniTaskStopDelegate): IOmniParallelLoop;

4 overload;

5 end;

Al utilizar esta versión de OnStop, el manejador de terminación puede utilizar task.Invoke para ejecutarcódigo en el hilo principal. Esto, sin embargo, requiere que la abstracción ForEach se mantenga con vida hastaque se ha ejecutado el código invocado, por tanto debe almacenarse el resultado de ForEach en una variableglobal (por ejemplo, un miembro del formulario) y destruirla solamente en el manejador de terminación.

Page 39: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 24

1 var

2 loop: IOmniParallelLoop<integer>;

3

4 loop := Parallel.ForEach(1, N).NoWait;

5 loop.OnStop(

6 procedure (const task: IOmniTask)

7 begin

8 task.Invoke(

9 procedure

10 begin

11 // do anything

12 loop := nil;

13 end); end);

14 loop.Execute(

15 procedure (const value: integer)

16 begin

17 ...

18 end);

PreserveOrdermodifica el comportamiento de tal manera que los valores de salida son generados en el ordende los valores de entrada correspondientes. Véase más adelante la sección Preservando el Orden de Salida.

TaskConfig configura un bloque de configuración de tarea. El mismo bloque de configuración de tarea seráaplicado a todas las tareas trabajadoras de ForEach.

El ejemplo siguiente utiliza TaskConfig para configurar un manejador de mensajes que recibirá los mensajesenviados desde las tareas trabajadoras de ForEach.

1 FParallel := Parallel.ForEach(1, 17)

2 .TaskConfig(Parallel.TaskConfig.OnMessage(Self))

3 .NoWait

4 .OnStop(procedure begin FParallel := nil; end);

5 FParallel

6 .Execute(

7 procedure (const task: IOmniTask; const value: integer)

8 begin

9 task.Comm.Send(WM_LOG, value);

10 end);

Losmensajes enviados desde las tareas trabajadoras son recibidos y despachados por la interfaz IOmniParallelLoop.Esto requiere que la abstracción ForEach se mantenga con vida hasta que los mensajes hayan sido procesados,por lo que debe almacenarse el resultado de ForEach en una variable global (un miembro de un formulario,por ejemplo) y destruirla solamente en el manejador OnStop

Algunas funciones devuelven una interfaz distinta. Por lo general, esta solo implementa la función Execute

aceptando un parámetro distinto al Execute ‘normal’. Por ejemplo, Aggregate devuelve la interfaz IOmniParallelAggregatorLoop.

Page 40: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 25

1 TOmniIteratorIntoDelegate =

2 reference to procedure(const value: TOmniValue; var result: TOmniValue);

3

4 IOmniParallelAggregatorLoop = interface

5 function Execute(loopBody: TOmniIteratorIntoDelegate): TOmniValue;

6 end;

Estas variantes de la interfaz IOmniParallelLoop serán descritas en las secciones siguientes.

2.3.5 Preservar el orden de salida

Cuándo se corre un bucle ForEach, no se puede determinar por adelantado el orden en que los elementos dela colección de entrada serán procesados. Por ejemplo, el código siguiente generará todos los primos desde 1hasta CMaxPrime y los escribirán en la cola de salida (primeQueue) en un orden no determinista.

1 primeQueue := TOmniBlockingCollection.Create;

2 Parallel.ForEach(1, CMaxPrime).Execute(

3 procedure (const value: integer)

4 begin

5 if IsPrime(value) then begin

6 primeQueue.Add(value);

7 end;

8 end);

En ocasiones, esto representa un gran problema y debe escribirse una función de ordenamiento que reordenela salida antes de que pueda seguir procesándose. Para aliviar este problema, IOmniParallelLoop implementael modificador PreserveOrder. Cuándo se utiliza, el bucle ForEach ordenará internamente los resultadosproducidos en el método de trabajo pasado al método Execute.

La utilización de PreserveOrder obliga también la utilización del método Into que devuelve la interfazIOmniParallelIntoLoop. (Como es de esperarse, también hay una versión <T> de dicha interfaz.)

1 TOmniIteratorIntoDelegate =

2 reference to procedure(const value: TOmniValue; var result: TOmniValue);

3 TOmniIteratorIntoTaskDelegate =

4 reference to procedure(const task: IOmniTask; const value: TOmniValue;

5 var result: TOmniValue);

6

7 IOmniParallelIntoLoop = interface

8 procedure Execute(loopBody: TOmniIteratorIntoDelegate); overload;

9 procedure Execute(loopBody: TOmniIteratorIntoTaskDelegate); overload;

10 end;

Como puede verse, el método Execute en IOmniParallelIntoLoop toma un parámetro distinto que el Execute‘normal’. Debido a esto, debe cambiarse el código que sería pasado a Execute para que devuelva un resultado.

Page 41: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 26

1 primeQueue := TOmniBlockingCollection.Create;

2 Parallel.ForEach(1, CMaxPrime)

3 .PreserveOrder

4 .Into(primeQueue)

5 .Execute(

6 procedure (const value: integer; var res: TOmniValue)

7 begin

8 if IsPrime(value) then

9 res := value;

10 end);

Cuándo se utiliza PreserveOrder e Into, ForEach llama al código de trabajo para cada valor de entrada.Si el código de trabajo asigna cualquier valor al parámetro de salida (res), este será insertado en un buffertemporal. Luego ocurrirá la magia (Véase luego la sección Interioridades) y tan pronto como el valor apropiado(ordenado) se encuentre disponible en el buffer temporal, este es insertado en la cola de salida (la que se hapasado en el parámetro Into).

También puede utilizarse Into sin PreserveOrder. Esto nos dará la gestión de la cola, pero no su ordenamiento.

2.3.6 Agregación

La agregación permite colectar datos desde las tareas paralelas del for y calcular un número que será devueltoal usuario.

Empecemos con un ejemplo – ¡uno muy malo! El siguiente fragmento de código trata de calcular el númerode números primos entre 1 y CMaxPrime.

1 numPrimes := 0;

2 Parallel.ForEach(1, CMaxPrime).Execute(

3 procedure (const value: integer)

4 begin

5 if IsPrime(value) then

6 Inc(numPrimes);

7 end);

Vamos a decirlo en voz alta – ¡Este código es incorrecto! El acceso a la variable compartida no ha sidosincronizado entre los hilos y eso hará que el resultado sea indeterminable. Una forma de resolver esteproblema es encerrar la llamada Inc(numPrimes) con un bloqueo y otro es usar InterlockedIncrement enlugar de Inc, pero ambos ralentizarán mucho la ejecución.

Una solución a este problema es utilizar la función Aggregate.

Page 42: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 27

1 procedure SumPrimes(var aggregate: TOmniValue; const value: TOmniValue)

2 begin

3 aggregate := aggregate.AsInt64 + value.AsInt64;

4 end;

5

6 procedure CheckPrime(const value: integer; var result: TOmniValue)

7 begin

8 if IsPrime(value) then

9 Result := 1;

10 end;

11

12 numPrimes :=

13 Parallel.ForEach(1, CMaxPrime)

14 .Aggregate(0, SumPrimes)

15 .Execute(CheckPrime);

Aggregate recibe dos parámetros – el primero es el valor inicial de la agregación y el segundo es una funciónde agregación – una pieza de código que toma el valor actual de la agregación y lo actualiza con el valorretornado desde la tarea del for paralelo.

Cuándo se utiliza Aggregate, la tarea del for paralelo (el código pasado a la función Execute) tiene la mismafirma que cuándo se ha utilizado Into. Este recibe el valor actual de la iteración y opcionalmente produce unresultado.

Podríamos remplazar el código anterior con un bucle for.

1 agg := 0;

2 result.Clear;

3 for value := 1 to CMaxPrime do begin

4 CheckPrime(value, result);

5 if not result.IsEmpty then begin

6 SumPrimes(agg, result);

7 result.Clear;

8 end;

9 end;

10 numPrimes := agg;

ForEach ejecuta la agregación en dos etapas. Mientras la tarea del for paralelo se ejecuta, se utilizará esteenfoque para agregar datos a una variable local. Cuándo se queda sin trabajo, esta llamará al método deagregación para agregar esta variable local al resultado global. En esta segunda etapa, sin embargo, se utilizabloqueos para proteger el acceso al resultado global.

Page 43: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 28

Dado que la suma es el caso más común de agregación, IOmniParallelLoop implementa la funciónAggregateSum, que trabaja exactamente de la misma manera como lo hace SumPrimes en el ejemplo anterior.

1 numPrimes :=

2 Parallel.ForEach(1, CMaxPrime)

3 .AggregateSum

4 .Execute(

5 procedure (const value: integer; var result: TOmniValue)

6 begin

7 if IsPrime(value) then

8 Result := 1;

9 end

10 );

La función de agregación puede hacer algo distinto de la suma. El siguiente segmento de código utilizaagregación para encontrar el tamaño de la línea más larga de un archivo.

Page 44: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 29

1 function GetLongestLineInFile(const fileName: string): integer;

2 var

3 maxLength: TOmniValue;

4 sl : TStringList;

5 begin

6 sl := TStringList.Create;

7 try

8 sl.LoadFromFile(fileName);

9 maxLength := Parallel.ForEach<string>(sl)

10 .Aggregate(0,

11 procedure(var aggregate: TOmniValue; const value: TOmniValue)

12 begin

13 if value.AsInteger > aggregate.AsInteger then

14 aggregate := value.AsInteger;

15 end)

16 .Execute(

17 procedure(const value: string; var result: TOmniValue)

18 begin

19 result := Length(value);

20 end);

21 Result := maxLength;

22 finally FreeAndNil(sl); end;

23 end;

2.3.7 Cancelación

ForEach incorpora un mecanismo de cancelación. Para hacer uso de este, se crea un token de cancelación quees pasado a la función CancelWith. Cuándo el token de cancelación es señalado, todos los bucles de trabajocompletarán la iteración actual y luego se detendrán.

Un ejemplo del uso del token de cancelación puede encontrarse en el capítulo Búsqueda Paralela en un Árbol.

2.3.8 Inicialización y Finalización de Tarea

En algunos casos, sería bueno si cada tarea paralela pudiera inicializar algunos datos al iniciar que estuviesendisponibles al código de enumeración (el que se ha pasado a Execute). Para esas ocasiones, ForEachimplementa una función Initialize.

Page 45: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 30

1 TOmniTaskInitializerDelegate =

2 reference to procedure(var taskState: TOmniValue);

3 TOmniTaskFinalizerDelegate =

4 reference to procedure(const taskState: TOmniValue);

5 TOmniIteratorStateDelegate =

6 reference to procedure(const value: TOmniValue; var taskState: TOmniValue\

7 );

8

9 IOmniParallelInitializedLoop = interface

10 function Finalize(taskFinalizer: TOmniTaskFinalizerDelegate):

11 IOmniParallelInitializedLoop;

12 procedure Execute(loopBody: TOmniIteratorStateDelegate);

13 end;

14

15 IOmniParallelLoop = interface

16 ...

17 function Initialize(taskInitializer: TOmniTaskInitializerDelegate):

18 IOmniParallelInitializedLoop;

19 end;

Se le da a Initialize un inicializador de tarea, un procedimiento que será llamado al crear cada tareaparalela y antes de que esta inicie la enumeración de valores. Este procedimiento puede inicializar el parámetrotaskState con cualquier valor.

Initialize devuelve una interfaz IOmniParallelInitializedLoop que implementa dos funciones – Finalizey Execute. Se llama a Finalize para configurar un finalizador de tarea, procedimiento que es llamado luegode que se ha enumerado a todos los valores y antes de que la tarea paralela termine su trabajo.

Execute recibe unmétodo de trabajo con dos parámetros – el primero es el acostumbrado valor del contenedorque se enumera y el segundo contiene el estado de tarea compartido.

Por supuesto, todas estas funciones e interfaces están implementadas también en la versión <T>.

El ejemplo siguiente muestra cómo calcular el número de primos desde 1 hasta CHighPrime utilizandoinicializadores y finalizadores.

1 var

2 lockNum : TOmniCS;

3 numPrimes: integer;

4 begin

5 numPrimes := 0;

6 Parallel.ForEach(1, CHighPrime)

7 .Initialize(

8 procedure (var taskState: TOmniValue)

9 begin

10 taskState.AsInteger := 0;

11 end)

Page 46: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 31

12 .Finalize(

13 procedure (const taskState: TOmniValue)

14 begin

15 lockNum.Acquire;

16 try

17 numPrimes := numPrimes + taskState.AsInteger;

18 finally lockNum.Release; end;

19 end)

20 .Execute(

21 procedure (const value: integer; var taskState: TOmniValue)

22 begin

23 if IsPrime(value) then

24 taskState.AsInteger := taskState.AsInteger + 1;

25 end

26 );

27 end;

2.3.9 Manejo de excepciones

La abstracción ForEach aún no implementa ningún manejo de excepciones. El método de trabajo (el códigoque se pasa a Execute) debe encerrarse siempre en un bloque try..except si se espera que dicho código lanceexcepciones.

2.3.10 Interioridades

Esta sección trata de explicar cómo ha sido implementado el ForEach. Debe considerarse como un materialextra para aquellos usuarios que desean saber más. No es necesario leer o entender esto para utilizar ForEachpor lo que eres libre de saltarte esta parte del libro.

Iniciaremos con un código muy sencillo.

1 Parallel.ForEach(1, 1000)

2 .Execute(

3 procedure (const elem: integer)

4 begin

5 end);

Este código simplemente itera, en paralelo, de 1 a 1000 en todos los núcleos disponibles y ejecuta un sencilloprocedimiento que no contiene trabajo alguno. En definitiva, el código no hace nada – ¡pero lo hace de unamanera bastante compleja!.

El método ForEach crea un nuevo objeto TOmniParallelLoop<integer> (ese será el objeto que coordinará lastareas en paralelo) y se lo pasa al source provider – un objeto que sabe cómo acceder a los valores que seránenumerados 8enteros desde 1 hasta 1000 en este ejemplo).

Page 47: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 32

La unidad OtlDataManager contiene cuatro diferentes proveedores de datos de origen – uno para cada tipode origen de datos que se le puede pasar al método ForEach. Si fuese necesario extender a ForEach con unanueva fuente de enumeración, yo solamente tendría que añadir unos cuantos métodos sencillos a la unidadOtlParallel y escribir un nuevo proveedor de datos de origen.

1 class function Parallel.ForEach(low, high: integer; step: integer):

2 IOmniParallelLoop<integer>;

3 begin

4 Result := TOmniParallelLoop<integer>.Create(

5 CreateSourceProvider(low, high, step), true);

6 end;

Page 48: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 33

Las tareas paralelas del for son iniciadas en InternalExecuteTask. Este método crea primero un gestor dedatos y lo asocia al proveedor de datos de origen (compara esto con la imagen de arriba – hay un proveedorde datos de origen y un gestor de datos). Luego, crea el número apropiado de tareas y llama al método delegadoespecífico para la tarea desde cada una. [Este delegado encierra tu código paralelo y le facilita la entrada (ya veces la salida) correcta. Hay muchas llamadas a InternalExecuteTask en la unidad OtlParallel, cada unacon un taskDelegate distinto y cada una ofreciendo soporte a un tipo diferente de bucle.]

1 procedure TOmniParallelLoopBase.InternalExecuteTask(

2 taskDelegate: TOmniTaskDelegate);

3 var

4 dmOptions : TOmniDataManagerOptions;

5 iTask : integer;

6 numTasks : integer;

7 task : IOmniTaskControl;

8 begin

9 ...

10 oplDataManager := CreateDataManager(oplSourceProvider,

11 numTasks, dmOptions);

12 ...

13 for iTask := 1 to numTasks do begin

14 task := CreateTask(

15 procedure (const task: IOmniTask)

16 begin

17 ...

18 taskDelegate(task);

19 ...

20 end,

21 ...

22 task.Schedule(GParallelPool);

23 end;

24 ...

25 end;

26 end;

El gestor de datos es un miembro global del objeto TOmniParallelLoop<T>, para que pueda ser reutilizadosencillamente por el delegado de tarea. El delegado de tarea más simple posible (a continuación) solo crea unacola local y obtiene los valores desde la misma uno a uno. Esto da como resultado muchas colas locales — unapor tarea — todas conectadas al mismo gestor de datos.

En caso que estés preguntándote qué es loopBody – es el método anónimo que se le pasó al métodoParallel.ForEach.Execute.

Page 49: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 34

1 procedure InternalExecuteTask(const task: IOmniTask)

2 var

3 localQueue: TOmniLocalQueue;

4 value : TOmniValue;

5 begin

6 localQueue := oplDataManager.CreateLocalQueue;

7 try

8 while (not Stopped) and localQueue.GetNext(value) do

9 loopBody(task, value);

10 finally FreeAndNil(localQueue); end;

11 end;

Reiterando:

• Se crea un source provider (proveedor de datos de origen).• Se crea un gestor de datos (Data manager) y es asociado al proveedor de datos de origen.• Cada tarea crea su propia cola local y la utiliza para obtener acceso al origen de datos.• Como se verá en la sección siguiente, la cola local obtiene los datos en paquetes (data package) y losenvía a un buffer de salida que asegura que esta salida se produce en el orden correcto (la parte delbuffer de salida ocurre solamente si se ha llamado al método PreserveOrder en el código de alto nivel).

• Si la tarea se queda sin trabajo, pide un nuevo paquete de datos al gestor de datos, que lo obtiene desdeel proveedor de datos de origen (más al respecto luego). Si el proveedor de datos de origen se queda sindatos, el gestor de datos tratará de robar algunos datos de otras tareas.

Page 50: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 35

Todo esto ha sido diseñado para proveer acceso rápido a datos (los bloqueos están limitados al proveedor dedatos de origen, todas las otras interacciones son libres de bloqueos), buena distribución de la carga de trabajo(cuándo una tarea se queda sin trabajo antes que otras tareas, esta robará algún trabajo de otras tareas) yordenamiento de la salida (cuándo se requiere).

2.3.10.1 Proveedor de datos de origen

Un proveedor de datos de origen es un objeto que recupera datos desde la fuente de enumeración (los datos quele fueron pasados al for paralelo) y los re-empaca en un formato apto para el consumo paralelo. Actualmentehay tres proveedores de datos de origen definidos en la unidad OtlDataManager.

• TOmniIntegerRangeProvider

Page 51: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 36

Itera sobre un rango de enteros (de manera similar a como lo hace una clausula for ‘normal’). Comotal, este realmente no recupera datos desde una fuente de enumeración, sino los genera internamente.

• TOmniValueEnumeratorProvider

Itera sobre IOmniValueEnumerator, que es un enumerador especial que puede accederse desde múltipleslectores sin requerir bloqueos. En la actualidad solo se provee por la IOmniBlockingCollection.

• TOmniEnumeratorProvider

Itera sobre enumeradores Windows (IEnumerator) o enumeradores Delphi (GetEnumerator, envueltoen una clase TOmniValueEnumerator).

Todos los proveedores de datos de origen descienden de la clase abstracta TOmniSourceProvider que proveeuna interfaz común de proveedor de datos de origen. En teoría, se podría utilizar una interfaz para esepropósito, pero en la práctica los proveedores de datos de origen son cruciales para el desempeño y el nousar interfaces acelera el programa en una cantidad apreciable.

1 TOmniSourceProvider = class abstract

2 public

3 function Count: int64; virtual; abstract;

4 function CreateDataPackage: TOmniDataPackage; virtual; abstract;

5 function GetCapabilities: TOmniSourceProviderCapabilities;

6 virtual; abstract;

7 function GetPackage(dataCount: integer;

8 package: TOmniDataPackage): boolean; virtual; abstract;

9 function GetPackageSizeLimit: integer; virtual; abstract;

10 end;

No todos los proveedores de datos de origen que se crean son iguales y por eso la función GetCapabilities

devuelva las capacidades del proveedor:

1 TOmniSourceProviderCapability = (

2 spcCountable, // proveedor que conoce cuántos datos posee

3 spcFast, // las operaciones en el proveedor son O(1)

4 spcDataLimit // el packete de datos tiene límite de tamaño

5 );

6

7 TOmniSourceProviderCapabilities = set of

8 TOmniSourceProviderCapability;

TOmniIntegerRangeProvider es tanto contable (spcCountable) (es muy fácil saber cuántos valores hayentre 1 y 10, por ejemplo) y rápido (spcFast) (se necesita la misma cantidad de tiempo para obtener 10

valores o 10.000 valores) mientras los otros dos proveedores de datos de origen no son ni spcCountable nispcFast. La tercera capacidad, spcDataLimit, es obsoleta y no se utiliza más. Fue remplazada por el métodoGetPackageSizeLimit.

Otro aspecto importante del proveedor de datos de origen es el método GetPackage. Este accede la fuente(asegurando el acceso con un bloqueo si es necesario), obtiene datos y los devuelve en un paquete de datos. La

Page 52: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 37

implementación depende en gran medida de la fuente de datos. Por ejemplo, un proveedor de datos enterossimplemente avanza el valor actual del campo low y devuelve un paquete de datos que, en lugar de contenerun puñado de valores contiene solamente los límites inferior y superior (y por eso es considerado rápido).El proveedor de datos enumerados bloquea la fuente, recupera los datos y construye un paquete de datosvalor por valor. Y en el caso más sencillo, el proveedor de datos de origen TOmniValueEnumerator solamenterecupera valores y construye el paquete de datos.

1 function TOmniValueEnumeratorProvider.GetPackage(dataCount: integer;

2 package: TOmniDataPackage): boolean;

3 var

4 iData : integer;

5 intPackage: TOmniValueEnumeratorDataPackage absolute package;

6 timeout : cardinal;

7 value : TOmniValue;

8 begin

9 Assert(not StorePositions);

10 Result := false;

11 dataCount := intPackage.Prepare(dataCount);

12 timeout := INFINITE;

13 for iData := 1 to dataCount do begin

14 if not vepEnumerator.TryTake(value, timeout) then

15 break; //for

16 intPackage.Add(value);

17 timeout := 0;

18 Result := true;

19 end;

20 end;

2.3.10.2 Gestor de datos

El gestor de datos (Data manager) es el eje central en la jerarquía OtlDataManager. Se ubica entre múltiplescolas locales y un único proveedor de datos de origen y se asegura que todas las tareas paralelas tengan siemprealgún trabajo por hacer.

Dos distintos gestores de datos han sido implementados a la fecha – un gestor de datos contable y un gestorde datos heurístico. El primero es utilizado si el proveedor de datos de origen es contable (spcCountable) y elúltimo si no lo es. Ambos descienden de la clase abstracta TOmniDataManager.

Page 53: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 38

1 TOmniDataManager = class abstract

2 public

3 function CreateLocalQueue: TOmniLocalQueue; virtual; abstract;

4 function AllocateOutputBuffer: TOmniOutputBuffer;

5 virtual; abstract;

6 function GetNext(package: TOmniDataPackage): boolean;

7 virtual; abstract;

8 procedure ReleaseOutputBuffer(buffer: TOmniOutputBuffer);

9 virtual; abstract;

10 procedure SetOutput(const queue: IOmniBlockingCollection);

11 overload; virtual; abstract;

12 end;

La principal diferencia entre ellos radica en la función GetNextFromProvider que lee datos desde el proveedorde origen (llamando a su método GetPackage). En el proveedor contable este es un sencillo forwarder mientrasque en el proveedor heurístico esta función trata de encontrar un buen tamaño de paquete que permita laejecución de todas las tareas paralelas a la máxima velocidad. %%todo: forwarder en este contexto?

1 function TOmniHeuristicDataManager.GetNextFromProvider(

2 package: TOmniDataPackage; generation: integer): boolean;

3 const

4 CDataLimit = Trunc(High(integer) / CFetchTimeout_ms);

5 var

6 dataPerMs: cardinal;

7 dataSize : integer;

8 time : int64;

9 begin

10 // the goal is to fetch as much (but not exceeding <fetch_limit>)

11 // data as possible in <fetch_timeout> milliseconds; highest amount

12 // of data is limited by the GetDataCountForGeneration method.

13 dataSize := GetDataCountForGeneration(generation);

14 if dataSize > hdmEstimatedPackageSize.Value then

15 dataSize := hdmEstimatedPackageSize.Value;

16 time := DSiTimeGetTime64;

17 Result := SourceProvider.GetPackage(dataSize, package);

18 time := DSiTimeGetTime64 - time;

19 if Result then begin

20 if time = 0 then

21 dataPerMs := CDataLimit

22 else begin

23 dataPerMs := Round(dataSize / time);

24 if dataPerMs >= CDataLimit then

25 dataPerMs := CDataLimit;

26 end;

27 // average over last four fetches for dynamic adaptation

Page 54: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 39

28 hdmEstimatedPackageSize.Value := Round

29 ((hdmEstimatedPackageSize.Value / 4 * 3) +

30 (dataPerMs / 4) * CFetchTimeout_ms);

31 end;

32 end;

2.3.10.3 Cola local

Cada tarea paralela lee datos de una cola local, que es una sencilla interfaz hacia el gestor de datos. La partemás importante de una cola local es su método GetNext que proporciona a la tarea el valor siguiente.

1 function TOmniLocalQueueImpl.GetNext(var value: TOmniValue): boolean;

2 begin

3 Result := lqiDataPackage.GetNext(value);

4 if not Result then begin

5 Result := lqiDataManager_ref.GetNext(lqiDataPackage);

6 if Result then

7 Result := lqiDataPackage.GetNext(value);

8 end;

9 end;

Cada cola local contiene un paquete de datos local. GetNext primero intenta leer el siguiente valor de esepaquete de datos. Si esto falla (el paquete de datos está vacío – ha sido procesado por completo), esta intentaobtener un nuevo paquete de datos del gestor de datos y (si esto tiene éxito) reintenta obtener el siguientedato de este nuevo paquete.

GetNext, en el gestor de datos, primero intenta obtener un nuevo paquete desde el proveedor de origen (através del método privado GetNextFromProvider que a su vez llama al método GetPackage del proveedor deorigen). Si esto falla, intenta robar parte de la carga de trabajo a otra tarea.

El robo es una característica que permite que estén activas todas las tareas paralelas hasta el último valor quees enumerado. Para implementarlo, el gestor de datos itera sobre todas las colas locales y trata de dividir elpaquete de datos de cada cola a la mitad. Si esto tiene éxito, la mitad del paquete de datos se deja en la colalocal original y la otra mitad es devuelta a la cola local que solicitó más datos.

La división del paquete es altamente dependiente del tipo de datos. Por ejemplo, un paquete de datos enterossimplemente re-calcula sus límites, mientras los paquetes basados en enumeraciones deben realizar copias delos datos.

Page 55: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 40

1 function TOmniValueEnumeratorDataPackage.Split(

2 package: TOmniDataPackage): boolean;

3 var

4 intPackage: TOmniValueEnumeratorDataPackage absolute package;

5 iValue : integer;

6 value : TOmniValue;

7 begin

8 Result := false;

9 for iValue := 1 to intPackage.Prepare(vedpApproxCount.Value div 2)

10 do begin

11 if not GetNext(value) then

12 break; //for

13 intPackage.Add(value);

14 Result := true;

15 end;

16 end;

2.3.10.4 Ordenación de la salida

La ordenación (PreserveOrder) se usa generalmente junto con el modificador Into. La razón para esto radicaen la integración entre la infraestructura de Parallel.ForEach y el código paralelo de usuario (el que seencuentra en ejecución como cargamento de Execute). En un ForEach ‘normal’, la salida de este cargamentoparalelo no está definida. Se permite la generación de cualquier salida en el cargamento y ForEach no se dapor enterado de nada sobre esta salida. En este caso la OTL no tiene la capacidad de preservar el ordenamientodebido a que — al menos desde el punto de vista de la biblioteca — el código paralelizado no produce salidaalguna.

Cuándo se utiliza Into, sin embargo, el código utiliza también una firma distinta (distintos parámetros).

1 Parallel.ForEach(1, CMaxTest)

2 .PreserveOrder

3 .Into(primeQueue)

4 .Execute(

5 procedure (const value: integer; var res: TOmniValue)

6 begin

7 if IsPrime(value) then

8 res := value;

9 end);

El cargamento paralelo recibe ahora dos parámetros. El primero es — como en el caso más común— el valor deentrada, mientras el segundo recibe el valor de salida. Como puede verse en el ejemplo, el código paralelizadopuede producir cero o una salida, pero no más.

Esta pequeña modificación lo cambia todo. Ya que la infraestructura paralela tiene control sobre el parámetrode salida, esta puede administrarlo internamente, asociarlo con la entrada y asegurarse que la salida seagenerada en el mismo orden en que estaba la entrada.

Page 56: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 41

Véamos el código interior – la parte que realiza la programación de tareas en paralelo. Cuándo se utiliza Into,InternalExecuteTask ejecuta el complicado código que sigue.

1 InternalExecuteTask(

2 procedure (const task: IOmniTask)

3 var

4 localQueue : TOmniLocalQueue;

5 outputBuffer_ref: TOmniOutputBuffer;

6 position : int64;

7 result : TOmniValue;

8 value : TOmniValue;

9 begin

10 oplDataManager.SetOutput(oplIntoQueueIntf);

11 localQueue := oplDataManager.CreateLocalQueue;

12 try

13 outputBuffer_ref := oplDataManager.AllocateOutputBuffer;

14 try

15 localQueue.AssociateBuffer(outputBuffer_ref);

16 result := TOmniValue.Null;

17 while (not Stopped) and

18 localQueue.GetNext(position, value) do

19 begin

20 loopBody(task, value, result);

21 if not result.IsEmpty then begin

22 outputBuffer_ref.Submit(position, result);

23 result := TOmniValue.Null;

24 end;

25 end;

26 finally

27 oplDataManager.ReleaseOutputBuffer(outputBuffer_ref);

28 end;

29 finally

30 FreeAndNil(localQueue);

31 end;

32 end);

Los puntos importantes aquí son:

• El gestor de datos se asocia con la cola de salida. (El campo oplIntoQueueIntf contiene el valor pasadoal método Into.)

• Se crea una cola local, igual a cuando se ejecuta un ForEach ‘normal’.• Se crea un buffer de salida por el gestor de datos y es asociado con la cola local.• Para cada entrada se ejecuta el código de usuario y cada salida no vacía es escrita al buffer de salida.• Se libera el buffer de salida, al igual que la cola local.

Page 57: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 42

La parte interesante se oculta tras bambalinas, dentro de la cola local, el gestor de datos y el buffer de salida.

La primera modificación radica en la fuente de datos. Cuándo se utiliza PreserveOrder, cada paquete dedatos conoce la posición de la que fue leído en la fuente. Para simplificar las cosas, en este caso, no se utiliza ladivisión de paquetes de datos. [Y por ello, el robo de datos no puede usarse, causando un uso un poco menosefectivo del CPU que en el caso del ForEach más sencillo.]

Cada cola local tiene un conjunto de buffers de salida asociado a ella.

Cada conjunto de buffers de salida gestiona dos buffers de salida. Uno está activo y la tarea escribe dentro deel y el otro puede estar libre o lleno. Cada buffer de salida se asocia con una posición de entrada – tal comolo está el paquete de datos.

Cuándo vemos la lectura/escritura de datos desde la perspectiva de una tarea, todo es bastante simple. Latarea lee datos desde la cola local (que lee datos desde un paquete de datos, asociado con cierta posición) ylos escribe a un buffer de salida (asociado con la misma posición).

La parte difícil viene cuando se agota el paquete de datos (La rama if not Result en el código siguiente).

1 function TOmniLocalQueueImpl.GetNext(var position: int64; var value: TOmniV\

2 alue): boolean;

3 begin

4 Result := lqiDataPackage.GetNext(position, value);

5 if not Result then begin

6 lqiBufferSet.ActiveBuffer.MarkFull;

7 lqiBufferSet.ActivateBuffer;

8 // this will block if alternate buffer is also full

9 Result := lqiDataManager_ref.GetNext(lqiDataPackage);

10 if Result then begin

11 Result := lqiDataPackage.GetNext(position, value);

12 if Result then

13 lqiBufferSet.ActiveBuffer.Range := lqiDataPackage.Range;

14 end;

Page 58: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 43

15 end;

16 end;

Primero, el buffer activo actualmente se marca como lleno. Esto provoca una llamada a NotifyBufferFull

(véase a continuación). Luego, se activa el buffer alterno. Esta llamada (ActivateBuffer) en realidad sebloqueará si el buffer alternativo no está libre. En este caso, el hilo actual es bloqueado hasta que uno desus buffers sea escrito en la cola de salida.

A partir de este punto, GetNext procede de la misma forma que cuándo es usando en un ForEach simple, conla excepción de que asigna la posición del buffer activo cada vez que se lee un nuevo paquete de datos delgestor.

La otra parte de la magia ocurre en el método que es llamado desde MarkFull. Este recorre la lista de buffersy comprueba si hay buffers de salida que están a) llenos y b) destinados a la posición de salida actual. Dichosbuffers son copiados a la salida y devueltos para su uso.

1 procedure TOmniBaseDataManager.NotifyBufferFull(

2 buffer: TOmniOutputBufferImpl);

3 begin

4 // Remove buffer from the list. Check if next buffer is waiting in

5 // the list. Copy buffer if it is full and repeat the process.

6 dmBufferRangeLock.Acquire;

7 try

8 while (dmBufferRangeList.Count > 0) and

9 (BufferList[0].Range.First = dmNextPosition) and

10 BufferList[0].IsFull do

11 begin

12 buffer := TOmniOutputBufferImpl(

13 dmBufferRangeList.ExtractObject(0));

14 dmNextPosition := buffer.Range.Last + 1;

15 buffer.CopyToOutput;

16 end;

17 finally dmBufferRangeLock.Release; end;

18 end;

Recapítulando:

• Cada buffer de datos está asociado a una posición.• Cada cola local tiene dos buffers de salida, uno está activo y el otro está libre o lleno.• Cada buffer de salida está también asociado a una posición.• La cola local escribe datos a un buffer de salida.• Cuándo el buffer está lleno, se coloca en una lista de buffers en espera. En este momento, todos losbuffers en espera apropiados son copiados a la salida.

Page 59: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Multi-hilos de alto nivel 44

2.3.11 Ejemplos

Un ejemplo práctico del uso de For Each se encuentra en los capítulos For paralelo con salida sincronizada yBúsqueda paralela en un Árbol.

Page 60: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

3 SincronizaciónAún cuando la biblioteca OmniThreadLibrary trata la comunicación con un enfoque superior al bloqueo,hay aún ocasiones donde resulta inevitable utilizar primitivas de sincronización “estándar” como las criticalsection. Dado que el enfoque estándar de Delphi/Windows hacia el bloqueo es de muy bajo nivel, OmniTh-readLibrary construye sobre y mejora este de manera significativa. Todas estas mejoras están reunidas en launidad OtlSync y se describen en las secciones siguientes. La única excepción es la clase/interfaz waitablevalue, que está declarada en la unidad OtlCommon.

Esta parte del libro asume que el lector tiene una comprensión básica de los bloqueos. Si eres nuevo al tema,primero debes leer los capítulos apropiados de alguno de los libros mencionados en la introducción.

45

Page 61: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Sincronización 46

3.1 Critical Sections

La primitiva de sincronización más útil para la programación multi-hilos es, sin duda, la critical section¹.

OmniThreadLibrary simplifica el compartir critical sections entre el propietario de una tarea y la tarea mismacon la utilización del método WithLock. Las tareas de alto nivel pueden acceder a este método a través delbloque de configuración de la tarea.

Siempre he tenido la opinión que los bloqueos deben ser tan granulares como sea posible. Poner muchosbloqueos pequeños en torno a muchas piezas de código no relacionado es mejor que usar un bloqueo gigantepara todo, pero los programadores con frecuencia utilizan uno o algunos pocos bloqueos debido a que lagestión de muchas critical sections puede ser una molestia.

Para ayudarte a escribir mejor código, OmniThreadLibrary implementa tres extensiones a la clase TCritical-Section de Delphi - IOmniCriticalSection, TOmniCS y Locked<T>.

3.1.1 IOmniCriticalSection

Delphi implementa el soporte a critical sections con la clase TCriticalSection, que debe ser creada y destruidaen el código. (También encontramos el registro TRTLCriticalSection, pero este está soportado únicamente enWindows.) OmniThreadLibrary extiende esta implementación con la interfaz IOmniCriticalSection, la cualsolamente debe crearse. El compilador se asegurará que esta sea liberada en el lugar apropiado.

1 type

2 IOmniCriticalSection = interface

3 ['{AA92906B-B92E-4C54-922C-7B87C23DABA9}']

4 function GetLockCount: integer;

5 //

6 procedure Acquire;

7 procedure Release;

8 function GetSyncObj: TSynchroObject;

9 property LockCount: integer read GetLockCount;

10 end;

11

12 function CreateOmniCriticalSection: IOmniCriticalSection;

IOmniCriticalSection utiliza internamente a TCriticalSection. Esta actúa únicamente como un proxy quellama a las funciones de TCriticalSection. Aparte de esto, provee la funcionalidad adicional de contar elnúmero de veces que una sección crítica ha sido adquirida, lo que puede resultar de mucha ayuda mientrasdepuramos. Este contador puede leerse a través de la propiedad LockCount.

Una sección crítica puede ser adquirida varias veces desde un hilo. Por ejemplo, el código quesigue es perfectamente válido:

¹http://es.wikipedia.org/wiki/Secci%C3%B3n_cr%C3%ADtica, http://docwiki.embarcadero.com/Libraries/XE3/en/System.SyncObjs.TCriticalSection

Page 62: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Sincronización 47

1 cSec := CreateOmniCriticalSection; //LockCount = 0

2 cSec.Acquire; //LockCount = 1

3 cSec.Acquire; //LockCount = 2

4 cSec.Release; //LockCount = 1

5 cSec.Release; //LockCount = 0

Adicionalmente, IOmniCriticalSection no utiliza directamente TCriticalSection, sino la envuelve en unobjeto de mayor tamaño, tal como fue sugerido por Eric Grange².

3.1.2 TOmniCS

Otra extensión de TCriticalSection que encontramos en OmniThreadLibrary es el registro TOmniCS. Estepermite utilizar una sección crítica con solo declarar un registro en el lugar adecuado.

Utilizando TOmniCS, el bloqueo puede ser tan simple como esto:

1 uses

2 GpLists,

3 OtlSync;

4

5 procedure ProcessList(const intf: IGpIntegerList);

6 begin

7 //...

8 end;

9

10 var

11 lock: TOmniCS;

12 intf: IGpIntegerList;

13

14 procedure Test1;

15 begin

16 intf := TGpIntegerList.Create;

17 //...

18 lock.Acquire;

19 try

20 ProcessList(intf);

21 finally lock.Release; end;

22 end;

TOmniCS ha sido implementado como un registro con un miembro privado que almacena una interfazIOmniCriticalSection.

²http://delphitools.info/2011/11/30/fixing-tcriticalsection/

Page 63: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Sincronización 48

1 type

2 TOmniCS = record

3 strict private

4 ocsSync: IOmniCriticalSection;

5 private

6 function GetLockCount: integer; inline;

7 function GetSyncObj: TSynchroObject; inline;

8 public

9 procedure Initialize;

10 procedure Acquire; inline;

11 procedure Release; inline;

12 property LockCount: integer read GetLockCount;

13 property SyncObj: TSynchroObject read GetSyncObj;

14 end;

El método Release simplemente llama al método Release de la interfaz interna, mientras el método Acquirees más complicado ya que tiene que inicializar primero el miembro ocsSync.

1 procedure TOmniCS.Acquire;

2 begin

3 Initialize;

4 ocsSync.Acquire;

5 end;

6

7 procedure TOmniCS.Release;

8 begin

9 ocsSync.Release;

10 end;

La inicialización, encapsulada dentro del método Initialize (que también puede ser llamado desde códigopara inicializar explícitamente la sección crítica) es bastante complejo debido que debe inicializar ocsSyncsolo una vez y debe trabajar correctamente cuándo es llamado desde dos sitios (dos hilos) al mismo tiempo.Esto se logra utilizando un enfoque de inicialización optimista, que se describe más adelante en este capítulo.

1 procedure TOmniCS.Initialize;

2 var

3 syncIntf: IOmniCriticalSection;

4 begin

5 Assert(cardinal(@ocsSync) mod SizeOf(pointer) = 0,

6 'TOmniCS.Initialize: ocsSync is not properly aligned!');

7 Assert(cardinal(@syncIntf) mod SizeOf(pointer) = 0,

8 'TOmniCS.Initialize: syncIntf is not properly aligned!');

9 if not assigned(ocsSync) then begin

10 syncIntf := CreateOmniCriticalSection;

Page 64: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Sincronización 49

11 if CAS(nil, pointer(syncIntf), ocsSync) then

12 pointer(syncIntf) := nil;

13 end;

14 end;

3.1.3 Locked

TOmniCS es en sí una gran simplificación al concepto de sección crítica, pero aún requiere la declaración de unaentidad separada de bloqueo. Si esta entidad de bloqueo es utilizada para sincronizar el acceso a una instanciaespecífica (sea esta un objeto, registro, interfaz o aún un tipo simple) algunas veces es mejor declarar unavariable/miembro de tipo Locked<T> que combina cualquier tipo con una critical section.

Utilizando Locked<T>, el ejemplo de la sección TOmniCS puede re-escribirse de la siguiente manera.

1 uses

2 GpLists,

3 OtlSync;

4

5 procedure ProcessList(const intf: IGpIntegerList);

6 begin

7 //...

8 end;

9

10 var

11 lockedIntf: Locked<IGpIntegerList>;

12

13 procedure Test2;

14 begin

15 lockedIntf := TGpIntegerList.CreateInterface;

16 //...

17 lockedIntf.Acquire;

18 try

19 ProcessList(lockedIntf);

20 finally lockedIntf.Release; end;

21 end;

El hecho interesante a notar es que aun cuando lockedIntf se ha declarado como una variable de tipoLocked<IGpIntegerList>, esta puede inicializarse y utilizarse como si fuera del tipo IGpIntegerList. Estose logra al proveer operadores Implicit para la conversión de Locked<T> a T y viceversa. El compilador deDelphi (tristemente) no es lo suficientemente inteligente para utilizar este operador de conversión en algunoscasos, por lo que de vez en cuando aún tiene que utilizarse la propiedad Value. Por ejemplo, se tiene quehacer de esta forma para liberar el objeto envuelto. (En el ejemplo anterior se ha envuelto una interfaz y elcompilador mismo se ha encargado de la destrucción.)

Page 65: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Sincronización 50

1 procedure ProcessObjList(obj: TGpIntegerList);

2 begin

3 //...

4 end;

5

6 var

7 lockedObj: Locked<TGpIntegerList>;

8

9 procedure Test3;

10 begin

11 lockedObj := TGpIntegerList.Create;

12 try

13 //...

14 lockedObj.Acquire;

15 try

16 ProcessObjList(lockedObj);

17 finally lockedObj.Release; end;

18 //...

19 finally lockedObj.Value.Free; end;

20 end;

Además de los métodos estándar Acquire/Release, Locked<T> también implementa métodos utilizados parahacer bloqueos pesimistas, que se describen más adelante en este capítulo, y dos métodos casi idénticosllamados Locked que permiten la ejecución de un segmento de código (un procedimiento, método o métodoanónimo) mientras la sección crítica es adquirida. (En otras palabras, se puede asegurar que el código pasadoal método Locked se ejecuta siempre una sola vez, siempre que todo el código en el programa bloqueeadecuadamente el acceso a la variable compartida.)

1 type

2 Locked<T> = record

3 public

4 type TFactory = reference to function: T;

5 type TProcT = reference to procedure(const value: T);

6 constructor Create(const value: T; ownsObject: boolean = true);

7 class operator Implicit(const value: Locked<T>): T; inline;

8 class operator Implicit(const value: T): Locked<T>; inline;

9 function Initialize(factory: TFactory): T; overload;

10 {$IFDEF OTL_ERTTI}

11 function Initialize: T; overload;

12 {$ENDIF OTL_ERTTI}

13 procedure Acquire; inline;

14 procedure Locked(proc: TProc); overload; inline;

15 procedure Locked(proc: TProcT); overload; inline;

16 procedure Release; inline;

17 procedure Free; inline;

Page 66: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Sincronización 51

18 property Value: T read GetValue;

19 end;

20

21 procedure Locked<T>.Locked(proc: TProc);

22 begin

23 Acquire;

24 try

25 proc;

26 finally Release; end;

27 end;

28

29 procedure Locked<T>.Locked(proc: TProcT);

30 begin

31 Acquire;

32 try

33 proc(Value);

34 finally Release; end;

35 end;

3.1.3.1 ¿Por qué no utilizar TMonitor?

Hay una alternativa incluida en Delphi desde el 2009, que provee una funcionalidad similar a la deLocked<T> – TMonitor. En Delphi moderno, cada objeto puede ser bloqueado llamando a la funciónSystem.TMonitor.Enter y desbloqueado utilizando System.TMonitor.Exit. El ejemplo anterior podría re-escribirse para utilizar TMonitor sin mayor trabajo.

1 var

2 obj: TGpIntegerList;

3

4 procedure Test4;

5 begin

6 obj := TGpIntegerList.Create;

7 try

8 //...

9 System.TMonitor.Enter(obj);

10 try

11 ProcessObjList(obj);

12 finally System.TMonitor.Exit(obj); end;

13 //...

14 finally FreeAndNil(obj); end;

15 end;

Una pregunta razonable es, entonces, ¿por qué implementar Locked<T>?. ¿Por qué TMonitor no es suficiente-mente bueno? Hay muchas razones para ello.

Page 67: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Sincronización 52

• TMonitor ha sido defectuoso desde su concepción³ (aunque creo que Enter and Exit podrían ser losuficientemente estables como para código de producción) y no me gusta utilizarlo.

• La utilización de TMonitor no refleja sus intenciones. Al solo ver la declaración de una variable/miem-bro, no te das cuenta que se supone que la entidad se utiliza de manera thread-safe. La utilización deLocked<T>, sin embargo, declara explícitamente esa intención.

• TMonitor.Enter/Exit no funciona con interfaces, registros y tipos primitivos. Locked<T> si lo hace.

³http://stackoverflow.com/questions/4856306/tthreadedqueue-not-capable-of-multiple-consumers, http://www.thedelphigeek.com/2011/05/tmonitor-bug.html

Page 68: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

4 How-toEsta parte del libro contiene ejemplos prácticos del uso de OmniThreadLibrary. Cada uno de ellos inicia conuna pregunta que presenta el problema y continúa con la discusión de la solución.

Se cubre los siguientes temas:

• Barrido de archivos en segundo plano

Barrido de carpetas y archivos en un hilo en segundo plano.

• Async/Await

Cómo escribir código multi-hilo ‘con bloqueo’ con un mecanismo parecido a async/await de .NET.

• Descarga Web y Almacenamiento en Base de Datos

Múltiples trabajadores descargando datos y almacenándolos en una sola base de datos.

• For paralelo con salida sincronizada

Redirigir la salida de un bucle for paralelo dentro de una estructura que no soporta el acceso multi-hilo.

• Trabajador en segundo plano y particionado de lista

Escritura de procesamiento en segundo plano al estilo de un servidor.

• Producción de datos en paralelo

Múltiples trabajadores que generan datos y los escriben a un único archivo.

• Construcción de un pool de conexiones

Utilización de OmniThreadLibrary para crear un pool de conexiones a base de datos.

• QuickSort y Max Paralelo

Cómo ordenar un arreglo y cómo procesar un arreglo usando múltiples hilos.

53

Page 69: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

How-to 54

• Búsqueda Paralela en un Árbol

Encontrar datos en un árbol.

• Trabajadores múltiples con frames múltiples

Interfaz de usuario gráfica que contiene múltiples frames, donde cada frame actúa como front-end para unatarea en segundo plano.

• OmniThreadLibrary y Bases de Datos

Utilización de bases de datos desde OmniThreadLibrary.

• OmniThreadLibrary y COM/OLE

Utilización de COM/OLE desde OmniThreadLibrary.

Page 70: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

How-to 55

4.1 Async/Await

..

.NET 4.5 introdujo los muy útiles conceptos asíncronos – async y await. ¿Hay alguna forma de implementarestas construcciones del lenguaje en Delphi?

En corto – estas no pueden ser implementadas, solamente emuladas.

Antes de mostrar cómo hacerlo, volvamos a lo básico y veamos cómo se podría utilizar async/await siexistieran en Delphi.

Asumamos que hemos heredado este código bastante inútil.

1 procedure TForm125.Button1Click(Sender: TObject);

2 var

3 button: TButton;

4 begin

5 button := Sender as TButton;

6 button.Caption := 'Working ...';

7 button.Enabled := false;

8 Sleep(5000);

9 button.Enabled := true;

10 button.Caption := 'Done!';

11 end;

Ahora, el jefe dice que, hay que hacerlo paralelo para que el usuario pueda iniciar tres copias de él. (Tambiénhay que añadir dos nuevos botones al formulario para iniciar estas instancias, pero eso se hace fácilmente.)

Hay muchas formas de resolver este problema, algunas más y otras menos complicadas; me gustaría señalarla solución más simple. Pero primero, tomemos un desvío por las aguas de .NET…

.NET 4.5 introdujo el súper mágico concepto de ‘async’ y ‘await’¹. En pocas palabras, este nos permite escribircódigo como este:

1 procedure TForm125.Button1Click(Sender: TObject); async;

2 var

3 button: TButton;

4 begin

5 button := Sender as TButton;

6 button.Caption := 'Working ...';

7 button.Enabled := false;

8 await CreateTask(

9 procedure begin

¹http://blogs.msdn.com/b/pfxteam/archive/2012/04/12/10293335.aspx

Page 71: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

How-to 56

10 Sleep(5000);

11 end);

12 button.Enabled := true;

13 button.Caption := 'Done!';

14 end;

[Hay que tener en cuenta que esta sintaxis no está soportada; es solo un ejemplo de como la sintaxis de .NETpodría lucir si Delphi la soportara.]

El truco aquí es que await en realidad no espera. Este cede el control de vuelta al ciclo principal, quecontinúa procesando eventos, etc. En otras palabras – el resto del programa sigue funcionando como decostumbre. También podría llamar a otra función asíncrona y esperarla. Solo cuándo la función asíncronaretorna (cualquiera de ellas si hay más de una corriendo), se retorna el control al punto de la llamada a awaitapropiada y el código continúa con la siguiente línea. [Carlo Kok escribió un buen artículo sobre como trabajaawait (how await works²) en el blog de RemObjects.]

Async/Awayt necesita un extensivo soporte del compilador y absolutamente no hay manera de escribir unclon de async/await in Delphi. Pero… hay un simple truco que nos permite escribir código en casi la mismamanera. Utiliza la construcción Async de OmniThreadLibrary y la magia de los métodos anónimos.

1 procedure TForm125.Button1Click(Sender: TObject);

2 var

3 button: TButton;

4 begin

5 button := Sender as TButton;

6 button.Caption := 'Working ...';

7 button.Enabled := false;

8 Parallel.Async(

9 procedure begin

10 Sleep(5000);

11 end,

12 Parallel.TaskConfig.OnTerminate(

13 procedure begin

14 button.Enabled := true;

15 button.Caption := 'Done!';

16 end));

17 end;

Async ejecuta su parámetro (el delegado que contiene la llamada a Sleep) en un hilo en segundo plano. Cuándose completa esta tarea en segundo plano, Async ejecuta el segundo parámetro (el delegado OnTerminate) en elhilo principal. Mientras la tarea en segundo plano está trabajando, el hilo principal hace girar su propio buclede mensajes y ejecuta la interfaz de usuario – al igual que ocurriría en el caso de .NET.

Con un poco de azúcar sintáctica, se puede fingir de manera convincente un comportamiento similar al de.NET.

²http://blogs.remobjects.com/blogs/ck/2012/08/08/p4690

Page 72: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

How-to 57

1 type

2 IAwait = interface

3 procedure Await(proc: TProc);

4 end;

5

6 TAwait = class(TInterfacedObject, IAwait)

7 strict private

8 FAsync: TProc;

9 public

10 constructor Create(async: TProc);

11 procedure Await(proc: TProc);

12 end;

13

14 function Async(proc: TProc): IAwait;

15 begin

16 Result := TAwait.Create(proc);

17 end;

18

19 { TAwait }

20

21 constructor TAwait.Create(async: TProc);

22 begin

23 inherited Create;

24 FAsync := async;

25 end;

26

27 procedure TAwait.Await(proc: TProc);

28 begin

29 Parallel.Async(FAsync, Parallel.TaskConfig.OnTerminated(

30 procedure begin

31 proc;

32 end));

33 end;

34

35 { TForm125 }

36

37 procedure TForm125.Button1Click(Sender: TObject);

38 var

39 button: TButton;

40 begin

41 button := Sender as TButton;

42 button.Caption := 'Working ...';

43 button.Enabled := false;

44 Async(

45 procedure begin

Page 73: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

How-to 58

46 Sleep(5000);

47 end).

48 Await(

49 procedure begin

50 button.Enabled := true;

51 button.Caption := 'Done!';

52 end);

53 end;

Para probarlo, hay que poner tres botones en un formulario y asignar el manejador Button1Click a todosellos. Hacer clic y disfrutar.

Page 74: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

How-to 59

4.2 QuickSort y Max Paralelo

..

Me gustaría ordenar un gran arreglo de datos, pero mi función de comparación es bastante complicada y laordenación tarda mucho tiempo. ¿Puedo usar OmniThreadLibrary para acelerar el ordenamiento?

En un tema similar – a veces también me gustaría encontrar el mayor elemento de los datos en este granarreglo, sin realizar el ordenamiento. ¿Cómo planteo este problema?*

La respuesta a ambas partes del problema es la misma – la utilización de la abstracción Fork/Join.

4.2.1 QuickSort

La primera parte de este how-to implementa el bien conocido algoritmo quicksort³ de forma paralela (vea laaplicación demo 44_Fork-Join QuickSort para el código completo).

Vamos a iniciar con un ordenador mono-hilo no optimizado. Esta implementación simple puede convertirsefácilmente a la forma multi-hilo.

1 procedure TSequentialSorter.QuickSort(left, right: integer);

2 var

3 pivotIndex: integer;

4 begin

5 if right > left then begin

6 if (right - left) <= CSortThreshold then

7 InsertionSort(left, right)

8 else begin

9 pivotIndex := Partition(left, right, (left + right) div 2);

10 QuickSort(left, pivotIndex - 1);

11 QuickSort(pivotIndex + 1, right);

12 end;

13 end;

14 end;

Como puede verse, el código cambia a insertion sort cuándo la dimensión del arreglo cae por debajo de ciertoumbral. Esto no es muy importante para la versión mono-hilo (solo brinda una pequeña aceleración) peroayudará enormemente con la versión multi-hilo.

La conversión de este QuickSort a una versión multi-hilo es bastante simple.

En primer lugar vamos a crear un pool computacional fork/join. En este ejemplo, este se almacena en uncampo global.

³http://es.wikipedia.org/wiki/Quicksort

Page 75: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

How-to 60

1 FForkJoin := Parallel.ForkJoin;

En segundo lugar, adaptaremos el método ‘QuickSort’.

1 procedure TParallelSorter.QuickSort(left, right: integer);

2 var

3 pivotIndex: integer;

4 sortLeft : IOmniCompute;

5 sortRight : IOmniCompute;

6 begin

7 if right > left then begin

8 if (right - left) <= CSortThreshold then

9 InsertionSort(left, right)

10 else begin

11 pivotIndex := Partition(left, right, (left + right) div 2);

12 sortLeft := FForkJoin.Compute(

13 procedure

14 begin

15 QuickSort(left, pivotIndex - 1);

16 end);

17 sortRight := FForkJoin.Compute(

18 procedure

19 begin

20 QuickSort(pivotIndex + 1, right);

21 end);

22 sortLeft.Await;

23 sortRight.Await;

24 end;

25 end;

26 end;

El código luce mucho más largo, pero los cambios son realmente simples. Cada llamada recursiva a QuickSortha sido remplazada por una llamada a Compute…

1 sortLeft := FForkJoin.Compute(

2 procedure

3 begin

4 QuickSort(left, pivotIndex - 1);

5 end);

… y el código espera sobre ambas sub-tareas.

En lugar de llamar directamente a QuickSort, la versión paralela crea la interfaz IOmniCompute llamando aFForkJoin.Compute. Esto crea una sub-tarea que envuelve la función anónima que ha sido pasada a Computey pone esta sub-tarea en el pool computacional de fork/join.

Page 76: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

How-to 61

Esta tarea es leída más adelante desde este pool por uno de los trabajadores de fork/join y es procesada en elhilo en segundo plano.

Llamando a Await comprobamos si la sub-tarea ha finalizado su trabajo. En ese caso, Await simplementeretorna y el código puede proceder. De otra forma (la sub-tarea aun está trabajando), Await trata de obteneruna sub-tarea del pool computacional, la ejecuta, y luego repite desde el inicio (comprobando si la sub-tareaha terminado su trabajo). De esta forma, todos los hilos están siempre ocupados ejecutando su propio códigoo una sub-tarea del pool computacional.

Debido a que dos interfaces IOmniCompute son almacenadas en la pila en cada llamada a QuickSort, estecódigo utiliza más espacio de pila que la versión de un solo hilo. Esa es la razón principal por la cual laejecución paralela es detenida en cierto nivel y se utiliza una versión secuencial simple para ordenar el resto.

4.2.2 Max Paralelo

La segunda parte de este how-to encuentra el mayor elemento de un arreglo en paralelo (véase la aplicacióndemo 45_Fork-Join max para el código completo).

La solución paralela es similar al ejemplo anterior de QuickSort con algunas diferencias importantes enrelación al hecho de que el código debe devolver un valor (el código de QuickSort solamente ordena el arreglosin devolver nada).

Esto afecta directamente la utilización de la interfaz – en lugar de trabajar con IOmniForkJoin e IOmniComputeel código utiliza IOmniForkJoin<T> e IOmniCompute<T>. Como nuestro arreglo de ejemplo contiene enteros,el código paralelo crea un IOmniForkJoin<integer> y se lo pasa a la función ParallelMax.

1 max := ParallelMax(Parallel.ForkJoin<integer>, Low(FData), High(FData));

En este ejemplo, el pool computacional fork/join es pasado como parámetro. Este planteamiento esmás flexiblepero también un poco más lento y – más importante – utiliza más espacio de stack.

1 function ParallelMax(

2 const forkJoin: IOmniForkJoin<integer>;

3 left, right: integer): integer;

4

5 var

6 computeLeft : IOmniCompute<integer>;

7 computeRight: IOmniCompute<integer>;

8 mid : integer;

9

10 function Compute(left, right: integer): IOmniCompute<integer>;

11 begin

12 Result := forkJoin.Compute(

13 function: integer

14 begin

15 Result := ParallelMax(forkJoin, left, right);

16 end

Page 77: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

How-to 62

17 );

18 end;

19

20 begin

21 if (right - left) < CSeqThreshold then

22 Result := SequentialMax(left, right)

23 else begin

24 mid := (left + right) div 2;

25 computeLeft := Compute(left, mid);

26 computeRight := Compute(mid + 1, right);

27 Result := Max(computeLeft.Value, computeRight.Value);

28 end;

29 end;

Cuándo el sub-rango del arreglo es lo suficientemente pequeño, ParallelMax llama a la versión secuencial(mono-hilo) – tal como lo hizo QuickSort, y por la misma razón – para no agotar el espacio en el stack.

Con un sub-rango grande, el código crea dos sub-tareas IOmniCompute<integer> cada una envolviendo unafunción que devuelve un entero. Esta función a su vez llama de vuelta a ParallelMax (con un rango máspequeño). Para obtener el resultado de la función anónima envuelta por Compute, el código llama a la funciónValue. Al igual que Await, Value o bien devuelve un resultado (si ya ha sido computado) o ejecuta otra sub-tarea fork/join desde el pool computacional.

Cuándo se crean programas fork/join, hay que tener en mente este anti-patrón. ¡El fragmento de códigoa continuación es incorrecto!

1 Result := Max(Compute(left, mid).Value,

2 Compute(mid + 1, right).Value);

¡Siempre debe crearse todas las sub-tareas antes de llamar a Await o Value! De otra forma, el código nose ejecutará para nada en paralelo – ¡todo será procesado por un único hilo!

Page 78: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

5 Aplicaciones DemoLa distribución y el trunk de SVN de OmniThreadLibrary incluye un montón de aplicaciones demo que teayudarán a empezar. Están almacenadas en la subcarpeta tests. Este capítulo lista todas esas aplicaciones.

• 0_Beep El código multi-hilos más sencillo posible con OmniThreadLibrary.• 1_HelloWorld “Hola, mundo” multi-hilos con un componente TOmniEventMonitor creado en tiempode corrida.

• 2_TwoWayHello “Hola, mundo” con comunicación bi-direccional; TOmniEventMonitor creado entiempo de corrida.

• 3_HelloWorld_with_package “Hola, mundo” multi-hilos con un componente TOmniEventMonitor enel formulario.

• 4_TwoWayHello_with_package “Hola, mundo” con comunicación bi-direccional; componente TOmni-EventMonitor en el formulario.

• 5_TwoWayHello_without_loop “Hola, mundo” con comunicación bi-direccional; al estilo de OTL.• 6_TwoWayHello_with_object_workerObsoleto, casi identico a la demo 5_TwoWayHello_without_loop.• 7_InitTest Demuestra .WaitForInit, .ExitCode, .ExitMessage, y .SetPriority.• 8_RegisterComm Demuestra la creación de canales de comunicación adicionales.• 9_Communications Comprobador simple del subsistema de comunicaciones.• 10_Containers Comprobador del subsistema de comunicaciones en toda regla. Se utiliza para verificarque el código libre de bloqueos sea correcto.

• 11_ThreadPool Demo de un pool de Threads.• 12_Lock Demuestra .WithLock.• 13_Exceptions Demuestra como atrapar de excepciones.• 14_TerminateWhen Demuestra .TerminateWhen and .WithCounter.• 15_TaskGroup Demuestra los grupos de tareas.• 16_ChainTo Demuestra .ChainTo.• 17_MsgWait Demuestra .MsgWait y cómo procesar mensajes de windows dentro de las tareas.• 18_StringMsgDispatch Llamadas a métodos de tareas por nombre y dirección.• 19_StringMsgBenchmark Benchmark de las diferentes maneras de invocar un método de una tarea.• 20_QuickSort Demo de QuickSort en paralelo.• 21_Anonymous_methods Demuestra el uso de métodos anónimos como código de trabajo de tareas enDelphi 2009.

• 22_Termination Comprobaciones para .Terminate y .Terminated.• 23_BackgroundFileSearch Demuestra como explorar archivos en un hilo en segundo plano.• 24_ConnectionPool Demuestra como crear un pool de conexiones con OmniThreadLibrary.• 25_WaitableComm Demo de ReceiveWait y SendWait.• 26_MultiEventMonitor Como ejecutar multiples monitores de eventos en paralelo.• 27_RecursiveTree Procesamiento de un árbol en paralelo.• 28_Hooks Demo del nuevo sistema de hooks.

63

Page 79: Programación Paralela con OmniThreadLibrarysamples.leanpub.com/omnithreadlibrary-es-sample.pdf · 2/3/2011 · IntroducciónaOmniThreadLibrary 5 1.3Instalación 1. DescargalaúltimaversiónestabledeGoogleCode³ohazcheckoutdelSVNtrunk⁴.Porlogenerales

Aplicaciones Demo 64

• 29_ImplicitEventMonitor Demo de OnMessage y OnTerminated, con enfoque de método nombrado.• 30_AnonymousEventMonitor Demo de OnMessage y OnTerminated, con enfoque de método anónimo.• 31_WaitableObjects Demo de la API RegisterWaitObject/UnregisterWaitObject.• 32_Queue Prueba de esfuerzo para TOmniBaseQueue y TOmniQueue.• 33_BlockingCollection Prueba de esfuerzo para TOmniBlockingCollection, también demuestra el usodel Entorno para establecer la afinidad del proceso.

• 34_TreeScan Exploración de un árbol en paralelo usando TOmniBlockingCollection.• 35_ParallelFor Exploración de un arbol usando Parallel.ForEach (Delphi 2009 y superiores).• 37_ParallelJoin ParallelJoin: demo de Parallel.Join.• 38_OrderedFor Bucles for paralelos ordenados.• 39_Future Futuros.• 40_Mandelbrot Demo muy simple de gráficos en paralelo.• 41_Pipeline Proceso multi-etapas en paralelo• 42_MessageQueue Prueba de esfuerzo para TOmniMessageQueue.• 43_InvokeAnonymous Demo para IOmniTask.Invoke.• 44_Fork-Join QuickSort Implementación de QuickSort utilizando Parallel.ForkJoin.• 45_Fork-Join max Implementación de Max(array) utilizando Parallel.ForkJoin.• 46_Async Demo de Parallel.Async.• 47_TaskConfig Demo de configuración de tarea con Parallel.TaskConfig.• 48_OtlParallelExceptions Manejo de excepciones en construcciones OTL de alto nivel.• 49_FramedWorkers Multiple frames each communication with own worker task.

• 50_OmniValueArray Envolviendo arreglos, hashes y registros en TOmniValue.• 51_PipelineStressTest Prueba de esfuerzo de Pipeline por [Anton Alisov].• 52_BackgroundWorker Demo de la abstracción Parallel.BackgroundWorker.• 53_AsyncAwait Demo de la abstracción Async/Await.