|
El concepto de Continuation Passing Style (CPS) fué
explicitado en 1975 en relación con el lenguaje Scheme [R1], pero
los mecanismos para su
implementación estaban presentes en el Lenguaje Lisp (del cual
Scheme se deriva) desde 1956.
Los mismos mecanismos están presentes en BScript, el lenguaje
de scripting de M&P, que también es derivado de Lisp [^].
La importancia que el concepto tiene actualmente está relacionada con la
programación asincrónica, en la que el CPS es el mecanismo
central [R9].
En .NET la implementación del concepto se basa en los "delegates"
y/o en elementos relacionados como las "lambda expressions" (lambda
passing).
En esta sección suponemos que el lector tiene conocimientos básicos
de CPS (existen tutorials en la Web) y nos enfocaremos en los aspectos
relacionados con el procesamiento paralelo y la operación
Dataflow.
Avance sin retorno
Un concepto central en la operación Dataflow es el de "avance sin
retorno": La información fluye unidireccionalmente por la red de
componentes que conforma el "dataflow network".
Un "dataflow pipeline" es una forma simple de "dataflow network" en la
que los componentes forman parte de una cadena lineal.
Métodos sin retorno
Imaginemos que en el contexto .NET no podamos usar métodos que
retornen valores (sólo podemos usar métodos "void"). Es posible
desarrollar aplicaciones con esta restricción? Si, implementando
un CPS basado en delegates.
Consideremos un ejemplo elemental para ilustrar el concepto. Vamos a
transformar al CPS un método "AddOne" que recibe y devuelve
números enteros:
int AddOne(int x)
{
return x + 1;
}
que utilizamos en forma imperativa (control-flow) como en el
siguiente ejemplo supersimplificado:
int y = AddOne(45);
int z = y + 24;
Transformamos el método AddOne agregando un argumento "continuation"
cuyo tipo es un delegate Action<int>
void AddOne(int x, Action<int> continuation)
{
// continuation(x + 1); /<<< Sync
continuation.BeginInvoke(x + 1, null, null); /<<< Async
}
y creamos el método Rest (que encapsula el resto de las sentencias) que satisface el delegate Action<int>
void Rest(int y)
{
int z = y + 24;
}
y para la invocación usamos ahora:
AddOne(45, Rest);
El resultado es equivalente a pesar de las diferencias notacionales.
Sin embargo, como veremos luego, la transformación nos a llevado del
dominio "control-flow" al dominio "dataflow".
Transformación de Dominio
Como ha sido la transformación? Hemos realizado tres cambios:
1. Cambiamos el "return type" a "void".
2. Agregamos un argumento adicional que es un delegate (en este ejemplo
de tipo Action<T> donde T es el tipo retornado originalmente por el
método).
3. Reemplazamos todas las sentencias "return" por invocaciones al
delegate "continuation" (recibido como argumento) con la expresión
retornada en la sentencia "return" original.
Pérdidas y Ganancias
La transformación, desde la
perspectiva de una mente acostumbrada al procesamiento imperativo
control-flow, ha aumentado la cantidad de código, ha complicado la comprensión del código (su flujo de
control) y el debugging, y ha aumentado la
probabilidad de cometer errores.
En contrapartida la transformación permite ingresar rápidamente al mundo
de la programación asincrónica, en la que, como mencionamos antes, el
CPS es el mecanismo central. Para una mente acostumbrada al
procesamiento asincrónico esta es la manera natural de programar.
| Creemos que la adaptación mental se facilita cuando se comprende el
"shift" del dominio control-flow al dominio dataflow, concepto que no
está explicitado en la mayoría de los tutorials. |
Por otra parte es importante hacer notar que C#, y también VB, han
sido, hasta hace poco tiempo, lenguajes "control-flow oriented"
escondiendo gran parte de los mecanismos de soporte intrínsecos
del control-flow tales como el "call stack" y los innumerables "goto".
En la actualidad ambos lenguajes estan evolucionando incorporando nuevas
facilidades para el procesamiento asincronico mediante los keywords
await y async [R9].
FAQ
La transformación es aplicable a todos los métodos ?
Si, todos los métodos, inclusive los recursivos, pueden ser
transformados. Algunos compiladores hacen la transformación internamente
para facilitar el análisis orientado a la optimización del código
generado.
Si los métodos no retornan que pasa con el "call stack" ?
En una implementación CPS pura el call stack no es necesario. El CPS
elimina (o minimiza) el problema del "stack overflow".
Un método puede aceptar varios "continuation" (continuaciones
alternativas) ?
Si, adecuando los correspondiente delegates. Las continuaciones
alternativas originan datataflow networks. Una aplicación muy
interesante de continuaciones alternativas tiene que ver con el
Exception Handling usando "error continuations". En síntesis, las
continuaciones alternativas permites el control del flujo mediante
delegates.
Una continuación se asemeja a un GOTO (que se trata de evitar en la
programación control-flow) ?.
Si, y además permiten "non-local gotos". Pero debemos
recordar que aunque el desarrollador no utilice "gotos" estos están
presentes en el código generado por el compilador.
Si fuere necesario, un método puede abortar completamente una
operación terminando sin invocar ninguna continuación ?
Si.
En .NET es posible usar también métodos que retornan valores en un
esquema CPS ?
Si, se pueden mezclar libremente los dos tipos de métodos.
Si los compiladores hacen la transformación automáticamente no sería
posible avanzar por este camino hacia la "paralelización implícita" ?
Si, pero en general no es conveniente paralelizar todos los métodos
porque, como mencionamos en Implicit Parallelism,
el paralelismo tiene un costo elevado en términos del Runtime y del Sistema
Operativo y un paralelismo de "grano fino" puede ser contraproducente.
En las nuevas versiones de C# y VB se utilizan las keywords
async y await (mencionadas previamente) para
indicar al compilador las partes que debe paralelizar
(tratando de lograr el mayor grado de implicit parallelism)
[R3].
El lenguaje Java no soporta pointers (referencias) a
métodos. Es posible implementar la programación CPS ?
No.
Ejemplo - Method Dataflow Pipeline
Para fijar el concepto de Dataflow entre métodos, consideremos tres
métodos (A, B y C) conectados formando un "dataflow pipeline"
Niveles macro y micro
Este pipeline es análogo al mostrado en Parallel
XAML. La diferencia está en que el pipeline en Parallel
XAML está conformado por BLOCKS M&P mientras que el pipeline que
describimos en esta sección está conformado por métodos.
El código de los métodos se muestra en el siguiente fragmento:
namespace cps
{
delegate void DEL(int val, object linkto);
public class CPSBLOCK : BLOCK
{
public CPSBLOCK()
{
}
private int _PropI = -1; // <<<<<<<<<<<
public int PropI
{
set
{
TestPipeline(value);
}
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void TestPipeline(int val)
{
DEL ADEL = new DEL(A);
// ADEL(val, new DEL(B)); //<<< Sync
ADEL.BeginInvoke(val, new DEL(B), null, null); //<<< Async
}
void A(int val, object linkto)
{
DEL BDEL = (DEL)linkto;
// BDEL(val + 45, new DEL(C)); //<<< Sync
BDEL.BeginInvoke(val + 45, new DEL(C), null, null); //<<< Async
}
void B(int val, object linkto)
{
DEL CDEL = (DEL)linkto;
// CDEL(val + 55, null); //<<< Sync
CDEL.BeginInvoke(val + 55, null, null, null); //<<< Async
}
void C(int val, object linkto)
{
OutS = "@" + val;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Output Property del BLOCK
private String _OutS = "";
public String OutS
{
get
{
return _OutS;
}
set
{
if (_OutS == value)
return;
_OutS = value;
OnPropertyChanged("OutS");
}
}
}
}
Referencias
Ver también
Links sugeridos
|