|
Procesadores multi-core
El procesamiento paralelo está ocupando el centro del escenario con
la aparición de los procesadores multi-core de bajo precio.
La conocida Ley de Moore (formulada por Gordon E. Moore, cofundador de
Intel, en 1965) se aplica ahora a los procesadores multi-core y se espera
que el numero de cores se duplique cada 18 meses.
Hace dos años (2009) una PC de alta gama era "dual-core", actualmente es
"quad-core", para el año 2015 se esperan 16-cores y para el
año 2020 64-cores.
Pero las aplicaciones puramente "secuenciales" actuales no permiten
aprovechar los procesadores paralelos y el aprovechamiento queda
limitado básicamente al multi-tasking que el Sistema Operativo realiza.
Para el aprovechamiento de la gran cantidad de procesadores paralelos
prevista se requiere introducir cambios importantes en las arquitecturas de
software y en los procesos de desarrollo actuales, lo que implica un
importante cambio cultural.
Para facilitar las cosas se trata de implementar sistemas de
desarrollo que provean "paralelización implícita".
Implicit Parallelism
Implicit Parallelism (IP) es un concepto desarrollado durante la
década de los 90s [R1] y es uno de los
Top Concepts sobre los que se
basa M&P.
La idea es que el desarrollador se pueda desentender de los detalles del
procesamiento paralelo para concentrarse en los detalles específicos
de la aplicación. Los detalles del procesamiento paralelo son manejados
automáticamente por el compilador, la arquitectura (framework) y/o el
Runtime.
El IP es el Santo Grial del procesamiento paralelo porque permitiría el
desarrollo de sistemas por personas no especializadas en procesamiento
paralelo al esconder
(abstraer) los mecanismos de threading, buffering, locking y
synchronization indispensables para el procesamiento paralelo.
El IP pretende que el nivel de complejidad de las aplicaciones paralelas
sea comparable al nivel de complejidad de las aplicaciones
secuenciales.
Los mecanismos de threading, buffering, locking y synchronization son
intrínsecos del procesamiento paralelo y en algunas aplicaciones puede
ser necesario que el desarrollador los maneje en forma directa (Explicit
Parallelism). En la practica el esquema ideal es aquel que permite tanto
la paralelización implícita como la paralelización explicita [R2].
En la búsqueda de ese Santo Grial se ha encontrado que los lenguajes de
programación declarativa [R1] y el procesamiento Dataflow [R3][R4]
permiten una aproximación natural a la paralelización implícita.
En M&P se integran la programación declarativa (en lenguaje XAML) y el
procesamiento Dataflow.
En M&P se extiende el runtime de XAML mediante la introducción de "Queued
Properties" de manera de lograr un "Parallel
XAML".
Los BLOCKS de M&P, que son UserControls XAML con algunas interfaces
adicionales requeridas por el MPR (M&P Runtime), tienen capacidad de
procesamiento paralelo y operan en paralelo por default.
Los elementos que conectan los BLOCKS en una aplicación son BLOCKS M&P
especialmente diseñados para implementar el concepto de paralelización
implícita.
Si bien la operación de M&P es paralela por default, el grado de
paralelismo puede ser limitado fácilmente por el desarrollador.
La posibilidad de limitar el paralelismo es importante porque el
paralelismo tiene un costo elevado en términos del Runtime y del Sistema
Operativo. Un paralelismo de "grano fino" puede ser contraproducente ya
que rige la ley de rendimientos decrecientes.
En general en el paralelismo a nivel de BLOCKS implementado en M&P los
beneficios superan a los costos y aumentan al aumentar la cantidad de
procesadores paralelos disponibles.
Paralelismo a nivel de métodos
Como vimos antes M&P introduce el concepto de paralelismo a nivel de
BLOCKS (nivel macro) además del uso de métodos asincrónicos (nivel
micro).
Buscando lograr el mayor grado de paralelismo implícito en el uso
de métodos asincrónicos, en C# 5.0 se introducen dos nuevas
keywords: await y async. Estas keywords permiten
desterrar definitivamente (del código fuente) los callbacks y permiten, como veremos en
un ejemplo muy simple, que el código de la programación asincrónica sea
similar (isomorfo) al de la programación secuencial.
Además de los keywords await y async el esquema tiene un elemento
central: el concepto de Task, proveniente de la Task Parallel
Library (TPL) [R4] [R9] introducida en .NET 4.
Un Task [R6] representa una operación asincrónica. Cada operación
asincrónica tiene asociado un objeto Task<TResult>.
TResult es el tipo de dato retornado por la operación
asincrónica encapsulada en el objeto Task. Si la operación asincrónica no retorna un valor (el método
es "void") se utiliza Task<>.
Un objeto Task no solo representa una operación asincrónica sino que
también permite manipularla (por ejemplo, cancelarla). Pero además, como
veremos, un objeto Task es un objeto "awaitable".
Para facilitar la comprensión de los conceptos anteriores y de su
interoperación, consideremos un ejemplo super simplificado en el que
logramos que dos métodos: A y B operan en paralelo.
En este ejemplo ambos métodos reciben y devuelven números enteros.
Sin paralelismo el código secuencial seria:
public void Test()
{
int y = A(1111);
int x = B(2222);
int z = x + y;
}
int A(int val)
{
return val + 111;
}
int B(int val)
{
return val + 333;
}
La correspondiente versión paralelizada (que analizaremos mas abajo)
es:
public async void TestAsync()
{
var task = AAsync(1111);
task.Start();
int x = B(2222);
int y = await task;
int z = x + y;
}
public Task<int> AAsync(int val)
{
return new Task<int>(() =>
{
return val + 111;
});
}
int B(int val)
{
return val + 333;
}
Lo primero que notamos es que no hay callbacks y que para el
método A hemos creado una versión asincrónica AAsync que retorna
inmediatamente un objeto Task<int>.
Vemos que en el constructor del Task<int> hemos incluido un
delegate que encapsula el código del método A original.
En el encabezamiento de TestAsync hemos incluido el keyword "async" para
indicarle al compilador que este método contiene "awaits" y que deberá
generar internamente el sofisticado código relacionado con la operación
de Tasks (el codigo que genera internamente es
Continuation Passing Style y para cada Task el compilador crea un "state machine" ).
En TestAsync la sentencia
task.Start();
arranca la operación del método AAsnc. Inmediatamente después se
invoca el método sincrónico B. A partir de ese momento ambos métodos están
operando en paralelo.
Antes de poder realizar la operación final
int z = x + y;
una vez que el método sincrónico B haya terminado debemos
esperar ("await") que el método AAsync haya terminado y obtener
su resultado, lo que se logra con la sentencia
int y = await task;
Si previamente al "await" el método AAsync ya había terminado el
await retorna el valor resultado inmediatamente.
Vemos que el paralelismo implícito logrado es muy bueno ya que el código
asincrónico es isomorfo con el código secuencial al haberse eliminado
los callbacks.
Este modelo de programación asincrónica es el utilizado en el nuevo
Framework WinRT de Windows 8 para crear aplicaciones Metro y
es el standard de MS para el futuro.
El ejemplo super simplificado anterior muestra la esencia del esquema
desde el punto de vista del paralelismo implícito, pero la Task Parallel
Library (TPL) es muy amplia y sofisticada ya que cubre todos los
aspectos y escenarios relacionados con el procesamiento paralelo.
La TPL cubre también la operación Dataflow (TDF) [R5]
que a nivel micro complementa al M&P Dataflow. Ver "Parallel
XAML".
FAQs sobre la TPL
Se pueden crear Tasks dentro de un Task ?
Si, TPL permite crear child y nested Tasks dentro de
Tasks [R9].
Cada Task opera en un thread diferente ?
Si, por default los threads utilizados provienen del ThreadPool, que
ha sido potenciado en .NET 4 [R7].
TPL permite también ejecutar Tasks en la UI Thread
y en threads independientes (fuera del ThreadPool) en el caso de
Tasks de larga duración (long-lived Tasks).
En versiones anteriores de .NET el ThreadPool utilizaba,
para encolar sus requerimientos de threads, una única cola FIFO
(first-in, first-out) en cada Application Domain. Esta
única "cola global" compartida constituía el cuello de botella
del ThreadPool.
En .NET Framework 4 el ThreadPool utiliza un conjunto de colas
que permite reducir los tiempos de encolado y desencolado de
requerimientos.
En el conjunto existe una "global queue" y hay una "local queue"
por cada thread del pool [R10].
Las "top level tasks" (que no son creadas dentro de otro Task)
se encolan en el "global queue".
Los child y nested tasks se encolan en el "local queue" del
thread en el cual esta siendo ejecutada su "parent task" .
Se utiliza un algoritmo de "work stealing" (si una cola
queda vacia toma requerimientos de otras colas).
Cual es la funcion de los Task Schedulers ?
Los Task Scheduler [R7] encolan los
Task en Threads y luego ejecutan y coordinan los Tasks.
El "default scheduler" de TPL utiliza threads del
ThreadPool.
El ThreadPool permite optimizar el procesamiento de Task
de corta duración (short-lived Tasks).
TPL balancea automáticamente la carga sobre todas las
CPU (cores) disponibles ?
Si, TPL se asegura que no haya CPUs inactivas mientras
haya Tasks pendientes.
|
Referencias
Ver también
Links sugeridos
|