В информатике будущее , обещание , задержка и отложенное относятся к конструкциям, используемым для синхронизации выполнения программы в некоторых параллельных языках программирования . Они описывают объект, который действует как прокси для результата, который изначально неизвестен, обычно потому, что вычисление его значения еще не завершено.
Термин «обещание» был предложен в 1976 году Дэниелом П. Фридманом и Дэвидом Уайзом [1] , а Питер Хиббард назвал его возможным . [2] Несколько похожая концепция будущего была представлена в 1977 году в статье Генри Бейкера и Карла Хьюитта . [3]
Термины будущее , обещание , задержка и отсрочка часто используются как взаимозаменяемые, хотя некоторые различия в использовании между будущим и обещанием рассматриваются ниже. В частности, если различать использование, будущее представляет собой представление переменной, доступное только для чтения , а обещание — это записываемый контейнер с одним назначением , который устанавливает значение будущего. Примечательно, что будущее может быть определено без указания того, какое конкретное обещание будет устанавливать его значение, а различные возможные обещания могут устанавливать значение данного будущего, хотя это можно сделать только один раз для данного будущего. В других случаях будущее и обещание создаются вместе и связываются друг с другом: будущее — это значение, обещание — это функция, которая устанавливает значение — по сути, возвращаемое значение (будущее) асинхронной функции (промиса). Установка значения будущего также называется его разрешением , выполнением или связыванием .
Фьючерсы и обещания возникли в функциональном программировании и связанных с ним парадигмах (таких как логическое программирование ) для отделения значения (будущего) от того, как оно было вычислено (обещание), что позволяет выполнять вычисления более гибко, в частности, путем их распараллеливания. Позже он нашел применение в распределенных вычислениях , для уменьшения задержки при передаче туда и обратно. Еще позже он получил большее распространение, позволив писать асинхронные программы в прямом стиле , а не в стиле передачи продолжения .
Использование фьючерсов может быть неявным (любое использование фьючерса автоматически получает его значение, как если бы это была обычная ссылка ) или явным (пользователь должен вызвать функцию для получения значения, например метод get
в java.util.concurrent.Future
Java ) . Получение ценности явного будущего можно назвать уколом или принуждением . Явные фьючерсы могут быть реализованы в виде библиотеки, тогда как неявные фьючерсы обычно реализуются как часть языка.
В оригинальной статье Бейкера и Хьюитта описывались неявные фьючерсы, которые естественным образом поддерживаются в актерской модели вычислений и в чисто объектно-ориентированных языках программирования, таких как Smalltalk . В статье Фридмана и Уайза описаны только явные фьючерсы, что, вероятно, отражает сложность эффективной реализации неявных фьючерсов на стандартном оборудовании. Трудность заключается в том, что стандартное оборудование не поддерживает фьючерсы для примитивных типов данных, таких как целые числа. Например, инструкция добавления не знает, как обращаться с . В чистых актерских или объектных языках эту проблему можно решить, отправив сообщение , которое просит будущее добавить к себе и вернуть результат. Обратите внимание, что подход с передачей сообщений работает независимо от того, когда завершаются вычисления, и что никаких дополнительных/принудительных действий не требуется.3 + future factorial(100000)
future factorial(100000)
+[3]
3
factorial(100000)
Использование фьючерсов может значительно снизить задержку в распределенных системах . Например, фьючерсы позволяют использовать конвейерную обработку обещаний , [4] [5] , реализованную в языках E и Joule , которая также называлась потоком вызовов [6] на языке Argus .
Рассмотрим выражение, включающее обычные вызовы удаленных процедур , например:
t3 := ( xa() ).c( yb() )
который можно было бы расширить до
т1 := ха(); т2 := yb(); т3 := t1.c(t2);
Перед выполнением следующего запроса каждому оператору необходимо отправить сообщение и получить ответ. Предположим, например, что x
, y
, t1
и t2
все расположены на одной и той же удаленной машине. В этом случае должны произойти два полных сетевых обращения к этой машине, прежде чем третий оператор сможет начать выполняться. Третий оператор затем вызовет еще один возврат к той же удаленной машине.
Используя фьючерсы, приведенное выше выражение можно записать
t3 := (x <- a()) <- c(y <- b())
который можно было бы расширить до
t1 := x <- a(); t2 := y <- b(); t3 := t1 <- c(t2);
Используемый здесь синтаксис соответствует синтаксису языка E, где означает асинхронную x <- a()
отправку сообщения в . Всем трем переменным немедленно присваиваются фьючерсы на их результаты, и выполнение переходит к последующим операторам. Более поздние попытки определить значение могут вызвать задержку; однако конвейеризация может сократить количество необходимых обращений туда и обратно. Если, как в предыдущем примере, , , и все расположены на одном и том же удаленном компьютере, конвейерная реализация может выполнять вычисления за один проход туда и обратно вместо трех. Поскольку все три сообщения предназначены для объектов, находящихся на одном и том же удаленном компьютере, необходимо отправить только один запрос и получить только один ответ, содержащий результат. Отправка не блокировалась бы, даже если бы и находились на разных машинах друг к другу, или к или к .a()
x
t3
x
y
t1
t2
t3
t1 <- c(t2)
t1
t2
x
y
Конвейерную обработку обещаний следует отличать от параллельной асинхронной передачи сообщений. В системе, поддерживающей параллельную передачу сообщений, но не конвейерную обработку, сообщение отправляется x <- a()
и y <- b()
в приведенном выше примере может происходить параллельно, но отправка t1 <- c(t2)
должна будет ждать, пока оба t1
и t2
не будут получены, даже если x
, y
, t1
, и t2
находятся на одном и том же удаленном компьютере. машина. Преимущество конвейерной обработки в относительной задержке становится еще больше в более сложных ситуациях, включающих большое количество сообщений.
Конвейерную обработку обещаний также не следует путать с конвейерной обработкой сообщений в системах актеров, где актер может указать и начать выполнение поведения для следующего сообщения до завершения обработки текущего сообщения.
В некоторых языках программирования, таких как Oz , E и AmbientTalk , можно получить доступное только для чтения представление будущего, которое позволяет читать его значение при разрешении, но не позволяет его разрешить:
!!
оператор используется для получения представления, доступного только для чтения.std::future
предоставляет представление только для чтения. Значение устанавливается непосредственно с помощью std::promise
или устанавливается в результат вызова функции с помощью std::packaged_task
или std::async
.System.Threading.Tasks.Task<T>
представляет собой представление только для чтения. Разрешение значения может быть выполнено с помощью System.Threading.Tasks.TaskCompletionSource<T>
.Поддержка представлений только для чтения соответствует принципу наименьших привилегий , поскольку позволяет устанавливать значение только для субъектов , которым необходимо его установить. В системе, которая также поддерживает конвейерную обработку, отправитель асинхронного сообщения (с результатом) получает обещание результата только для чтения, а цель сообщения получает преобразователь.
Некоторые языки, такие как Alice ML , определяют фьючерсы, связанные с определенным потоком, который вычисляет значение фьючерса. [9] Эти вычисления могут начинаться либо сразу , когда создается будущее, либо лениво, когда его значение впервые требуется. Ленивое будущее похоже на thunk в смысле отложенного вычисления.
Алиса ML также поддерживает фьючерсы, которые могут быть разрешены любым потоком, и называет эти обещания . [8] Такое использование обещания отличается от его использования в E, как описано выше. В Алисе обещание не доступно только для чтения, а конвейеризация обещаний не поддерживается. Вместо этого конвейеризация естественным образом происходит для фьючерсов, в том числе связанных с обещаниями.
Если доступ к значению будущего осуществляется асинхронно, например, путем отправки ему сообщения или явного ожидания его с помощью конструкции, такой как when
в E, то нет никаких трудностей с отсрочкой до тех пор, пока будущее не будет решено, прежде чем сообщение может быть отправлено. получено или ожидание завершается. Это единственный случай, который следует учитывать в чисто асинхронных системах, таких как чистые языки актеров.
Однако в некоторых системах также можно попытаться немедленно или синхронно получить доступ к значению будущего времени. Далее предстоит сделать выбор дизайна:
В качестве примера первой возможности в C++11 поток, которому нужно значение будущего, может блокироваться до тех пор, пока оно не станет доступным, вызвав функции- члены wait()
или get()
. Вы также можете указать тайм-аут ожидания с помощью функций-членов wait_for()
или wait_until()
, чтобы избежать неопределенной блокировки. Если будущее возникло в результате вызова then, то std::async
ожидание блокировки (без таймаута) может привести к синхронному вызову функции для вычисления результата в ожидающем потоке.
Фьючерсы — это частный случай примитива синхронизации « событий », который может быть завершен только один раз. В общем, события можно сбрасывать в исходное пустое состояние и, таким образом, выполнять сколько угодно раз. [11]
I -var (как и в языке Id ) — это будущее с семантикой блокировки, как определено выше. I -структура — это структура данных , содержащая I-переменные. Связанная конструкция синхронизации, которую можно устанавливать несколько раз с разными значениями, называется M-var . M-переменные поддерживают атомарные операции по получению или помещению текущего значения, при этом принятие значения также возвращает M-var в исходное пустое состояние. [12]
Параллельная логическая переменная [ нужна цитация ] похожа на фьючерсную, но обновляется путем унификации , точно так же, как логические переменные в логическом программировании . Таким образом, его можно привязать к унифицированным значениям более одного раза, но нельзя вернуть в пустое или неразрешенное состояние. Переменные потока данных Oz действуют как переменные параллельной логики, а также имеют семантику блокировки, как упоминалось выше.
Переменная параллельного ограничения — это обобщение переменных параллельной логики для поддержки программирования логики ограничений : ограничение можно сужать несколько раз, указывая меньшие наборы возможных значений. Обычно существует способ указать преобразователь, который должен запускаться всякий раз, когда ограничение еще больше сужается; это необходимо для поддержки распространения ограничений .
Eager фьючерсы, специфичные для потока, могут быть напрямую реализованы в фьючерсах, не специфичных для потока, путем создания потока для вычисления значения одновременно с созданием будущего. В этом случае желательно вернуть клиенту представление только для чтения, чтобы только вновь созданный поток мог разрешить это будущее.
Для реализации неявных ленивых фьючерсов, специфичных для потока (как, например, предоставлено Алисой МЛ) в терминах фьючерсов, не специфичных для потока, необходим механизм, определяющий, когда в первую очередь требуется значение фьючерса (например, конструкция WaitNeeded
в Oz [13] ] ). Если все значения являются объектами, то возможности реализовать прозрачные объекты пересылки достаточно, поскольку первое сообщение, отправленное в пересылку, указывает на то, что требуется будущее значение.
Неспецифические для потока фьючерсы могут быть реализованы в фьючерсах, специфичных для потока, при условии, что система поддерживает передачу сообщений, заставляя разрешающий поток отправлять сообщение собственному потоку будущего. Однако это можно рассматривать как ненужную сложность. В языках программирования, основанных на потоках, наиболее выразительным подходом, по-видимому, является обеспечение сочетания фьючерсов, не связанных с потоками, представлений только для чтения и либо конструкции WaitNeeded , либо поддержки прозрачной пересылки.
Стратегия оценки фьючерсов, которую можно назвать вызовом будущего , недетерминирована: стоимость фьючерса будет оценена в какой-то момент между созданием фьючерса и моментом использования его значения, но точное время не определено. заранее и может меняться от запуска к запуску. Вычисление может начаться сразу после создания будущего ( активная оценка ) или только тогда, когда значение действительно необходимо ( ленивая оценка ), и может быть приостановлено на полпути или выполнено за один прогон. Как только значение будущего присвоено, оно не пересчитывается при будущих обращениях; это похоже на мемоизацию , используемую при вызове по необходимости .
Аленивое будущее — это будущее, которое детерминированно имеет семантику ленивых вычислений: вычисление значения будущего начинается, когда это значение впервые необходимо, как при вызове по необходимости. Ленивые будущие значения используются в языках, стратегия оценки которых по умолчанию не является ленивой. Например, вC++11такие ленивые фьючерсы можно создать, передавstd::launch::deferred
политику запуска вstd::async
, а также функцию для вычисления значения.
В модели актера выражение формы future <Expression>
определяется тем, как оно реагирует на Eval
сообщение со средой E и клиентом C следующим образом: Выражение будущего отвечает на Eval
сообщение, отправляя клиенту C вновь созданного актера F (прокси для ответ оценки <Expression>
) в качестве возвращаемого значения одновременно с отправкой <Expression>
сообщения Eval
среде E и клиенту C. Поведение F по умолчанию следующее:
<Expression>
следующие действия:<Expression>
, тогда V сохраняется в F иОднако некоторые фьючерсы могут обрабатывать запросы особым образом, чтобы обеспечить больший параллелизм. Например, выражение 1 + future factorial(n)
может создать новое будущее, которое будет вести себя как число 1+factorial(n)
. Этот трюк не всегда срабатывает. Например, следующее условное выражение:
if m>future factorial(n) then print("bigger") else print("smaller")
приостанавливается до тех пор, пока будущее factorial(n)
не ответит на запрос, m
больше ли оно самого себя.
Конструкции будущего и/или обещания были впервые реализованы в таких языках программирования , как MultiLisp и Act 1 . Использование логических переменных для связи в языках программирования параллельной логики очень похоже на фьючерсы. Они начались в Прологе с Freeze и IC Prolog и стали настоящим примитивом параллелизма с Relational Language, Concurrent Prolog , защищенными предложениями Horn (GHC), Parlog , Strand , Vulcan , Janus , Oz-Mozart , Flow Java и Alice ML . I-var с одним присвоением из языков программирования потоков данных , происходящий из Id и включенный в Concurrent ML Reppy , во многом похож на переменную параллельной логики.
Техника конвейеризации обещаний (использование фьючерсов для преодоления задержки) была изобретена Барбарой Лисков и Любой Шрирой в 1988 году [6] и независимо Марком С. Миллером , Дином Трибблом и Робом Джеллингхаусом в контексте проекта «Занаду» примерно в 1989 году . [14]
Термин обещание был придуман Лисковым и Шрирой, хотя они ссылались на механизм конвейерной обработки под названием call-stream , которое сейчас используется редко.
Как дизайн, описанный в статье Лискова и Шриры, так и реализация конвейерной обработки обещаний в Xanadu, имели ограничение, заключающееся в том, что значения обещаний не были первоклассными : аргумент или значение, возвращаемое вызовом или отправкой, не могли напрямую быть обещанием. (поэтому приведенный ранее пример конвейерной обработки обещаний, в котором обещание для результата одной отправки используется в качестве аргумента для другой, не мог быть напрямую выражен в конструкции потока вызовов или в реализации Xanadu). Похоже, что промисы и потоки вызовов никогда не были реализованы ни в одной общедоступной версии Argus, [15] языка программирования, используемого в статье Лискова и Шриры. Разработка Argus прекратилась примерно в 1988 году. [16] Реализация конвейерной обработки обещаний в Xanadu стала общедоступной только с выпуском исходного кода Udanax Gold [17] в 1999 году и никогда не объяснялась ни в одном опубликованном документе. [18] Более поздние реализации в Joule и E полностью поддерживают первоклассные промисы и преобразователи.
Несколько ранних языков актеров, включая серию Act, [19] [20] поддерживали как параллельную передачу сообщений, так и конвейерную обработку сообщений, но не конвейерную обработку обещаний. (Хотя технически возможно реализовать последнюю из этих функций в первых двух, нет никаких доказательств того, что языки Закона сделали это.)
После 2000 года произошло значительное возрождение интереса к фьючерсам и обещаниям из-за их использования в отзывчивости пользовательских интерфейсов, а также в веб-разработке из-за модели передачи сообщений « запрос-ответ ». В нескольких основных языках теперь есть языковая поддержка фьючерсов и обещаний, наиболее популяризированная FutureTask
в Java 5 (анонсировано в 2004 г.) [21] и конструкциях async/await в .NET 4.5 (анонсировано в 2010 г., выпущено в 2012 г.) [22] [23] в основном вдохновлен асинхронными рабочими процессами F#, [24] появившимися в 2007 году. [25] Впоследствии это было принято другими языками, в частности Dart (2014), [26] Python (2015), [27] Hack (HHVM), и проекты ECMAScript 7 (JavaScript), Scala и C++ (2011 г.).
Некоторые языки программирования поддерживают фьючерсы, обещания, переменные параллельной логики, переменные потока данных или I-вары либо посредством прямой поддержки языка, либо в стандартной библиотеке.
java.util.concurrent.Future
илиjava.util.concurrent.CompletableFuture
async
, await
начиная с ECMAScript 2017 [33]async
и await
[23]kotlin.native.concurrent.Future
обычно используется только при написании Kotlin, предназначенного для запуска в исходном виде [35]..await
) [41]Языки, также поддерживающие конвейеризацию обещаний, включают:
async
неблокируемость в стиле C# await
[95]Фьючерсы могут быть реализованы в сопрограммах [27] или генераторах [103] , что приводит к одной и той же стратегии оценки (например, кооперативная многозадачность или ленивая оценка).
Фьючерсы легко реализовать в каналах : фьючерс — это одноэлементный канал, а обещание — это процесс, который отправляет в канал, исполняя будущее. [104] [105] Это позволяет реализовать фьючерсы на параллельных языках программирования с поддержкой каналов, таких как CSP и Go . Полученные фьючерсы являются явными, поскольку доступ к ним должен быть получен путем чтения из канала, а не только путем оценки.
{{cite journal}}
: Требуется цитировать журнал |journal=
( помощь ){{cite journal}}
: Требуется цитировать журнал |journal=
( помощь )