En informática , future , promise , delay y deferred hacen referencia a construcciones utilizadas para sincronizar la ejecución de programas en algunos lenguajes de programación concurrente . Describen un objeto que actúa como proxy de un resultado que inicialmente es desconocido, generalmente porque el cálculo de su valor aún no está completo.
El término promesa fue propuesto en 1976 por Daniel P. Friedman y David Wise, [1] y Peter Hibbard lo llamó eventual . [2] Un concepto algo similar, futuro, fue introducido en 1977 en un artículo de Henry Baker y Carl Hewitt . [3]
Los términos future , promise , delay y deferred se usan a menudo indistintamente, aunque a continuación se tratan algunas diferencias de uso entre future y promise . Específicamente, cuando se distingue el uso, un future es una vista de marcador de posición de solo lectura de una variable, mientras que promise es un contenedor de asignación única escribible que establece el valor del future. En particular, un future se puede definir sin especificar qué promesa específica establecerá su valor, y diferentes promesas posibles pueden establecer el valor de un future dado, aunque esto solo se puede hacer una vez para un future dado. En otros casos, un future y una promise se crean juntos y se asocian entre sí: el future es el valor, la promise es la función que establece el valor, esencialmente el valor de retorno (futuro) de una función asincrónica (promesa). Establecer el valor de un future también se llama resolverlo , cumplirlo o vincularlo .
Los futuros y las promesas se originaron en la programación funcional y paradigmas relacionados (como la programación lógica ) para disociar un valor (un futuro) de cómo se calculó (una promesa), lo que permite que el cálculo se realice de manera más flexible, en particular al paralelizarlo. Más tarde, encontró uso en la computación distribuida , para reducir la latencia de los viajes de ida y vuelta de la comunicación. Más tarde aún, ganó más uso al permitir escribir programas asincrónicos en estilo directo , en lugar de en estilo de paso de continuación .
El uso de futuros puede ser implícito (cualquier uso del futuro obtiene automáticamente su valor, como si fuera una referencia ordinaria ) o explícito (el usuario debe llamar a una función para obtener el valor, como el get
método de java.util.concurrent.Future
en Java ). La obtención del valor de un futuro explícito puede denominarse stinging o forcing . Los futuros explícitos pueden implementarse como una biblioteca, mientras que los futuros implícitos suelen implementarse como parte del lenguaje.
El artículo original de Baker y Hewitt describió futuros implícitos, que son compatibles de forma natural con el modelo de actor de computación y los lenguajes de programación orientados a objetos puros como Smalltalk . El artículo de Friedman y Wise describió solo futuros explícitos, probablemente reflejando la dificultad de implementar de manera eficiente futuros implícitos en hardware estándar. La dificultad es que el hardware estándar no maneja futuros para tipos de datos primitivos como números enteros. Por ejemplo, una instrucción add no sabe cómo manejar . En lenguajes de actor u objeto puros, este problema se puede resolver enviando el mensaje , que le pide al futuro que se agregue a sí mismo y devuelva el resultado. Tenga en cuenta que el enfoque de paso de mensajes funciona independientemente de cuándo finalice el cálculo y que no se necesita picar ni forzar.3 + future factorial(100000)
future factorial(100000)
+[3]
3
factorial(100000)
El uso de futuros puede reducir drásticamente la latencia en sistemas distribuidos . Por ejemplo, los futuros permiten la segmentación de promesas , [4] [5] tal como se implementa en los lenguajes E y Joule , que también se denominaba flujo de llamadas [6] en el lenguaje Argus .
Considere una expresión que involucre llamadas a procedimientos remotos convencionales , como:
t3 := (xa()).c(yb())
que podría ampliarse a
t1 := xa(); t2 := yb(); t3 := t1.c(t2);
Cada instrucción necesita que se envíe un mensaje y se reciba una respuesta antes de que pueda continuar la siguiente instrucción. Supongamos, por ejemplo, que x
, y
, t1
y t2
están todos ubicados en la misma máquina remota. En este caso, deben realizarse dos viajes de ida y vuelta completos de la red a esa máquina antes de que pueda comenzar a ejecutarse la tercera instrucción. La tercera instrucción provocará entonces otro viaje de ida y vuelta a la misma máquina remota.
Usando futuros, la expresión anterior podría escribirse
t3 := (x <- a()) <- c(y <- b())
que podría ampliarse a
t1 := x <- a(); t2 := y <- b(); t3 := t1 <- c(t2);
La sintaxis utilizada aquí es la del lenguaje E, donde x <- a()
significa enviar el mensaje a()
de forma asincrónica a x
. A las tres variables se les asignan inmediatamente futuros para sus resultados, y la ejecución procede a las instrucciones subsiguientes. Los intentos posteriores de resolver el valor de t3
pueden causar una demora; sin embargo, la canalización puede reducir la cantidad de viajes de ida y vuelta necesarios. Si, como en el ejemplo anterior, x
, , y y
están todos ubicados en la misma máquina remota, una implementación canalizada puede realizar el cálculo con un viaje de ida y vuelta en lugar de tres. Debido a que los tres mensajes están destinados a objetos que están en la misma máquina remota, solo se necesita enviar una solicitud y solo se necesita recibir una respuesta que contenga el resultado. El envío no se bloquearía incluso si y estuvieran en máquinas diferentes entre sí, o para o .t1
t2
t3
t1 <- c(t2)
t1
t2
x
y
La canalización de promesas debe distinguirse del paso de mensajes asíncrono en paralelo. En un sistema que admita el paso de mensajes en paralelo pero no la canalización, el mensaje se envía x <- a()
y y <- b()
en el ejemplo anterior podría realizarse en paralelo, pero el envío de t1 <- c(t2)
tendría que esperar hasta que se hubieran recibido tanto como , incluso cuando t1
, , y están en la misma máquina remota. La ventaja de latencia relativa de la canalización se vuelve aún mayor en situaciones más complicadas que involucran muchos mensajes.t2
x
y
t1
t2
La canalización de promesas tampoco debe confundirse con el procesamiento de mensajes canalizado en sistemas de actores, donde es posible que un actor especifique y comience a ejecutar un comportamiento para el próximo mensaje antes de haber completado el procesamiento del mensaje actual.
En algunos lenguajes de programación como Oz , E y AmbientTalk , es posible obtener una vista de solo lectura de un futuro, que permite leer su valor cuando se resuelve, pero no permite resolverlo:
!!
operador se utiliza para obtener una vista de solo lectura.std::future
proporciona una vista de solo lectura. El valor se establece directamente mediante a std::promise
o se establece en el resultado de una llamada de función mediante std::packaged_task
o std::async
.System.Threading.Tasks.Task<T>
representa una vista de solo lectura. La resolución del valor se puede realizar mediante System.Threading.Tasks.TaskCompletionSource<T>
.La compatibilidad con vistas de solo lectura es coherente con el principio de privilegio mínimo , ya que permite restringir la capacidad de establecer el valor a los sujetos que necesitan establecerlo. En un sistema que también admite la canalización, el remitente de un mensaje asincrónico (con resultado) recibe la promesa de solo lectura para el resultado y el destino del mensaje recibe el solucionador.
Algunos lenguajes, como Alice ML , definen futuros que están asociados con un hilo específico que calcula el valor del futuro. [9] Este cálculo puede comenzar de manera ansiosa cuando se crea el futuro o de manera diferida cuando se necesita su valor por primera vez. Un futuro diferido es similar a un thunk , en el sentido de un cálculo retrasado.
Alice ML también admite futuros que pueden ser resueltos por cualquier hilo y los llama promesas . [8] Este uso de promesa es diferente de su uso en E como se describió anteriormente. En Alice, una promesa no es una vista de solo lectura y la segmentación de promesas no es compatible. En cambio, la segmentación ocurre naturalmente para futuros, incluidos aquellos asociados con promesas.
Si se accede al valor de un futuro de forma asincrónica, por ejemplo, enviándole un mensaje o esperándolo explícitamente mediante una construcción como when
en E, no hay ninguna dificultad en esperar hasta que se resuelva el futuro antes de poder recibir el mensaje o de que se complete la espera. Este es el único caso que se debe considerar en sistemas puramente asincrónicos, como los lenguajes de actores puros.
Sin embargo, en algunos sistemas también puede ser posible intentar acceder de forma inmediata o sincrónica al valor de un futuro. En ese caso, hay que tomar una decisión de diseño:
Como ejemplo de la primera posibilidad, en C++11 , un hilo que necesita el valor de un futuro puede bloquearse hasta que esté disponible llamando a las funciones miembro wait()
o get()
. También se puede especificar un tiempo de espera en la espera utilizando las funciones miembro wait_for()
o wait_until()
para evitar un bloqueo indefinido. Si el futuro surgió de una llamada a , std::async
entonces una espera de bloqueo (sin un tiempo de espera) puede provocar la invocación sincrónica de la función para calcular el resultado en el hilo que espera.
Los futuros son un caso particular de la primitiva de sincronización " eventos ", que pueden completarse sólo una vez. En general, los eventos pueden restablecerse al estado inicial vacío y, por lo tanto, completarse tantas veces como se desee. [11]
Una I-var (como en el lenguaje Id ) es un futuro con semántica de bloqueo como la definida anteriormente. Una I-estructura es una estructura de datos que contiene I-vars. Una construcción de sincronización relacionada que se puede configurar varias veces con diferentes valores se denomina M-var . Las M-vars admiten operaciones atómicas para tomar o colocar el valor actual, donde tomar el valor también hace que la M-var vuelva a su estado vacío inicial. [12]
Una variable lógica concurrente [ cita requerida ] es similar a un futuro, pero se actualiza por unificación , de la misma manera que las variables lógicas en la programación lógica . Por lo tanto, se puede vincular más de una vez a valores unificables, pero no se puede volver a establecer en un estado vacío o sin resolver. Las variables de flujo de datos de Oz actúan como variables lógicas concurrentes y también tienen semántica de bloqueo como se mencionó anteriormente.
Una variable de restricción concurrente es una generalización de las variables lógicas concurrentes para respaldar la programación lógica de restricciones : la restricción se puede restringir varias veces, lo que indica conjuntos más pequeños de valores posibles. Por lo general, existe una forma de especificar un procesador que se debe ejecutar siempre que la restricción se reduzca aún más; esto es necesario para respaldar la propagación de restricciones .
Los futuros específicos de subprocesos se pueden implementar de manera directa en futuros no específicos de subprocesos, creando un subproceso para calcular el valor al mismo tiempo que se crea el futuro. En este caso, es conveniente devolver una vista de solo lectura al cliente, de modo que solo el subproceso recién creado pueda resolver este futuro.
Para implementar futuros específicos de subprocesos implícitos perezosos (como los que proporciona Alice ML, por ejemplo) en términos de futuros no específicos de subprocesos, se necesita un mecanismo para determinar cuándo se necesita por primera vez el valor del futuro (por ejemplo, la WaitNeeded
construcción en Oz [13] ). Si todos los valores son objetos, entonces la capacidad de implementar objetos de reenvío transparente es suficiente, ya que el primer mensaje enviado al reenvío indica que se necesita el valor del futuro.
Los futuros no específicos de subprocesos se pueden implementar en futuros específicos de subprocesos, suponiendo que el sistema admita el paso de mensajes, haciendo que el subproceso de resolución envíe un mensaje al propio subproceso del futuro. Sin embargo, esto puede verse como una complejidad innecesaria. En los lenguajes de programación basados en subprocesos, el enfoque más expresivo parece ser proporcionar una combinación de futuros no específicos de subprocesos, vistas de solo lectura y una construcción WaitNeeded o compatibilidad con el reenvío transparente.
La estrategia de evaluación de futuros, que puede denominarse call by future (llamada por futuro) , no es determinista: el valor de un futuro se evaluará en algún momento entre el momento en que se crea el futuro y el momento en que se utiliza su valor, pero el momento preciso no se determina de antemano y puede cambiar de una ejecución a otra. El cálculo puede comenzar tan pronto como se crea el futuro ( evaluación ansiosa ) o solo cuando el valor es realmente necesario ( evaluación perezosa ), y puede suspenderse a mitad de camino o ejecutarse en una sola ejecución. Una vez que se asigna el valor de un futuro, no se vuelve a calcular en los futuros accesos; esto es como la memorización utilizada en call by need (llamada por necesidad ).
AUn futuro perezoso es un futuro que tiene una semántica de evaluación perezosa de manera determinista: el cálculo del valor del futuro comienza cuando el valor se necesita por primera vez, como en una llamada por necesidad. Los futuros perezosos se utilizan en lenguajes cuya estrategia de evaluación no es perezosa por defecto. Por ejemplo, enC++11,estos futuros perezosos se pueden crear pasando lastd::launch::deferred
política de lanzamiento astd::async
, junto con la función para calcular el valor.
En el modelo de actor, una expresión de la forma future <Expression>
se define por cómo responde a un Eval
mensaje con el entorno E y el cliente C de la siguiente manera: La expresión futura responde al Eval
mensaje enviando al cliente C un actor recién creado F (el proxy para la respuesta de evaluación <Expression>
) como valor de retorno simultáneamente con el envío <Expression>
de un Eval
mensaje con el entorno E y el cliente C . El comportamiento predeterminado de F es el siguiente:
<Expression>
procediendo de la siguiente manera:<Expression>
, entonces V se almacena en F ySin embargo, algunos futuros pueden tratar las solicitudes de maneras especiales para proporcionar un mayor paralelismo. Por ejemplo, la expresión 1 + future factorial(n)
puede crear un nuevo futuro que se comportará como el número 1+factorial(n)
. Este truco no siempre funciona. Por ejemplo, la siguiente expresión condicional:
if m>future factorial(n) then print("bigger") else print("smaller")
suspende hasta que el futuro factorial(n)
haya respondido a la solicitud preguntando si m
es mayor que él mismo.
Las construcciones de futuro y/o promesa se implementaron por primera vez en lenguajes de programación como MultiLisp y Act 1. El uso de variables lógicas para la comunicación en lenguajes de programación lógica concurrente fue bastante similar a los futuros. Estos comenzaron en Prolog con Freeze e IC Prolog , y se convirtieron en una verdadera primitiva de concurrencia con Relational Language, Concurrent Prolog , cláusulas Horn protegidas (GHC), Parlog , Strand , Vulcan , Janus , Oz-Mozart , Flow Java y Alice ML . La I-var de asignación única de los lenguajes de programación de flujo de datos , que se originó en Id e incluyó en Concurrent ML de Reppy , es muy similar a la variable lógica concurrente.
La técnica de canalización de promesas (utilizando futuros para superar la latencia) fue inventada por Barbara Liskov y Liuba Shrira en 1988, [6] e independientemente por Mark S. Miller , Dean Tribble y Rob Jellinghaus en el contexto del Proyecto Xanadu alrededor de 1989. [14]
El término promesa fue acuñado por Liskov y Shrira, aunque se referían al mecanismo de canalización con el nombre call-stream , que ahora rara vez se utiliza.
Tanto el diseño descrito en el artículo de Liskov y Shrira, como la implementación de la segmentación de promesas en Xanadu, tenían el límite de que los valores de las promesas no eran de primera clase : un argumento o el valor devuelto por una llamada o envío no podía ser directamente una promesa (por lo que el ejemplo de segmentación de promesas dado anteriormente, que utiliza una promesa para el resultado de un envío como argumento para otro, no habría sido directamente expresable en el diseño de flujo de llamadas o en la implementación de Xanadu). Parece que las promesas y los flujos de llamadas nunca se implementaron en ninguna versión pública de Argus, [15] el lenguaje de programación utilizado en el artículo de Liskov y Shrira. El desarrollo de Argus se detuvo alrededor de 1988. [16] La implementación de Xanadu de la segmentación de promesas solo se hizo pública con la publicación del código fuente de Udanax Gold [17] en 1999, y nunca se explicó en ningún documento publicado. [18] Las implementaciones posteriores en Joule y E admiten promesas y solucionadores de primera clase por completo.
Varios de los primeros lenguajes de actores, incluida la serie Act, [19] [20] admitían tanto el paso de mensajes en paralelo como el procesamiento de mensajes segmentados, pero no la segmentación de mensajes. (Aunque técnicamente es posible implementar la última de estas características en los dos primeros, no hay evidencia de que los lenguajes Act lo hicieran).
Después de 2000, se produjo un importante resurgimiento del interés en futuros y promesas, debido a su uso en la capacidad de respuesta de las interfaces de usuario, y en el desarrollo web , debido al modelo de solicitud-respuesta de paso de mensajes. Varios lenguajes principales ahora tienen soporte de lenguaje para futuros y promesas, más notablemente popularizado por FutureTask
en Java 5 (anunciado en 2004) [21] y las construcciones async/await en .NET 4.5 (anunciado en 2010, lanzado en 2012) [22] [23] en gran medida inspirado por los flujos de trabajo asincrónicos de F#, [24] que datan de 2007. [25] Esto ha sido adoptado posteriormente por otros lenguajes, en particular Dart (2014), [26] Python (2015), [27] Hack (HHVM) y borradores de ECMAScript 7 (JavaScript), Scala y C++ (2011).
Algunos lenguajes de programación admiten futuros, promesas, variables lógicas concurrentes, variables de flujo de datos o I-vars, ya sea mediante soporte directo del lenguaje o en la biblioteca estándar.
java.util.concurrent.Future
ojava.util.concurrent.CompletableFuture
async
y await
desde ECMAScript 2017 [33]async
y await
[23]kotlin.native.concurrent.Future
generalmente solo se usa cuando se escribe Kotlin que está destinado a ejecutarse de forma nativa [35]..await
) [41]Los idiomas que también admiten la canalización de promesas incluyen:
async
/sin bloqueo await
[95]Los futuros se pueden implementar en corrutinas [27] o generadores [103] , lo que da como resultado la misma estrategia de evaluación (por ejemplo, multitarea cooperativa o evaluación perezosa).
Los futuros se pueden implementar fácilmente en canales : un futuro es un canal de un elemento y una promesa es un proceso que envía al canal, cumpliendo el futuro. [104] [105] Esto permite que los futuros se implementen en lenguajes de programación concurrentes con soporte para canales, como CSP y Go . Los futuros resultantes son explícitos, ya que se debe acceder a ellos leyendo desde el canal, en lugar de solo evaluarlos.