Как атомарно проверить, существует ли ключ на карте, и добавить его, если он не существует - PullRequest
0 голосов
/ 27 декабря 2018

Я пытаюсь сгенерировать новый ключ, которого нет на моей карте (атом), затем немедленно добавить его на мою карту и вернуть ключ.Однако проверка ключа и обновление не выполняются атомарно.Мне интересно, как сделать это атомарно, чтобы он был безопасен для параллелизма.

В идеале этот ключ достаточно короткий, чтобы его можно было набирать, но его трудно угадать (чтобы пользователь мог создать сеанс, и его / ее друзья могли присоединиться к нему).с ключом).Таким образом, 0,1,2,3 ... не идеально, так как пользователь может попытаться ввести сеансы n-1.Что-то вроде UUID, где мне не нужно беспокоиться о столкновениях, тоже не идеально.Я планировал сгенерировать короткую случайную строку (например, «udibwi»), но я использовал rand-int 25 в фрагменте кода ниже, чтобы упростить проблему.

Я написал функцию, которая случайным образом генерирует ключ,Он проверяет, содержит ли карта это.Если это уже так, попробуйте новый ключ.Если это не так, свяжите его с моей картой, а затем верните ключ.

Это работает, но я не думаю, что это безопасно для нескольких потоков.Есть ли способ сделать это с помощью атомов или есть лучший способ?

(defonce sessions (atom {}))

(defn getNewSessionId []
  (let [id (rand-int 25)]
    (if (contains? @sessions id) 
      (createNewId) 
      (do 
        (swap! sessions assoc id "")
        id))))

Ответы [ 3 ]

0 голосов
/ 27 декабря 2018

Не могли бы вы обновить свой вопрос, указав причину, по которой вы пытаетесь это сделать?Почти наверняка есть лучшие решения, чем те, которые вы предлагаете.

Если вы действительно хотите сгенерировать уникальные ключи для карты, есть два простых ответа.

(1) Для согласованных ключей выможет использовать atom для хранения целого числа последнего сгенерированного ключа.

(def last-map-key (atom 0))
(defn new-map-key (swap! last-map-key inc))

, который гарантированно генерирует уникальные новые ключи карты.

(2) Для несогласованных ключей используйте UUID , как с clj-uuid/v1

(3) Если вы действительно настаиваете на своем первоначальном алгоритме, вы можете использовать Clojure ref, но это злоупотребление его назначением.

0 голосов
/ 29 декабря 2018

Вы также можете хранить информацию о том, какой идентификатор был последним в атоме.

(defonce data
  (atom {:sessions {}
         :latest-id nil}))

(defn generate-session-id [sessions]
  (let [id (rand-int 25)]
    (if (contains? sessions id)
      (recur sessions)
      id)))

(defn add-new-session [{:keys [sessions] :as data}]
  (let [id (generate-session-id sessions)]
    (-> data
        (assoc-in [:sessions id] {})
        (assoc :latest-id id))))

(defn create-new-session! []
  (:latest-id (swap! data add-new-session)))

Как показывает Carcigenicate, с помощью swap-vals! он выводится из состояний до и после, нопроще просто держать рядом.

0 голосов
/ 27 декабря 2018

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

  • Функция, которая генерирует идентификатор на основе существующей карты
  • Функция, которая обновляет обычную неизменную карту используя вышеупомянутую функцию
  • Функция, которая обновляет атом (хотя это будет настолько просто после реализации двух предыдущих функций, что это может вообще не понадобиться).

Что-то вроде:

; 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)))

Но, опять же, это очень неэффективно.Требуется две итерации каждой карты.

...