En informática , futuro , promesa , retraso y diferido se refieren a construcciones utilizadas para sincronizar la ejecución de programas en algunos lenguajes de programación concurrentes . Describen un objeto que actúa como sustituto de un resultado inicialmente 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 de futuro algo similar fue introducido en 1977 en un artículo de Henry Baker y Carl Hewitt . [3]
Los términos futuro , promesa , demora y diferido a menudo se usan indistintamente, aunque a continuación se tratan algunas diferencias de uso entre futuro y promesa . Específicamente, cuando se distingue el uso, un futuro es una vista de marcador de posición de solo lectura de una variable, mientras que una promesa es un contenedor de asignación única escribible que establece el valor del futuro. En particular, se puede definir un futuro sin especificar qué promesa específica fijará su valor, y diferentes promesas posibles pueden fijar el valor de un futuro determinado, aunque esto sólo puede hacerse una vez para un futuro determinado. En otros casos, un futuro y una promesa se crean juntos y se asocian entre sí: el futuro es el valor, la promesa es la función que establece el valor; esencialmente el valor de retorno (futuro) de una función asincrónica (promesa). Fijar el valor de un futuro 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 desacoplar un valor (un futuro) de cómo se calculó (una promesa), permitiendo que el cálculo se hiciera de manera más flexible, en particular al paralelizarlo. Posteriormente, encontró uso en informática distribuida , para reducir la latencia de los viajes de ida y vuelta de las comunicaciones. 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 continuo .
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 ). Obtener el valor de un futuro explícito puede denominarse punzante o forzado . 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 original de Baker y Hewitt describía futuros implícitos, que naturalmente están respaldados por el modelo de actor de computación y lenguajes de programación puramente orientados a objetos como Smalltalk . El artículo de Friedman y Wise describía sólo futuros explícitos, lo que probablemente refleja la dificultad de implementar eficientemente futuros implícitos en hardware en stock. La dificultad es que el hardware estándar no se ocupa de futuros para tipos de datos primitivos como los números enteros. Por ejemplo, una instrucción de adición no sabe cómo manejarla . En lenguajes puros de actor u objeto, este problema se puede resolver enviando el mensaje , que 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 finaliza el cálculo y que no es necesario 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 canalización de promesas , [4] [5] tal como se implementa en los lenguajes E y Joule , que también se llamó call-stream [6] en el lenguaje Argus .
Considere una expresión que implique llamadas a procedimientos remotos convencionales , como por ejemplo:
t3 := ( xa() ).c( yb() )
que podría ampliarse a
t1 := xa(); t2 := yb(); t3 := t1.c(t2);
Cada declaración necesita que se envíe un mensaje y se reciba una respuesta antes de que pueda continuar con la siguiente declaració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 completos de ida y vuelta a la red hasta esa máquina antes de que la tercera instrucción pueda comenzar a ejecutarse. La tercera declaración provocará 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 continúa con las declaraciones posteriores. Los intentos posteriores de resolver el valor de t3
pueden causar un retraso; sin embargo, la canalización puede reducir la cantidad de viajes de ida y vuelta necesarios. Si, como en el ejemplo anterior, x
, y
, t1
y t2
están todos ubicados en la misma máquina remota, una implementación canalizada puede calcular t3
con un viaje de ida y vuelta en lugar de tres. Debido a que los tres mensajes están destinados a objetos que se encuentran en la misma máquina remota, sólo es necesario enviar una solicitud y sólo es necesario recibir una respuesta que contenga el resultado. El envío t1 <- c(t2)
no se bloquearía incluso si t1
y t2
estuvieran en máquinas diferentes entre sí, o hacia x
o y
.
La canalización de promesas debe distinguirse del paso de mensajes asincrónicos en paralelo. En un sistema que admite el paso de mensajes en paralelo pero no la canalización, el mensaje enviado x <- a()
y y <- b()
en el ejemplo anterior podría proceder en paralelo, pero el envío de t1 <- c(t2)
tendría que esperar hasta que ambos t1
y t2
se hubieran recibido, incluso cuando x
, y
, t1
y t2
están en el mismo control remoto. máquina. La ventaja relativa de la latencia de la canalización se vuelve aún mayor en situaciones más complicadas que involucran muchos mensajes.
La canalización de promesas tampoco debe confundirse con el procesamiento de mensajes canalizados en sistemas de actores, donde es posible que un actor especifique y comience a ejecutar un comportamiento para el siguiente 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, lo que permite leer su valor cuando se resuelve, pero no permite resolverlo:
!!
operador se utiliza para obtener una vista de sólo lectura.std::future
proporciona una vista de solo lectura. El valor se establece directamente mediante a std::promise
, o se establece según 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 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 asociados con un hilo específico que calcula el valor del futuro. [9] Este cálculo puede comenzar con entusiasmo cuando se crea el futuro o con pereza cuando se necesita su valor por primera vez. Un futuro perezoso es similar a un golpe seco , en el sentido de un cálculo retrasado.
Alice ML también admite futuros que pueden resolverse mediante cualquier hilo y los llama promesas . [8] Este uso de promesa es diferente de su uso en E como se describe anteriormente. En Alice, una promesa no es una vista de solo lectura y no se admite la canalización de promesas. En cambio, la canalización ocurre naturalmente para los futuros, incluidos los 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 usando una construcción como when
en E, entonces no hay dificultad en retrasar hasta que el futuro se resuelva antes de que el mensaje pueda ser enviado. recibido o se completa 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 es posible intentar acceder inmediata o sincrónicamente a un valor futuro. Luego hay que hacer una elección de diseño:
Como ejemplo de la primera posibilidad, en C++11 , un subproceso que necesita el valor de un futuro puede bloquearse hasta que esté disponible llamando a las funciones miembro wait()
o get()
. También 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 tiempo de espera) puede provocar una invocación sincrónica de la función para calcular el resultado en el hilo en espera.
Los futuros son un caso particular de los " eventos " primitivos de sincronización , que sólo pueden completarse una vez. En general, los eventos se pueden restablecer al estado vacío inicial y, por lo tanto, completarse tantas veces como desee. [11]
Una I-var (como en el lenguaje Id ) es un futuro con semántica de bloqueo como se define anteriormente. Una estructura I 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 . M-vars admite operaciones atómicas para tomar o colocar el valor actual, donde tomar el valor también devuelve M-var a su estado vacío inicial . [12]
Una variable lógica concurrente [ cita requerida ] es similar a un futuro, pero se actualiza mediante unificación , de la misma manera que las variables lógicas en la programación lógica . Por lo tanto, puede vincularse más de una vez a valores unificables, pero no puede devolverse a 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 una semántica de bloqueo como se mencionó anteriormente.
Una variable de restricción concurrente es una generalización de variables lógicas concurrentes para soportar la programación lógica de restricciones : la restricción puede reducirse varias veces, indicando conjuntos más pequeños de valores posibles. Normalmente hay una manera de especificar un procesador que debería ejecutarse siempre que la restricción se reduzca aún más; esto es necesario para soportar la propagación de restricciones .
Los futuros ansiosos de subprocesos específicos se pueden implementar directamente 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 deseable devolver una vista de solo lectura al cliente, de modo que solo el hilo recién creado pueda resolver este futuro.
Para implementar futuros implícitos específicos de subprocesos diferidos (como los proporcionados por 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 transparentes es suficiente, ya que el primer mensaje enviado al reenviador indica que se necesita el valor 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 soporte para reenvío transparente.
La estrategia de evaluación de futuros, que puede denominarse 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 no se determina el momento preciso. 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 realmente se necesita ( evaluación diferida ), y puede suspenderse a la mitad o ejecutarse en una sola ejecución. Una vez asignado el valor de un futuro, no se vuelve a calcular en accesos futuros; Esto es como la memorización utilizada en la llamada por necesidad .
AEl futuro perezoso es un futuro que de manera determinista tiene una semántica de evaluación perezosa: el cálculo del valor del futuro comienza cuando el valor se necesita por primera vez, como en la llamada por necesidad. Los futuros perezosos son útiles 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 del formulario 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 F recién creado (el proxy para el respuesta de evaluación <Expression>
) como valor de retorno al mismo tiempo que se envía <Expression>
un Eval
mensaje con el entorno E y el cliente C. El comportamiento predeterminado de F es el siguiente:
<Expression>
y procede de la siguiente manera:<Expression>
, entonces V se almacena en F ySin embargo, algunos futuros pueden abordar solicitudes de formas 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")
se suspende hasta que el futuro for 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 era bastante similar al de 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 origina en Id y se incluye en Concurrent ML de Reppy , es muy parecida a la variable lógica concurrente.
La técnica de canalización de promesas (usar 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 canalización de promesas en Xanadu tenían el límite de que los valores de promesa 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 tanto, el ejemplo de canalización de promesas dado anteriormente, que utiliza una promesa para el resultado de un envío como argumento para otro, no habría sido expresable directamente en el diseño del 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 canalización de promesas solo estuvo disponible públicamente con el lanzamiento 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 resolutores totalmente de primera clase.
Varios de los primeros lenguajes de actores, incluida la serie Act, [19] [20] admitían tanto el paso de mensajes paralelos como el procesamiento de mensajes canalizados, pero no el canalización de promesas. (Aunque es técnicamente posible implementar la última de estas características en los dos primeros, no hay evidencia de que los idiomas de la Ley 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 transmisión de mensajes de solicitud-respuesta . Varios lenguajes convencionales ahora tienen soporte de lenguaje para futuros y promesas, más notablemente popularizados 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 en los flujos de trabajo asincrónicos de F#, [24] que data 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 de lenguaje directo 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 destinado a ejecutarse de forma nativa [35].await
) [41]Los lenguajes que también respaldan 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 diferida).
Los futuros se pueden implementar fácilmente en canales : un futuro es un canal de un solo elemento y una promesa es un proceso que envía al canal, cumpliendo el futuro. [104] [105] Esto permite que 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 mediante la lectura del canal, y no solo mediante la evaluación.
{{cite journal}}
: Citar diario requiere |journal=
( ayuda ){{cite journal}}
: Citar diario requiere |journal=
( ayuda )