【问题标题】:Clojure mutable storage typesClojure 可变存储类型
【发布时间】:2010-11-04 22:13:50
【问题描述】:

我正在尝试从网站上提供的 API 和文档中学习 Clojure。我对 Clojure 中的可变存储有点不清楚,我想确保我的理解是正确的。如果我有任何错误的想法,请告诉我。

编辑:我正在更新它,因为我收到了关于其正确性的 cmets。


免责声明:所有这些信息都是非正式的,并且可能是错误的。不要使用这篇文章来了解 Clojure 的工作原理。


Vars 总是包含一个根绑定,也可能包含一个线程绑定。它们与命令式语言中的常规变量相当,不适合在线程之间共享信息。 (感谢 Arthur Ulfeldt)

Refs 是在支持原子事务的线程之间共享的位置,这些原子事务可以更改单个事务中任意数量的 refs 的状态。在退出同步表达式(dosync)时提交事务,并使用 STM 魔法(回滚、队列、等待等)自动解决冲突

代理是通过调度独立的动作函数来改变代理的状态,使信息能够以最小的开销在线程之间异步共享的位置。代理会立即返回,因此是非阻塞的,尽管在分派的函数完成之前不会设置代理的值。

原子是可以在线程之间同步共享的位置。它们支持不同线程之间的安全操作。

以下是我基于何时使用这些结构的友好总结:

  • Var 类似于命令式语言中的常规旧变量。 (尽可能避免)
  • Atom 与 Vars 类似,但具有线程共享安全性,允许立即读取和安全设置。 (感谢马丁)
  • Agent 类似于 Atom,但它不会阻塞它,而是生成一个新线程来计算其值,只有在更改值的过程中才会阻塞,并且可以让其他线程知道它已完成分配。
  • Refs 是在事务中锁定自身的共享位置。我们无需让程序员为每段锁定代码决定竞争条件下会发生什么,而是启动一个事务并让 Clojure 处理该事务中 ref 之间的所有锁定条件。

另外,一个相关的概念是函数future。对我来说,future 对象似乎可以被描述为一个同步代理,其中在计算完成之前根本无法访问该值。它也可以被描述为一个非阻塞的 Atom。这些是对未来的准确概念吗?

【问题讨论】:

    标签: multithreading clojure future mutable


    【解决方案1】:

    我认为你关于 Atoms 的结论是错误的:

    Atom 与 Vars 类似,但具有线程共享安全性,在值更改之前会阻塞

    swap! 更改原子或用compare-and-set! 更改低级原子。这永远不会阻止任何东西。 swap! 就像只有一个参考的交易:

    1. 旧值取自原子并存储在线程本地
    2. 该函数应用于旧值以生成新值
    3. 如果成功,则使用旧值和新值调用比较和设置;仅当原子的值没有被任何其他线程更改(仍然等于旧值)时,才写入新值,否则操作在 (1) 处重新开始,直到最终成功。

    【讨论】:

    • 啊,我想你是对的,没有阻塞。在那种情况下,我想知道他们说 Atom 是同步的是什么意思。
    • swap 与交易不太像,因为它们不与其他交易组合 - 如果您使用 swap,请小心!在一个事务中,因为它不是事务安全的,特别是它可能会被重试多次或干扰另一个正在运行的事务。
    【解决方案2】:

    Var 并不总是有根绑定。使用

    创建没有绑定的 var 是合法的
    (def x)
    

    (declare x)
    

    尝试在 x 有值之前对其求值将导致

    Var user/x is unbound.
    [Thrown class java.lang.IllegalStateException]
    

    【讨论】:

      【解决方案3】:

      Martin 说 Atoms 操作从 1 点重新开始是正确的。直到最终成功。 它也称为旋转等待。 值得注意的是,在锁上确实阻塞了执行操作的线程被阻塞,直到操作成功,所以它是阻塞操作而不是异步操作。

      关于 Futures,Clojure 1.1 增加了对 Promise 和 Futures 的抽象。 Promise 是一种同步构造,可用于将值从一个线程传递到另一个线程。在传递该值之前,任何取消引用该承诺的尝试都将被阻止。

      (def a-promise (promise))
      (deliver a-promise :fred)
      

      Future 代表异步计算。它们是一种让代码在另一个线程中运行并获得结果的方法。

      (def f (future (some-sexp)))
      (deref f) ; blocks the thread that derefs f until value is available
      

      【讨论】:

        【解决方案4】:

        我发现你的问题有两个问题。

        你说:

        如果在操作发生时访问代理,则在操作完成之前不会返回值

        http://clojure.org/agents 说:

        Agent 的状态总是可以立即被任何线程读取

        即您永远不必等待获取代理的值(我假设由操作更改的值是代理并自动更改的)。

        Agentderef 方法的代码如下所示(SVN 修订版 1382):

        public Object deref() throws Exception{
            if(errors != null)
            {
                throw new Exception("Agent has errors", (Exception) RT.first(errors));
            }
        return state;
        

        }

        不涉及阻塞。

        另外,我不明白你的意思(在你的参考部分)

        事务在调用 deref 时提交

        当 dosync 块的所有操作都已完成,没有抛出异常并且没有任何事情导致事务被重试时,事务被提交。我认为deref与它无关,但也许我误解了你的意思。

        【讨论】:

        • 嗨,pmf,我觉得您对代理感到困惑。 Agent 的 state 总是立即可用,但在调用 send 后 action 函数完成时,Clojure 会异步更改它的值。在我的解释中,我只提到了代理调度的最后一步,如下所示:“5. 如果在函数执行期间进行了任何其他调度(直接或间接),它们将一直保持到代理的状态已经被变了。”这是这里发生的唯一阻塞。
        • 我关于提交事务的陈述是根据我从 Rich Hickley 那里读到的幻灯片从内存中得出的,尽管我可能错了。对我来说,在 dosync 之后提交也更有意义。
        【解决方案5】:

        听起来你真的在使用 Clojure!干得好:)

        Var 有一个在所有线程中可见的“根绑定”,每个单独的线程都可以更改它看到的值,而不会影响其他线程。如果我的理解是正确的,那么一个 var 不能只存在于一个线程中而没有一个对所有人可见的根绑定,并且在第一次使用 (def ... ) 定义它之前它不能“反弹”。

        引用在包含更改的 (dosync ... ) 事务结束时提交,但仅在事务能够以一致状态完成时提交。

        【讨论】:

        • 您对这些更正的解释非常清楚,谢谢!根绑定的想法让我有点困惑,因为我看不到它有任何优势。我认为这更多是出于技术原因,例如将可变变量保留在堆栈之外,这样它们就不会被释放。
        猜你喜欢
        • 1970-01-01
        • 2015-07-16
        • 2021-08-01
        • 2019-02-19
        • 2018-09-23
        • 1970-01-01
        • 1970-01-01
        • 2016-10-18
        • 1970-01-01
        相关资源
        最近更新 更多