Вы пытаетесь сделать слишком много одновременно.Наличие этой единственной функции, генерирующей идентификатор и обновляющей атом, усложняет ситуацию.Я бы разбил это на три функции:
- Функция, которая генерирует идентификатор на основе существующей карты
- Функция, которая обновляет обычную неизменную карту используя вышеупомянутую функцию
- Функция, которая обновляет атом (хотя это будет настолько просто после реализации двух предыдущих функций, что это может вообще не понадобиться).
Что-то вроде:
; Notice how this doesn't deal with atoms at all
(defn generate-new-id [old-map]
(let [new-id (rand-int 25)]
(if (old-map new-id) ; or use "contains?"
(recur old-map) ; Using "recur" so we don't get a StackOverflow
new-id)))
; Also doesn't know anything about the atom
(defn assoc-new-id [old-map]
(let [new-id (generate-new-id old-map)]
(assoc old-map new-id "")))
(defonce data (atom {}))
(defn swap-new-id! []
(swap! data assoc-new-id))
Основные изменения:
Все, что можно было удалить из логики обмена атомами, было перемещено всвоя функция.Это позволяет просто передать функцию, обрабатывающую всю логику, в swap!
, и она будет обработана атомарно.
Clojure использует регистр, а не camelCase.
Я использовал recur
вместо реальной рекурсии, так что вы не получите StackOverflow
, пока ID брут-форсированный.
Конечно, конечно,Это страдает от проблем, если доступное количество идентификаторов осталось мало.Это может занять много времени, чтобы он "нашел" доступный идентификатор с помощью грубой силы.Возможно, было бы лучше использовать «генератор», поддерживаемый atom
, для получения идентификаторов, атомарно начиная с 0:
(defn new-id-producer []
(atom -1))
(defn generate-id [producer]
(swap! producer inc)) ; "swap!" returns the new value that was swapped in
(let [producer (new-id-producer)]
; Could be run on multiple threads at once
(doseq [id (repeatedly 5 #(generate-id producer))]
(println id)))
0
1
2
3
4
=> nil
Я попытался написать пример этой операции одновременно для нескольких потоков:
(let [producer (new-id-producer)
; Emulate the "consumption" of IDs
consume (fn []
(doseq [id (repeatedly 20 #(generate-id producer))]
(println (.getId (Thread/currentThread)) id)))]
(doto (Thread. consume)
(.start))
(doto (Thread. consume)
(.start)))
37 0
3738 1
38 3
38 4
38 5
38 6
38 7
38 8
38 9
38 10
38 11
38 12
38 13
38 14
38 15
38 16
38 17
38 18
38 19
38 20
38 21
2
37 22
37 23
37 24
37 25
37 26
37 27
37 28
37 29
37 30
37 31
37 32
37 33
37 34
37 35
37 36
37 37
37 38
37 39
Но несинхронизированный характер печати с опережением сделал этот вывод беспорядочным.Если вы немного щуритесь, вы можете видеть, что потоки (с идентификаторами потоков 37 и 38) сменяются.
Если вам нужен возвращенный новый идентификатор, единственный чистый способ, который я знаю оlocking
не требует использования второго атома для получения возвращенного идентификатора из функции подкачки.Это требует избавления от assoc-new-id
:
(defn generate-new-id [old-map]
(let [new-id (rand-int 25)]
(if (old-map new-id)
(recur old-map)
new-id)))
(defn swap-new-id! [old-map]
(let [result-atom (atom nil)]
(swap! data (fn [m]
(let [id (generate-new-id m)]
(reset! result-promise id) ; Put the ID in the result atom
(assoc m id ""))))
@result-promise)) ; Then retrieve it here
Или, если очень неэффективное решение подходит и вы используете Clojure 1.9.0, вы можете просто найти карты, чтобы найти, какой ключ был добавлен с помощьюclojure.set.difference
:
(defn find-new-id [old-map new-map]
(clojure.set/difference (set (keys new-map))
(set (keys old-map))))
(defn swap-new-id! []
(let [[old-map new-map] (swap-vals! data assoc-new-id)] ; New in 1.9.0
(find-new-id new-map old-map)))
Но, опять же, это очень неэффективно.Требуется две итерации каждой карты.