定义线程安全吗?

Kir*_*mov 10 clojure

我可以实时重新定义功能而不会产生副作用吗?定义线程安全吗?

Art*_*ldt 14

"线程安全,足以开发,而不是用于生产."

使用defn重新定义函数可能会破坏调用它的函数,如果它们在调用更改时正在运行.它在开发中是可以的,因为你可以在它休息后重新启动.如果您可以控制何时调用您正在更改的功能,那么它就足够安全了.

defn 是一个宏,解决了类似的东西

(def name (fn [args] (code-here)))
Run Code Online (Sandbox Code Playgroud)

所以它创建了一个函数的实例,然后将它放入var的根绑定中.vars是一个可变数据结构,允许每线程值.所以当你调用defn时,它会分配所有线程都会看到的基值.如果另一个线程然后将var更改为指向某个其他函数,它将更改它的副本而不影响任何其他线程.所有旧线程仍然会看到旧副本

当您通过def再次调用(通过defn宏)重新绑定var的根值时,您将更改未设置其自身值的每个线程将看到的值.决定设置自己的值的线程将继续看到它们自己设置的值,而不必担心从它们下面改变的值.


单线程没有比赛

当进行函数调用时,使用具有函数名称的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
Run Code Online (Sandbox Code Playgroud)

两个线程,仍然没有比赛

然后我们运行另一个线程并在该线程中重新定义foo以返回12

user=> (.start (Thread. (fn [] (binding  [foo (fn [] 12)] (println (bar))))))
nil
user=> 12
Run Code Online (Sandbox Code Playgroud)

foo的值(由bar看到)在第一个线程(运行repl的线程)中仍然没有变化

user=> (bar)
6
user=> 
Run Code Online (Sandbox Code Playgroud)

两个线程和一个竞争条件

接下来,我们将从没有本地绑定的线程下更改根绑定的值,并看到函数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
Run Code Online (Sandbox Code Playgroud)

如果对foo的更改(间接调用)改变了参数的数量,那么这将是崩溃而不是错误的答案(这可能更好).在这一点上,很明显,如果我们想要使用变量和devn来改变我们的函数,那么需要做一些事情.


如何使用没有竞争条件的变种

您真的可能希望函数不会更改中间调用,因此您可以使用线程本地绑定来保护您的自我,方法是更改​​新线程中运行的函数以将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
Run Code Online (Sandbox Code Playgroud)

神奇的是在表达式中,(binding [foo foo] (code-that-uses-foo))这可以被读作"将一个线程本地值赋给foo当前值的foo",这样它就会保持一致,直到绑定表单结束并进入从该绑定表单调用的任何内容.


Clojure为您提供选择,但您必须选择

vars足以保存你的功能,并在开发代码时重新定义它们的内容.使用代码在使用vars的已部署系统上快速自动重新定义函数将不太明智.并不是因为vars不是线程安全的,而是因为在这种情况下vars是一个错误的可变结构来保存你的函数.Clojure对于每个用例都有可变的结构,并且在快速自动编辑需要通过事务运行保持一致的函数的情况下,您最好在refs中保存函数.还有哪种语言可以让您选择保留您职能的结构!*

  • 不是一个真正的问题,任何功能语言都可以做到这一点


mik*_*era 10

是的,它是线程安全的....但它确实有副作用.因此,根据您的尝试,您可能会得到意想不到的结果.

实质上,对现有函数进行定义将重新绑定命名空间中的相应var.

这意味着:

  • 将来访问var将获得该函数的新版本
  • 以前从var读取的旧函数的现有副本不会更改

只要你理解并对此感到满意 - 你应该没问题.

编辑:回应亚瑟的评论,这是一个例子:

; 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
Run Code Online (Sandbox Code Playgroud)