Статьи

Обещания и Фьючерсы в Clojure

Clojure, предназначенный для параллелизма, естественным образом подходит для нашей серии « Назад в будущее » . Кроме того, фьючерсы поддерживаются «из коробки» в Clojure. И последнее, но не менее важное, Clojure является первым языком / библиотекой, который проводит четкое различие между фьючерсами и обещаниями . Они настолько похожи, что большинство платформ поддерживают только фьючерсы или объединяют их. Clojure очень явно здесь, что хорошо. Начнем с обещаний:

обещания

Promise — это потокобезопасный объект, который инкапсулирует неизменное значение. Это значение может быть еще недоступно и может быть доставлено ровно один раз, из любого потока, позже. Если другой поток попытается разыменовать обещание до его выполнения, он будет блокироваться бесконечно. Если обещают

уже разрешено (доставлено), блокировка не происходит. Promise может быть доставлен только один раз и никогда не сможет изменить его значение после установки:

1
2
3
4
5
(def answer (promise))
  
@answer
  
(deliver answer 42)

answer promise вар. Попытка разыменования его с помощью @answer или (deref answer) на этом этапе просто заблокирует. Этот или какой-то другой поток должен сначала доставить какое-то значение этому обещанию (используя функцию deliver ). Все потоки, заблокированные на deref , проснутся, и последующие попытки разыменовать это обещание немедленно вернут 42 . Обещание является потокобезопасным, и вы не можете изменить его позже. Попытка передать другое значение в answer игнорируется.

фьючерсы

Фьючерсы ведут себя в Clojure почти одинаково с точки зрения пользователя — они являются контейнерами для одного значения (конечно, это может быть map или list — но это должно быть неизменным) и пытаются разыменовать будущее, прежде чем оно будет разрешено бесконечно. Также как и обещания, фьючерсы могут быть разрешены только один раз, и разыменование разрешенного будущего имеет немедленный эффект. Разница между ними семантическая, а не техническая. Будущее представляет собой фоновые вычисления, обычно в пуле потоков, в то время как обещание — это простой контейнер, который может быть доставлен (заполнен) кем угодно в любой момент времени. Обычно нет связанной фоновой обработки или вычислений. Это больше похоже на событие, которое мы ждем (например, ответ на сообщение JMS, которого мы ждем ). При этом давайте начнем некоторую асинхронную обработку. Как и в Akka , базовый пул потоков является неявным, и мы просто передаем фрагмент кода, который хотим запустить в фоновом режиме. Например, чтобы вычислить сумму натуральных чисел ниже десяти миллионов, мы можем сказать:

1
2
3
4
5
(let
    [sum (future (apply + (range 1e7)))]
    (println "Started...")
    (println "Done: " @sum)
)

sum является будущим экземпляром. Сообщение "Started..." появляется сразу же после вычисления в фоновом потоке. Но @sum блокируется, и нам на самом деле нужно немного подождать 1, чтобы увидеть сообщение "Done: " и результаты вычислений. И вот тут самое большое разочарование: ни future ни promise в Clojure не поддерживают асинхронное прослушивание завершения / неудачи. API в значительной степени эквивалентен очень ограниченному java.util.concurrent.Future<T> . Мы можем создать future , cancel его , проверить, realized? ли оно realized? (решено) и блокировать ожидание значения. Так же, как Future<T> в Java, на самом деле результат future функции даже реализует java.util.concurrent.Future<T> . Как бы я ни любил примитивы параллелизма Clojure, такие как STM и агенты, фьючерсы чувствуют себя недостаточно развитыми. Отсутствие управляемых событиями асинхронных обратных вызовов, которые вызываются при завершении фьючерсов (обратите внимание, что add-watch не работает с фьючерсами и все еще находится в альфа-режиме), значительно снижает полезность будущего объекта. Мы больше не можем:

  • сопоставить фьючерсы для асинхронного преобразования значения результата
  • цепные фьючерсы
  • перевести список фьючерсов в будущее списка
  • … И многое другое, посмотрите, как это делает Akka и Guava в некоторой степени

Это позор, и, поскольку это не техническая сложность, а только отсутствующий API, я надеюсь скоро увидеть поддержку слушателей завершения. Для полноты здесь приведена немного более крупная программа, использующая фьючерсы для одновременного извлечения содержимого нескольких веб-сайтов, которая является основой для нашего образца сканирования :

01
02
03
04
05
06
07
08
09
10
11
12
(let [
    top-sites `("www.google.com" "www.youtube.com" "www.yahoo.com" "www.msn.com")
    futures-list (doall (
            map #(
                future (slurp (str "http://" %))
            )
            top-sites
    ))
    contents (map deref futures-list)
    ]
(doseq [s contents] (println s))
)

Приведенный выше код начинает одновременную загрузку содержимого нескольких веб-сайтов. map deref ждет всех результатов один за другим, и как только все фьючерсы из futures-list все завершены, doseq выводит содержимое ( contents представляет собой список строк).

Одной из ловушек, в которую я попал, было отсутствие doall (которое заставляет ленивую оценку последовательности) в моей первоначальной попытке. map создает ленивую последовательность из списка top-sites , что означает, что future функция вызывается только при первом доступе к данному элементу futures-list . Это хорошо. Но каждый элемент доступен в первый раз только во время (map deref futures-list) . Это означает, что, ожидая разыскивания первого будущего, второе будущее еще даже не началось! Это начинается, когда первое будущее завершается, и мы пытаемся разыменовать второе. Это означает, что последнее будущее начинается тогда, когда все предыдущие фьючерсы уже завершены. Короче говоря, безо doall которая заставляет все фьючерсы запускаться немедленно, наш код запускается последовательно, одно за другим. Красота побочных эффектов.

1 — BTW (1L to 9999999L).sum в Scala быстрее почти на порядок, просто скажу

Ссылка: Обещания и фьючерсы на Clojure от нашего партнера по JCG Томаша Нуркевича в блоге NoBlogDefFound .