Clojure

Ссылки и транзакции

Содержание

В то время как переменные обеспечивают безопасное использование изменяемых хранилищ через изоляцию потоков, транзакционные ссылки обеспечивают безопасное коллективное использование изменяемых хранилищ с помощью программной транзакционной памяти (software transactional memory, SТМ). Ссылки привязаны к одному хранилищу в течении своей жизни и позволяют изменять его содержимое только внутри транзакции.

Понять транзакции Clojure должно быть просто, если вы когда-нибудь использовали транзакции баз данных - они гарантируют, что все действия со ссылками атомарны, консистентны и изолированы. Атомарны означает, что каждое изменение ссылок сделанное внутри транзакции либо происходит целиком либо не происходит вовсе. Консистентны означает, что каждое новое значение может быть проверено с помощью функции перед выполнением транзакции. Изолированы означает, что пока транзакция не завершена, другие транзацкции не увидят результат её работы. Еще одно свойство общее с STM (программной трензакционной памятью) - если во время выполнения транзакции возникает конфликт - она автоматически повторяется.

Существуют несколько способов реализации STM (программной транзакционной памяти) - блокирующая/пессимистичная, безблокировочная/оптимистическая, гибриды. В этом направлении разработки ведутся до сих пор. Для реализации STM Clojure использует управление конкурентным доступом с помощью многоверсионности (MVCC - multiversion concurrency control) с адаптивной историей для изоляции версий и предоставляет отдельную функцию commute.

На практике это означает:

  1. Все операции чтения ссылок будут производиться в версии "мира ссылок", соответствующей началу транзакции (её "точка чтения"). Транзакции будут видеть все свои изменения, называемые значение-внутри-транзакции.

  2. Все изменения, производимые со ссылками в течении транзакции (с помощью ref-set, alter или commute) будут применены в "мире ссылок" в один момент времени ("точке записи" транзакции).

  3. Другие транзакции не смогут модифицировать ссылки, изменение которых было запрещено функциями ref-set / alter / ensure.

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

  5. Операции чтения и объединения ссылок (с помощью commute) никогда не блокируют другие операции, а том числе и операции записи ссылок.

  6. Операции записи не блокируют операции чтения и объединения.

  7. Внутри транзакций необходимо избегать операций ввода/вывода, а также других действий, имеющих подобные "скрытые последствия", так как транзакции могут повторно исполняться. Макрос io! может быть использован, чтобы запретить выполнять подобные операции внутри транзакций.

  8. Если условие корректности значения одной ссылки зависит от значения другой и при этом первая изменяется внутри транзакции, а вторая - нет, то вторая ссылка может быть "защищена" от изменений с помощью ensure. Защищенные таким образом ссылки не будут модифицироваться другими транзациями (пункт 3) и их значение не будет изменено в точке записи транзакции (пункт 2).

  9. Реализация MVCC STM, используемая в Clojure создана для работы с персистентными коллекциями. Настоятельно рекомендуется использовать коллекции Clojure в качестве значений ссылок. Так как высока вероятность повторного исполнения транзакций, крайне необходима как можно более низкая цена копий и модификаций. Персистентные коллекции имеют "бесплатные" копии (оригинал просто переиспользуется, так как все равно не может быть изменен) и модификации коллекций эффективно переиспользуют структуры данных. В любом случае:

  10. Значения, хранящиеся с ссылках, обязяны быть или рассматриваться как неизменяемые!! Иначе Clojure не поможет вам.

Пример

В этом примере создается вектор ссылок на вектора, каждый из которых содержит изначально последовательный набор уникальных чисел. Затем запускается множество потоков, которые несколько раз выбирают две случайных позиции в двух случайных векторах и меняют содержимое местами внутри транзакции. При этом не предпринимается никаких дополнительных усилий по предотвращению неизбежных конфликтов, кроме использования транзакций.

(defn run [nvecs nitems nthreads niters]
  (let [vec-refs (vec (map (comp ref vec)
                           (partition nitems (range (* nvecs nitems)))))
        swap #(let [v1 (rand-int nvecs)
                    v2 (rand-int nvecs)
                    i1 (rand-int nitems)
                    i2 (rand-int nitems)]
                (dosync
                 (let [temp (nth @(vec-refs v1) i1)]
                   (alter (vec-refs v1) assoc i1 (nth @(vec-refs v2) i2))
                   (alter (vec-refs v2) assoc i2 temp))))
        report #(do
                 (prn (map deref vec-refs))
                 (println "Distinct:"
                          (count (distinct (apply concat (map deref vec-refs))))))]
    (report)
    (dorun (apply pcalls (repeat nthreads #(dotimes [_ niters] (swap)))))
    (report)))

При выполнении мы увидим, что ни одно значение не было потеряно или продублировано при перемешивании:

(run 100 10 10 100000)

([0 1 2 3 4 5 6 7 8 9] [10 11 12 13 14 15 16 17 18 19] ...
 [990 991 992 993 994 995 996 997 998 999])
Distinct: 1000

([382 318 466 963 619 22 21 273 45 596] [808 639 804 471 394 904 952 75 289 778] ...
 [484 216 622 139 651 592 379 228 242 355])
Distinct: 1000

Функции

Создать ссылку: ref

Получить значение ссылки: deref (см. также макрос считывателя @)

Макросы транзакций: dosync io!

Разрешены только внутри транзакций: ensure ref-set alter commute

Валидаторы ссылок: set-validator! get-validator