Мы уже познакомились с атомами. Атом - контейнер, атомарно обновляющий свое содержимое.
Вызов swap!
блокирует текущий поток, т.е. это синхнонная операция.
При этом операция swap!
нескоординированная, т.е. swap!
влияет только на один объект.
Допустим, мы хотим перевести деньги с одного счета на другой, так, чтобы в любой момент вермени в системе было постоянное количество денег.
(let [a (atom 1000)
b (atom 0)]
(future (swap! a - 100)
(Thread/sleep 100)
(swap! b + 100))
(Thread/sleep 50)
{:total (+ @a @b)
:a @a
:b @b}) ;;=> {:total 900, :a 900, :b 0}
Как видим, атомы не позволяют обеспечить постоянное количество денег в системе. Получилось так, что со счета списалось 100 монет, а на другой еще не записались.
Существуют и другие ссылочные типы, которые могут быть синнхронными/ассинхронными и скоординированными/нескоординированными.
С их помощью реализуется механизм Software Transaction Memory(STM).
(let [a (ref 1000)
b (ref 0)]
(future (dosync
(alter a - 100)
(Thread/sleep 100)
(alter b + 100)))
(Thread/sleep 50)
{:total (+ @a @b)
:a @a
:b @b}) ;;=> {:total 1000, :a 1000, :b 0}
(let [a (ref 1000)
b (ref 0)]
(future (dosync
(alter a - 100)
(Thread/sleep 100)
(alter b + 100)))
(Thread/sleep 150)
{:total (+ @a @b)
:a @a
:b @b}) ;;=> {:total 1000, :a 900, :b 100}
dosync
- начинает транзакцию, alter
- аналог swap!
, только для ref
,
обязательно вызвывается внутри dosync
.
В примере с ref в любой момент времени состояние счетов непротиворечиво, и ссылки обновляются согласовано.
Если какой-то сторонний поток поменяет ссылку, участвующую в транзакции, то это транзакция перезапустится. Именно поэтому для обновления ссылок нужно использовать чистые функции.
Для изменения значения ссылки есть следующие функции:
Т.е. это скоординированный ссылочный тип, с синхронными операциями.
Мы используем их с начала знакомства с clojure.
Когда мы вызываем (def x 1)
или (defn f [arg] 1)
, то на самом деле создаем var(переменную).
Вместо того, чтобы постоянно писать (@f arg)
для вызова функции, хранящейся в переменной f
,
просто пишут (f arg)
.
Чтобы получить саму переменную - нужно воспользоваться специальной формой
(var f)
или #'f
. Наверняка, вы видили #'user/f
, когда выполняли выражение (defn f [])
.
user
- это пространство имен, в котором определена переменная.
Переменные позволяют переопределять свое значение для всех потоков:
(defn f [] 1)
(f) ;;=> 1
(alter-var-root #'f
(fn [old-value]
(fn [] 2)))
(f) ;;=> 2
alter-var-root
можно использовать для декорирования фукнций:
(defn f [] (prn :ok))
(alter-var-root #'f memoize)
(f) => вернет nil и напечатает :ok
(f) => просто вернет nil
memoize
- стандартная функция, принимающая функцию, и возвращающая мемоизированный вариант.
Повторюсь, что x
- получение значения переменной, а не самой переменной:
(def x 1)
(let [x' x] ;; запомнили содержимое x в x'
(alter-var-root #'x inc)
[x' x]) ;;=> [1 2]
Если мы используем функции, то значение извлекается на каждый вызов:
(def x 1)
(defn f [] x)
(f) ;;=> 1
(alter-var-root #'x inc)
(f) ;;=> 2
Если мы хотим ссылаться на саму переменную, то нужно поступить так:
(def x 1)
(let [x' #'x] ;; запомнили саму переменную x в x'
(alter-var-root #'x inc)
[@x' x]) ;;=> [2 2]
Т.е. переменные, как и прочие ссылочные типы, позволяют получить свое значение с помощью @
.
Переменные реализуют интерфейс фукнкций:
(defn f [] 1)
;; вызываем функцию
(f) ;;=> 1
;; получаем переменную, извлекаем ее заначение и вызываем это значение
;; аналогично предыдущему
(@#'f) ;;=> 1
;; используем переменную в качестве функции
(#'f) ;;=> 1
Это бывает полезно, если вы передаете функцию как значение, например используете функцию высшего порядка, и хотите оставить возможность переопределять значение вашей функции. Это похоже на передачу по значению и по ссылке(указателю):
(defn f [] 1)
(defn inspect [f]
(fn [& args]
(prn args) ;; печатаем аргументы
(let [result (apply f args)] ;; вызываем функцию с аргументами в виде коллекции
(prn result) ;; печатаем результат
result)))
(let [f1 (inspect f) ;; передача по значению
f2 (inspect #'f)] ;; похоже на передачу по ссылке
(alter-var-root #'f (fn [old]
(fn [] 2)))
[(f1) (f2)]) ;;=> [1 2]
Переменную можно переопределить для определенной области и вернуть исходное значение:
(defn f [] 1)
(with-redefs [f (fn [] 2)]
(f)) ;;=> 2
Нужно учитывать, что все потоки увидят это измененное изменение, что может привести к неожиданным результатам.
Этот способ не работает для inline функций, т.к. они не используют var, т.е. для большинства стандартных:
(with-redefs [+ -]
(+ 2 1)) ;;=> 3
Все переменные по умолчанию статические. Бывают еще и динамические. Они позволяют переопределить
свое значение только для текущего потока. По соглашению таким переменным надевают наушники: *some-var*
.
(def ^:dynamic *x* 1)
(let [a (future
(binding [*x* 2]
(Thread/sleep 100)
*x*))
b (future
(Thread/sleep 50)
*x*)]
[@a @b]) ;;=> [2 1]
Если бы binding переопределял значение для всех потоков, то @b
вернул бы 2.
При этом, clojure функции, умеют запоминать контекст, а java tread - нет:
(def ^:dynamic *x* 1)
(binding [*x* 2]
(future (prn "future" *x*))
(.start (Thread. (fn [] (prn "thread" *x*))))
;; вывод на печать:
;; "future" 2
;; "thread" 1
Ленивые коллекции, future и т.п. умеют запоминать контекст треда начиная с версии clojure 1.3. Сторонние библиотеки, вроде core.async, также сохраняют контекст. Есть максрос bound-fn с помощью которого вы можете запомнить контекст, например, при работе с java interop.
В дальнейшем мы будем использовать динамические переменные для внедрения зависимостей
и я покажу когда стоит или не стоит их использовать.
Внедрять зависимость можно и с помощью alter-var-root
, но как быть, если вам нужен
новый инстанс зависимости на каждый запрос, например сессия пользователя.
Переменные могут быть и локальными, смотри with-local-vars
.
Так же переменные можно сделать приватными, т.е. они доступны только в пределах своего неймспейса:
(def ^:private x 1)
(defn ^:private f [])
(defn- g [])
По нашей классификации это нескоординированный ссылочный тип, с синхронными операциями.
Агент - контейнер для значения с очередью операций над ним:
(let [a (agent 0)]
(send a (fn [old]
(Thread/sleep 50)
(inc old)))
@a) ;;=> 0
(let [a (agent 0)]
(send a inc)
(await a) ;; ждем, пока обработается очередь
@a) ;;=> 1
Агент принимает сообщения в виде функций, выстраивает их в очередь и в отдельном потоке
заменяет свое значение с помощью этих фукнций. Т.е. send
- неблокирующая фукнция.
При этом агенты встроены в STM, т.е. сообщение будет отправлено только после успешного завершения транзакции:
(let [counter (ref 0)
calls-atom (atom 0)
calls-agent (agent 0)]
(->> (repeatedly #(future
(dosync
(swap! calls-atom inc)
(send calls-agent inc)
(alter counter inc))))
(take 100)
(doall)
(map deref)
(doall))
(await calls-agent) ;; ждем, пока обработается очередь
{:counter @counter
:calls-atom @calls-atom
:calls-agent @calls-agent}) ;;=> {:counter 100, :calls-atom 102, :calls-agent 100}
Атомы не итнегрированы в STM, поэтому при повторении транзакции из-за конфликтов calls-atom
показывает
количество успешных и неуспешных транзакций.
Но агент интегрирован в STM и получает сообщения только после успешного
завершения транзакции.
Фукнция send
использует системный тредпул, и если функция может долго выполяеться, то использют
send-off
,
который выполняет эту функцию вне системного тредпула.
Это ассинхронный несогласованный ссылочный тип.
Все ссылочные типы позволяют установить валидатор и добавить наблюдателей:
Это неполноценный ссылочный тип, но зато очень быстрый. При этом он не имеет поддержки валидаторов и наблюдателей.
(let [v (volatile! 0)]
(vswap! v inc)
@v) ;;=> 1
Он реализован
как тривиальный java класс, хранящий состояние в volatile
переменной.
JVM содержит оптимизации, и если один поток изменил переменную, другие потоки могут не увидеть
это изменение. Для этого случая в java есть ключевое слово volatile
, которое показывает, что значение
переменной может быть изменено в другом потоке.
Естественно, он не гарантирует атомарности как атом:
(let [counter-a (atom 0)
counter-v (volatile! 0)]
(->> (repeatedly #(future
(swap! counter-a inc)
(vswap! counter-v inc)))
(take 100)
(doall)
(map deref)
(doall))
{:counter-a @counter-a
:counter-v @counter-v}) ;;=> {:counter-a 100, :counter-v 98}