Как вы используете агент и core.async для правильной асинхронной регистрации в Clojure?

В следующем коде я использую цикл go, который прослушивает канал и записывает прочитанные значения агенту. Затем в том же цикле функция записи в файл должна записывать каждый элемент агента в файл. Однако вывод в файл необычен, так как агент с [8 6 5 13] может передать свое содержимое в файл как 865n13. Если я удаляю вызов журнала в файл из функции регистратора и вызываю его отдельно, иногда вывод файла в порядке, иногда нет. Может ли кто-нибудь просто объяснить мне, что происходит, и, возможно, показать мне безопасный способ сделать это, демонстрирующий последовательное поведение? Я знаю, что отказ от использования агента, вероятно, был бы лучшим способом добиться согласованного поведения. Я просто хочу знать, в духе обучения, чего можно и чего нельзя достичь с помощью агентов. Заранее спасибо!

(defn log-to-file [file] (for [i (deref log)] (spit file (str "\n" i) :append true)))

(defn logger [file]
  (go 
   (loop []
     (when-let [v (<! print-chan)]
       (send log conj v)
       (log-to-file file)
       (recur)))))

;; testing it out

(def log (agent []))
(>!! print-chan 5)
(logger "resources/test.txt")
(deref log)

РЕДАКТИРОВАТЬ:

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

(def ch (chan))
(def a (agent []))

(defn double-async-print []
    (go-loop []
       (when-let [x (<! ch)]
         (send a conj x)
         (print (clojure.string/join "\n" @a)))
     (recur)))

(go (doseq [n (range 10)] (>! ch n)))

(double-async-print) 

    ;=>jsync 0.1.0-SNAPSHOT[stdout]:    
    00
    10
    1
    20
    1
    2
    30
    1
    2
    3
    40
    1
   ...

person kurofune    schedule 13.05.2014    source источник


Ответы (1)


Я подозреваю, что происходит ряд вещей, которые взаимодействуют между собой, чтобы вызвать ваше замешательство. Проблема не в использовании агента внутри go-loop.

send является асинхронным, поэтому нет гарантии, что ваш вызов deref в log-to-file увидит изменение во время вызова log-to-file внутри цикла.

Также вы неправильно используете for с побочным кодом (файл io с spit). Похоже, вы пытаетесь использовать его аналогично императивному циклу for-each в других языках. doseq — правильная конструкция clojure для обработки последовательности побочных эффектов (см.: Разница между дозойq и for в Clojure).

Вы также ничего не делаете для удаления содержимого вектора, создаваемого в агенте, поэтому, даже если все работает синхронно, вы, например, добавляете :a, :b и :c один за раз вывод в файл, например:

:a
:a
:b
:a
:b
:c

Из вопроса не ясно, ожидаете ли вы такого поведения от своего кода.


В стороне от clojure.string/join:

Как правило, использование spit, как указано выше, очень неэффективно, даже если вы правильно использовали doseq, если вы просто хотите вывести вектор или другую коллекцию в файл, разделенный новой строкой. Гораздо лучше использовать clojure.string/join, например.

user>  (print (clojure.string/join "\n" [:a :b :c :d]))
:a
:b
:c
:d

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

user> (clojure.pprint/pprint (clojure.string/join "\n" [:a :b :c :d]))
":a\n:b\n:c\n:d"
person Alex Stoddard    schedule 13.05.2014
comment
Спасибо, Алекс, ваш ответ действительно полезен. Я забыл, что вы не должны использовать for с побочным кодом. и после этого я сделаю обновление вектора агента. Последний запрос, не могли бы вы отредактировать свой ответ, чтобы показать пример того, как clojure.string/join сделает запись более эффективной? - person kurofune; 13.05.2014
comment
Сможет сделать. Между прочим, for никогда не должен был вызывать никаких побочных эффектов в неинтерактивной среде, потому что for производит ленивую последовательность. Я вижу из вашего другого вопроса что вы используете LightTable. Встроенная оценка LT, вероятно, частично объясняет, почему она почти работает. - person Alex Stoddard; 13.05.2014
comment
Также похоже, что вы пытаетесь использовать агент, чтобы заставить изменяемое состояние и императивный цикл вернуться в ваш код. Это во многом анти-шаблон в clojure. Помимо простого экспериментирования, я не вижу веских причин для желания или необходимости смешивать два разных асинхронных механизма в одном логическом цикле. - person Alex Stoddard; 13.05.2014
comment
Спасибо за все ваши отзывы. Я просто экспериментирую, но все еще на стадии, когда я не знаю, что идиоматично для реальных приложений. Однако я не могу сказать из вашего примера, как clojure.string/join записывает внешний файл. Или вы говорите, что это неэффективная часть? - person kurofune; 14.05.2014
comment
Ты прав. Я не показываю запись во внешний файл, но печать эквивалентна одной записи (под капотом он вызывает append для java.io.PrintWriter с именем out в repl.) Создание одного string для вывода в StringBuffer (используемый clojure.string/join) намного лучше, чем повторение коллекции и вызов str несколько раз. - person Alex Stoddard; 14.05.2014
comment
Право на. Я переписал код в редактировании в моем сообщении выше. Пожалуйста, взгляните и скажите, ясно ли я понял вашу точку зрения. - person kurofune; 15.05.2014
comment
Вы получили очки с дозой и соединением строк. Я не знаю, правильно ли вы поняли send асинхронность агента. Агенты полностью ортогональны механизмам core.async, блок go ничего не делает для синхронизации использования агентов. send не блокируется, и функция обновления агента (просто conj в вашем примере) запускается в отдельном потоке в неопределенное время. Deref @a в цикле go, даже если кажется, что это происходит после send лексически, может произойти до обновления в хронологическом порядке. - person Alex Stoddard; 15.05.2014
comment
Хорошо, теперь все сходится. Я понимаю, что асинхронность агента подразумевает другие потоки и отсутствие хронологической гарантии. Я определенно должен больше узнать о блокировке / неблокировке и т. Д. Спасибо за всю вашу помощь. Вы, вероятно, скоро увидите мой следующий вопрос здесь ;) - person kurofune; 15.05.2014