future, promise та delay — конструкції, що використовуються в деяких мовах програмування та утворюють стратегію обчислення, яка застосовується для паралельних обчислень. За їхньою допомогою описується об'єкт, до якого можна звернутися за результатом, обчислення якого на цей момент може ще не бути завершеним.

Термінологія

Термін promise був запропонований 1976 року Деніелом Фрідманом і Девідом Вайзом,Preliminary version of: а Пітер Гіббард назвав його eventual. Подібна концепція під назвою future була запропонована 1977 року в статті Генрі Бейкера та Карла Г'юїтта.

Терміни future, promise і delay досить часто використовуються як взаємозамінні, однак нижче описано різницю між future і promise. Під future зазвичай мається на увазі подання змінної, доступне лише для читання, тоді як promise — змінний контейнер з одноразовим присвоєнням, який передає значення future.SIP-14 — Futures and Promises // Scala Future може бути визначений без зазначення того, з якого promise буде отримано значення. Також з одним future може бути пов'язано кілька promise, однак присвоїти значення future зможе лише один promise. В інших випадках future і promise створюються разом і пов'язуються одне з одним: future — це значення, а promise — функція, яка присвоює це значення. На практиці future — це значення, що повертається асинхронною функцією promise. Процес присвоєння значення future називають resolving, fulfilling або binding.

Застосування Future і promise виникли у функційному програмуванні та споріднених парадигмах (таких як логічне програмування) для відокремлення значення (future) від способу його обчислення (promise), що дає змогу виконувати обчислення гнучкіше, зокрема шляхом їх паралелізації. Пізніше вони знайшли застосування в розподілених обчисленнях, де використовувалися для зменшення затримок, спричинених циклами обміну повідомленнями. Згодом вони набули ще ширшого застосування, оскільки дали змогу писати асинхронні програми у прямому стилі, а не в стилі передавання продовження ().

Неявне та явне використання future Використання future може бути неявним (при будь-якому зверненні до future повертається посилання на значення) і явним (користувач повинен викликати функцію, щоб отримати значення). Прикладом є метод get класу java.util.concurrent.Future у мові Java. Отримання значення з явного future називають stinging або forcing. Явні future можуть бути реалізовані як бібліотека, тоді як неявні зазвичай реалізуються як частина мови.

У статті Бейкера і Г'юїтта описано неявні future, які природним чином підтримуються в обчислювальній моделі акторів і суто об'єктно-орієнтованих мовах, таких як Smalltalk. У статті Фрідмана і Вайза описано лише явні future, найімовірніше через складність реалізації неявних future на звичайних комп'ютерах. Складність полягає в тому, що на апаратному рівні неможливо працювати з future як із примітивним типом даних на кшталт цілих чисел.

Конвеєризація promise Використання future може істотно зменшити затримку в розподілених системах. Зокрема, future уможливлюють конвеєризацію promise (promise pipelining),Promise Pipelining at erights.orgPromise Pipelining on the C2 wiki реалізовану в мовах E і Joule, яку також називали call-stream Also published in ACM SIGPLAN Notices 23(7). у мові Argus.

Розглянемо вираз, що використовує звичайні віддалені виклики процедур, наприклад:

t3 := ( x.a() ).c( y.b() )

який можна розгорнути до

t1 := x.a(); t2 := y.b(); t3 := t1.c(t2);

Кожен оператор потребує надсилання повідомлення й отримання відповіді, перш ніж можна буде виконати наступний оператор. Припустімо, наприклад, що x, y, t1 і t2 розташовані на одній віддаленій машині. У такому разі до початку виконання третього оператора мають відбутися два повних мережевих цикли «запит–відповідь» до цієї машини. Потім третій оператор спричинить ще один такий самий цикл до тієї самої віддаленої машини.

За допомогою future наведений вираз можна записати так:

t3 := (x <- a()) <- c(y <- b())

Його можна розгорнути до

t1 := x <- a(); t2 := y <- b(); t3 := t1 <- c(t2);

Тут використано синтаксис мови E, де x <- a() означає асинхронне надсилання повідомлення a() до x. Усім трьом змінним негайно присвоюються future, що представляють їхні результати, після чого виконання переходить до наступних операторів. Пізніші спроби отримати значення t3 можуть призвести до затримки; однак конвеєризація дає змогу зменшити кількість необхідних мережевих циклів. Якщо, як і в попередньому прикладі, x, y, t1 і t2 розташовані на одній віддаленій машині, то реалізація з конвеєризацією може обчислити t3 за один мережевий цикл замість трьох. Оскільки всі три повідомлення призначені для об'єктів, що перебувають на одній віддаленій машині, достатньо надіслати лише один запит і отримати лише одну відповідь, яка містить результат. Надсилання t1 <- c(t2) не блокуватиметься навіть тоді, коли t1 і t2 перебувають на різних машинах між собою або відносно x чи y.

Конвеєризацію promise слід відрізняти від паралельного асинхронного обміну повідомленнями. У системі, що підтримує паралельне передавання повідомлень, але не підтримує конвеєризацію, надсилання повідомлень x <- a() і y <- b() у наведеному вище прикладі може виконуватися паралельно, але надсилання t1 <- c(t2) мусить чекати, доки не будуть отримані і t1, і t2, навіть якщо x, y, t1 і t2 перебувають на одній віддаленій машині. Відносна перевага конвеєризації щодо затримки стає ще більшою в складніших ситуаціях, коли обмінюються великою кількістю повідомлень.

Конвеєризацію promise також не слід плутати з конвеєрною обробкою повідомлень у системах акторів, де актор може визначити й розпочати виконання поведінки для наступного повідомлення ще до завершення обробки поточного.

Представлення лише для читання

У деяких мовах програмування, таких як Oz, E і AmbientTalk, можна отримати представлення лише для читання (read-only view) future, яке дає змогу читати його значення після resolve, але не дозволяє робити resolve:

В Oz для отримання представлення лише для читання використовується оператор !!. У E та AmbientTalk future представляється парою значень, що називається пара promise/resolver. Promise є представленням лише для читання, а resolver потрібен для встановлення значення future. У C++ (починаючи з C++11) std::future забезпечує представлення лише для читання. Значення встановлюється безпосередньо за допомогою std::promise або задається як результат виклику функції за допомогою std::packaged_task чи std::async. В API Deferred бібліотеки Dojo Toolkit, починаючи з версії 1.5, об'єкт promise лише для споживача (consumer-only promise object) є представленням лише для читання. У Alice ML future забезпечують представлення лише для читання, тоді як promise містить як future, так і можливість завершити його, встановивши значення. У .NET System.Threading.Tasks.Task<T> є представленням лише для читання. Завершення future зі встановленням значення може бути виконане за допомогою System.Threading.Tasks.TaskCompletionSource<T>.

Підтримка представлень лише для читання узгоджується з принципом найменших привілеїв, оскільки дає змогу обмежити можливість встановлення значення лише тими суб'єктами, яким це необхідно. У системі, що також підтримує конвеєризацію, відправник асинхронного повідомлення (із результатом) отримує promise лише для читання, що представляє результат, тоді як одержувач повідомлення отримує resolver.

Future, специфічні для потоку

Деякі мови, такі як Alice ML, визначають future, пов'язані з певним потоком, який обчислює значення future. Це обчислення може розпочинатися або жадібно під час створення future, або ліниво, коли його значення вперше стає потрібним. Лінивий future подібний до thunk у тому сенсі, що являє собою відкладене обчислення.

Alice ML також підтримує future, які можуть бути завершені будь-яким потоком, і називає їх promise. Таке використання терміна promise відрізняється від його використання в E, описаного вище. В Alice promise не є представленням лише для читання, а конвеєризація promise не підтримується. Натомість конвеєризація природним чином відбувається для future, зокрема й тих, що пов'язані з promise.

Блокуюча та неблокуюча семантика

Якщо до значення future звертаються асинхронно, наприклад шляхом надсилання йому повідомлення або явного очікування за допомогою такої конструкції, як when у мові E, то немає жодних труднощів із тим, щоб відкласти виконання до моменту, коли future буде завершено, перш ніж повідомлення буде отримано або очікування завершиться. Це єдиний випадок, який потрібно розглядати в суто асинхронних системах, таких як чисті мови акторів.

Однак у деяких системах також можлива спроба негайного або синхронного доступу до значення future. У такому разі необхідно зробити вибір щодо проєктування:

доступ може блокувати поточний потік або процес, доки future не буде завершено (можливо, із тайм-аутом). Такою є семантика змінних потоку даних (dataflow variables) у мові Oz. спроба синхронного доступу може завжди сигналізувати про помилку, наприклад викидаючи виняток. Такою є семантика віддалених promise у мові E. можливо також, що доступ буде успішним, якщо future вже завершено, і сигналізуватиме про помилку, якщо це не так. Недоліком такого підходу є внесення недетермінізму та можливість виникнення станів гонитви, тому він, імовірно, використовується рідко.

Як приклад першого варіанта, у C++11 потік, якому потрібне значення future, може блокуватися до моменту, коли воно стане доступним, викликавши функції-члени wait() або get(). Щоб уникнути блокування на невизначений час, під час очікування також можна задати тайм-аут за допомогою функцій-членів wait_for() або wait_until(). Якщо future виник унаслідок виклику std::async, то блокувальне очікування (без тайм-ауту) може спричинити синхронний виклик функції для обчислення результату в потоці, який очікує.

Споріднені конструкції

Future є окремим випадком примітиву синхронізації «подія», який може бути завершений лише один раз. Загалом події можна повертати до початкового порожнього стану і, таким чином, завершувати стільки разів, скільки потрібно.500 lines or less, "A Web Crawler With asyncio Coroutines" by A. Jesse Jiryu Davis and Guido van Rossum says "implementation uses an asyncio.Event in place of the Future shown here. The difference is an Event can be reset, whereas a Future cannot transition from resolved back to pending."

I-var (як у мові Id) — це future із блокувальною семантикою, визначеною вище. I-structure — це структура даних, що містить I-var. Споріднена конструкція синхронізації, якій можна багаторазово присвоювати різні значення, називається M-var. M-var підтримують атомарні операції take і put для поточного значення, причому операція take також повертає M-var до її початкового порожнього стану.

Паралельна логічна змінна (concurrent logic variable) подібна до future, але оновлюється за допомогою уніфікації так само, як і логічні змінні в логічному програмуванні. Тому її можна зв'язувати більше одного разу з такими значеннями, що допускають уніфікацію, але не можна повернути до порожнього або незавершеного стану. Змінні потоку даних у Oz поводяться як паралельні логічні змінні, а також мають блокувальну семантику, згадану вище.

Паралельна змінна обмежень (concurrent constraint variable) є узагальненням паралельних логічних змінних для підтримки логічного програмування з обмеженнями: обмеження можна багаторазово звужувати, указуючи менші множини можливих значень. Зазвичай існує спосіб задати thunk, який має виконуватися щоразу, коли обмеження ще більше звужується; це необхідно для підтримки поширення обмежень (constraint propagation).

Виразність різних форм future

Жадібні future, специфічні для потоку, можна безпосередньо реалізувати за допомогою future, не прив'язаних до конкретного потоку, створивши потік для обчислення значення одночасно зі створенням future. У такому разі бажано повертати клієнтові представлення лише для читання, щоб лише новостворений потік мав змогу завершити цей future.