En informáticafuture , las construcciones promisey delayen algunos lenguajes de programación forman la estrategia de evaluación utilizada para la computación paralela . Con su ayuda, se describe un objeto al que se puede acceder para obtener un resultado, cuyo cálculo puede no estar completo en este momento.
El término promesa fue acuñado en 1976 por Daniel Friedman y David Wise [1] y Peter Hibbard lo llamó eventual . [2] Un concepto similar llamado futuro fue propuesto en un artículo de 1977 por Henry Baker y Carl Hewitt. [3]
Los términos futuro , promesa y demora a menudo se usan indistintamente, pero la diferencia entre futuro y promesa se describe a continuación . El futuro suele ser una representación de solo lectura de una variable, mientras que la promesa es un contenedor de asignación única mutable que transmite el valor del futuro . [4] Se puede definir un futuro sin especificar de qué promesa provendrá el valor. Además , se pueden asociar múltiples promesas con un solo futuro , pero solo una promesa puede asignar un valor a un futuro. De lo contrario, el futuro y la promesa se crean juntos y están vinculados entre sí: el futuro es un valor y la promesa es una función que asigna un valor. En la práctica, el futuro es el valor de retorno de una función de promesa asíncrona . El proceso de asignar un valor futuro se denomina resolución , cumplimiento o vinculación .
Algunas fuentes en ruso usan las siguientes traducciones de términos: para futuro - resultados futuros [5] , futuros [6] [7] [8] ; por promesa, una promesa [9] [5] ; por demora — demora.
Cabe señalar que las traducciones incontables (" futuro ") y de dos palabras (" valor futuro ") tienen una aplicabilidad muy limitada (ver discusión ). En particular, el lenguaje Alice ML proporciona futurespropiedades de primera clase, incluida la provisión de módulos de ML de nivel futures de primera clase , y [10] , y todos estos términos resultan intraducibles usando estas variantes. Una posible traducción del término en este caso resulta ser " futuro ", respectivamente, dando un grupo de términos " futuros de primera clase ", " futuros a nivel de módulo ", " estructuras futuras " y " firmas futuras ". Es posible una traducción libre de " perspectiva ", con la gama terminológica correspondiente. future modulesfuture type modules
El uso del futuro puede ser implícito (cualquier referencia al futuro devuelve una referencia al valor) o explícito (el usuario debe llamar a una función para obtener el valor). Un ejemplo es el método get de una clase java.util.concurrent.Futureen el lenguaje Java . Obtener un valor de un futuro explícito se llama picar o forzar . Los futuros explícitos se pueden implementar como una biblioteca, mientras que los futuros implícitos generalmente se implementan como parte del lenguaje.
El artículo de Baker y Hewitt describe futuros implícitos, que naturalmente se admiten en el modelo computacional actor y lenguajes puramente orientados a objetos como Smalltalk . El artículo de Friedman y Wise describe solo futuros explícitos, probablemente debido a la dificultad de implementar futuros implícitos en computadoras convencionales. La dificultad radica en que a nivel de hardware no será posible trabajar con el futuro como un tipo de dato primitivo como los enteros. Por ejemplo, el uso de la instrucción append no podrá procesar 3 + factorial futuro (100000) . En lenguajes puramente de objetos y lenguajes que soportan el modelo actor, este problema se puede resolver enviando el mensaje factorial(100000) +[3] del futuro , en el que se le indicará al futuro que sume 3 y devuelva el resultado. Vale la pena señalar que el enfoque de paso de mensajes funciona sin importar cuánto tiempo tome factorial (100000) para calcular, y no requiere picar ni forzar.
Al utilizar el futuro, los retrasos en los sistemas distribuidos se reducen significativamente . Por ejemplo, utilizando futuros, puede crear una canalización a partir de la promesa [11] [12] , que se implementa en lenguajes como E y Joule , así como en Argus llamado flujo de llamadas .
Considere una expresión usando llamadas a procedimientos remotos tradicionales :
t3 := ( xa() ).c( yb() )que puede revelarse como
t1 := xa(); t2 := yb(); t3 := t1.c(t2);En cada declaración, primero debe enviar un mensaje y recibir una respuesta antes de continuar con la siguiente. Supongamos que x , y , t1 y t2 están en la misma máquina remota. En este caso, para completar la tercera afirmación, primero debe realizar dos transferencias de datos a través de la red. Luego, la tercera declaración realizará otra transferencia de datos a la misma máquina remota.
Esta expresión se puede reescribir usando el futuro
t3 := (x <- a()) <- c(y <- b())y revelado como
t1 := x <- a(); t2 := y <- b(); t3 := t1 <- c(t2);Esto utiliza la sintaxis del lenguaje E, donde x <- a() significa "reenviar asíncronamente el mensaje a() a x ". Las tres variables se vuelven futuras y la ejecución del programa continúa. Posteriormente, al tratar de obtener el valor de t3 , puede haber un retraso; sin embargo, el uso de una tubería puede reducir esto. Si, como en el ejemplo anterior, x , y , t1 y t2 están ubicados en la misma máquina remota, entonces es posible implementar el cálculo de t3 utilizando una canalización y una transferencia de datos a través de la red. Dado que los tres mensajes son para variables ubicadas en la misma máquina remota, solo necesita ejecutar una solicitud y obtener una respuesta para obtener el resultado. Tenga en cuenta que la transferencia t1 <- c(t2) no se bloqueará incluso si t1 y t2 estuvieran en máquinas diferentes entre sí o desde x e y .
El uso de una canalización de una promesa debe distinguirse de pasar un mensaje en paralelo de forma asíncrona. En los sistemas que admiten el paso de mensajes en paralelo pero que no admiten canalizaciones, el envío de los mensajes x <- a() e y <- b() del ejemplo se puede realizar en paralelo, pero el envío de t1 <- c(t2) tendrá que espere hasta que se reciban t1 y t2 , incluso si x , y , t1 y t2 están en la misma máquina remota. La ventaja de la latencia de usar una canalización se vuelve más significativa en situaciones complejas en las que se deben enviar varios mensajes.
Es importante no confundir la canalización de promesas con la canalización de mensajes en los sistemas de actores, donde es posible que un actor especifique y comience a ejecutar el comportamiento del siguiente mensaje antes de que el anterior haya terminado de procesarse.
En algunos lenguajes de programación, como Oz , E y AmbientTalk , es posible obtener una representación inmutable del futuro que le permite obtener su valor después de resolver, pero no le permite resolver:
El soporte para representaciones inmutables es consistente con el principio de privilegio mínimo , ya que solo se puede otorgar acceso a un valor a aquellos objetos que lo necesitan. En los sistemas que admiten canalizaciones, el remitente de un mensaje asíncrono (con un resultado) recibe una promesa inmutable del resultado y el receptor del mensaje es un resolutor.
En algunos lenguajes, como Alice ML , los futuros están vinculados a un hilo específico que evalúa un valor. La evaluación puede comenzar inmediatamente cuando se crea el futuro, o de forma perezosa , es decir, según sea necesario. Un futuro "perezoso" es como un golpe (en términos de evaluación perezosa).
Alice ML también admite futuros, que se pueden resolver mediante cualquier subproceso, y también se denomina promesa allí . [14] Vale la pena señalar que en este contexto, promesa no significa lo mismo que el ejemplo E anterior : la promesa de Alicia no es una representación inmutable, y Alicia no admite canalización de promesas. Pero los oleoductos funcionan naturalmente con futuros (incluidos los vinculados a promesas).
Si se accede a un valor futuro de forma asincrónica, como pasarle un mensaje o esperar usando una construcción whenen E, entonces no es difícil esperar a que se resuelva el futuro antes de recibir el mensaje. Esto es lo único a tener en cuenta en sistemas puramente asíncronos, como los lenguajes con modelo de actor.
Sin embargo, en algunos sistemas es posible acceder al valor futuro de forma inmediata y sincrónica . Esto se puede lograr de las siguientes maneras:
La primera forma, por ejemplo, se implementa en C++11 , donde el subproceso en el que desea obtener el valor futuro puede bloquearse hasta que el miembro funcione wait()o get(). Usando wait_for()o wait_until(), puede especificar explícitamente un tiempo de espera para evitar el bloqueo eterno. Si el futuro se obtiene como resultado de la ejecución std::async, entonces con una espera de bloqueo (sin tiempo de espera) en el subproceso en espera, el resultado de la ejecución de la función se puede recibir sincrónicamente.
Una variable I (en lenguaje Id ) es un futuro con la semántica de bloqueo descrita anteriormente. La estructura I es una estructura de datos que consta de variables I. Una construcción similar utilizada para la sincronización, en la que se puede asignar un valor varias veces, se denomina variable M. Las variables M admiten operaciones atómicas de obtener y escribir el valor de una variable, donde obtener el valor devuelve la variable M a un estado vacío . [17]
La variable booleana paralela es similar al futuro, pero se actualiza durante la unificación de la misma manera que las variables booleanas en la programación lógica . Por lo tanto, se puede asociar con más de un valor uniforme (pero no puede volver a un estado vacío o sin resolver). Las variables de hilo en Oz funcionan como variables booleanas concurrentes con la semántica de bloqueo descrita anteriormente.
La variable paralela restringida es una generalización de las variables booleanas paralelas con soporte para la programación lógica restringida : una restricción puede reducir el conjunto de valores permitidos varias veces. Por lo general, hay una forma de especificar un procesador que se ejecutará en cada estrechamiento; esto es necesario para soportar la propagación de restricciones .
Los futuros específicos de subprocesos altamente calculados se pueden implementar directamente en términos de futuros no específicos de subprocesos mediante la creación de un subproceso para evaluar el valor en el momento en que se crea el futuro. En este caso, es deseable devolver una vista de solo lectura al cliente, para que solo el hilo creado pueda ejecutar el futuro.
La implementación de futuros implícitos perezosos específicos de subprocesos (como en Alice ML) en términos de futuros no específicos de subprocesos requiere un mecanismo para determinar el primer punto de uso de un valor futuro (como la construcción WaitNeeded en Oz [18] ). Si todos los valores son objetos, entonces es suficiente implementar objetos transparentes para reenviar el valor, ya que el primer mensaje al objeto de reenvío indicará que se debe evaluar el valor del futuro.
Los futuros no específicos de subprocesos se pueden implementar a través de futuros específicos de subprocesos, suponiendo que el sistema admita el paso de mensajes. Un subproceso que requiere un valor futuro puede enviar un mensaje al subproceso futuro. Sin embargo, este enfoque introduce una complejidad redundante. En los lenguajes de programación basados en subprocesos, el enfoque más expresivo es probablemente una combinación de futuros no específicos de subprocesos, vistas de solo lectura y la construcción 'WaitNeeded' o la compatibilidad con el reenvío transparente.
La estrategia de evaluación " call by future " no es determinista: el valor del futuro se evaluará en algún momento después de la creación, pero antes de su uso. La evaluación puede comenzar inmediatamente después de la creación del futuro (" evaluación ansiosa "), o solo en el momento en que se necesita el valor ( evaluación perezosa , evaluación diferida). Una vez que se ha evaluado el resultado del futuro, las llamadas posteriores no se recalculan. Por lo tanto, el futuro proporciona tanto la llamada por necesidad como la memorización .
El concepto de futuro perezoso proporciona una semántica de evaluación perezosa determinista: la evaluación del valor futuro comienza la primera vez que se usa el valor, como en el método de "llamada por necesidad". Los futuros perezosos son útiles en lenguajes de programación que no proporcionan una evaluación perezosa. Por ejemplo, en C++11 , se puede crear una construcción similar especificando una política std::launch::syncde lanzamiento std::asyncy pasando una función que evalúa el valor.
En el modelo Actor, una expresión del formulario ''future'' <Expression>se define como una respuesta a un mensaje Eval en el entorno E para el consumidor C , de la siguiente manera: una expresión futura responde a un mensaje Eval enviando al consumidor C el actor F recién creado (un proxy para la respuesta con evaluación <Expression>) como valor de retorno, al mismo tiempo que se envían los <Expression>mensajes de expresión Eval en el entorno E para el consumidor C . El comportamiento de F se define así:
Algunas implementaciones del futuro pueden manejar las solicitudes de manera diferente para aumentar el grado de paralelismo. Por ejemplo, la expresión 1 + futuro factorial(n) puede crear un nuevo futuro que se comporte como el número 1+factorial(n) .
Las construcciones de futuro y promesa se implementaron por primera vez en los lenguajes de programación MultiLisp y Act 1 . El uso de variables booleanas para la interacción en lenguajes de programación de lógica concurrente es bastante similar al futuro. Entre ellos se encuentran Prolog con Freeze e IC Prolog , un primitivo competitivo completo ha sido implementado por Relational Language , Concurrent Prolog , Guarded Horn Clauses (GHC), Parlog , Strand , Vulcan , Janus , Mozart / Oz , Flow Java y Alice ML . Las asignaciones de I-var únicas de los lenguajes de programación de flujo de datos , introducidas originalmente en Id e incluidas en Reppy Concurrent ML , son similares a las variables booleanas concurrentes.
Barbara Liskov y Liuba Shrira en 1988 [19] propusieron una técnica de canalización prometedora que utiliza futuros para superar los retrasos , y de forma independiente Mark S. Miller , Dean Tribble y Rob Jellinghaus como parte del Proyecto Xanadu alrededor de 1989 [20] .
El término promesa fue acuñado por Liskov y Shrira, aunque llamaron flujo de llamada al mecanismo de tubería (ahora rara vez se usa).
En ambos trabajos, y en la implementación del pipeline de promesas de Xanadu, las promesas no eran objetos de primera clase : los argumentos de función y los valores devueltos no podían ser promesas directamente (lo que complica la implementación del pipeline, por ejemplo en Xanadu). la promesa y el flujo de llamadas no se implementaron en las versiones públicas de Argus [21] (el lenguaje de programación utilizado en el trabajo de Liskov y Shrira); Argus dejó de desarrollarse en 1988. [22] La implementación de la tubería en Xanadu solo estuvo disponible con el lanzamiento de Udanax Gold [23] en 1999 y no se explica en la documentación publicada. [24]
Las implementaciones de Promise en Joule y E los admiten como objetos de primera clase.
Varios de los primeros lenguajes Actor, incluidos los lenguajes Act, [25] [26] admitían el paso de mensajes en paralelo y la canalización de mensajes, pero no la canalización de promesas. (A pesar de la posibilidad de implementar la tubería de promesa a través de construcciones compatibles, no hay evidencia de tales implementaciones en los lenguajes Act).
El concepto de futuro se puede implementar en términos de canales : un futuro es un canal único y una promesa es un proceso que envía un valor a un canal mediante la ejecución del futuro [27] . Así es como se implementan los futuros en lenguajes habilitados para canales simultáneos como CSP y Go . Los futuros que implementan son explícitos porque se accede a ellos leyendo desde un canal, no mediante una evaluación de expresión normal.