Является ли defn потокобезопасным?

Могу ли я переопределить функцию в режиме реального времени без побочных эффектов? Является ли defn потокобезопасным?

Ответ 1

", достаточно безопасной для разработки, а не для использования в производстве.

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

defn - макрос, который разрешает somthing как

(def name (fn [args] (code-here)))

поэтому он создает экземпляр функции, а затем помещает его в корневую привязку var, vars - изменяемая структура данных, позволяющая значениям в потоке. поэтому, когда вы вызываете defn, который назначает базовое значение, которое будут видеть все потоки. если другой поток затем изменил var, чтобы указать на какую-то другую функцию, он бы изменил бы его копию, вне зависимости от каких-либо других потоков. все старые потоки все равно будут видеть старую копию

Когда вы повторно привязываете корневое значение var, снова вызывая def (через макрос defn), вы изменяете значение, которое будет видеть каждый поток, который не установил его собственное значение. потоки, которые решили установить свои собственные значения, будут продолжать видеть значение, которое они сами устанавливают, и не должны беспокоиться о том, что значение будет изменено из-под них.


одиночная нить без гонки

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

user=> (defn foo [] 4)
#'user/foo
user=> (defn bar [] (foo))
#'user/bar
user=> (bar)
4
user=> (defn foo [] 6)
#'user/foo
user=> (bar)
6

две нити, по-прежнему нет гонки

тогда мы запускаем другой поток и в этом потоке переопределяем foo, чтобы возвращать 12 вместо

user=> (.start (Thread. (fn [] (binding  [foo (fn [] 12)] (println (bar))))))
nil
user=> 12

значение foo (как видно на панели) по-прежнему остается неизменным в первом потоке (тот, который выполняет repl)

user=> (bar)
6
user=> 

два потока и состояние гонки

Далее мы изменим значение корневой привязки из нити без локальной привязки и увидим, что значение функции foo изменяется на полпути через функцию, запущенную в другом потоке:

user=> (.start (Thread. (fn [] (println (bar)) 
                        (Thread/sleep 20000) 
                        (println (bar)))))                        
nil
user=> 6                ;foo at the start of the function

user=> (defn foo [] 7)  ;in the middle of the 20 seond sleep we redefine foo
#'user/foo
user=> 7                ; the redefined foo is used at the end of the function

Если изменение foo (которое называется косвенно) изменило количество аргументов, это было бы ошибкой вместо неправильного ответа (что, возможно, лучше). На данный момент достаточно ясно, что нужно что-то делать, если мы хотим использовать vars и devn для изменения наших функций.


как использовать vars без условия гонки

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

user=> (.start (Thread. (fn [] (binding [foo foo] (println (bar)) 
                                                  (Thread/sleep 20000)
                                                  (println (bar))))))
nil
user=> 7

user=> (defn foo [] 9)
#'user/foo
user=> 7

Магия находится в выражении (binding [foo foo] (code-that-uses-foo)), это может быть прочитано как "назначить локальное значение потока для foo текущего значения foo" таким образом, чтобы он оставался последовательным до конца формы привязки и во все, что называется из этой обязательной формы.


Clojure дает вам выбор, но вы должны выбрать

vars достаточно хороши, чтобы удерживать ваши функции и переопределять их до вашего сердца при разработке кода. использование кода для автоматического переопределения функций очень быстро в развернутой системе с использованием vars было бы менее разумным. Не потому, что vars не являются потокобезопасными, а потому, что в этом контексте vars являются неправильной изменчивой структурой для хранения вашей функции. Clojure имеет изменчивую структуру для каждого варианта использования, а в случае быстрого автоматизированного редактирования функций, которые должны оставаться согласованными с помощью транзакции, вам лучше было бы удерживать функции в ссылках. Какой другой язык позволяет вам выбрать структуру, в которой хранятся ваши функции! *

  • Не настоящий вопрос, и любой функциональный язык может это сделать

Ответ 2

Да, это потокобезопасно.... но у него есть побочные эффекты. Следовательно, вы можете получить неожиданные результаты в зависимости от того, что вы пытаетесь сделать.

По сути, defn на существующей функции будет перепроверять соответствующий var в пространстве имен.

Это означает, что:

  • Будущие обращения к var будут получать новую версию функции
  • Существующие копии старой функции, которые ранее были прочитаны из var, не будут меняться

Пока вы понимаете и им комфортно, вы должны быть в порядке.

EDIT: в ответ на комментарий Артура, вот пример:

; original function
(defn my-func [x] (+ x 3))

; a vector that holds a copy of the original function
(def my-func-vector [my-func])

; testing it works
(my-func 2)
=> 5
((my-func-vector 0) 2)
=> 5

; now redefine the function
(defn my-func [x] (+ x 10))

; direct call to my-func uses the new version, but the vector still contains the old version....
(my-func 2)
=> 12
((my-func-vector 0) 2)
=> 5