2024 年 Clojure 状态调查中分享您的想法!

欢迎!请查看关于页面以获取更多有关此功能的信息。

0
Clojure
我们使用 Clojure 打“规则引擎”。每个函数代表一条规则,元数据描述规则并提供规则本身的某些静态配置。该系统是不可变的和并发的。

如果有两个或更多线程并发调用同一个 Var,由于 AReference#meta() 是同步的,它们将最终互相阻塞(请参见附图,红色点)。


(defn
  ^{:rule {:remote-address "127.0.0.1"}}
  example
  [request]
  (let [rule (:rule (meta #'example))]
    (= (:remote-address rule) (:remote-address request))))


*方法:* 用 rwlock 替换同步块以提高读并发性。这种方法消除了元数据读冲突(请参见评论中的实际例子)。但是,它也有一些缺点:*

* 每个 AReference 都需要额外的字段(所有命名空间、vars、atoms、refs 和 agents)*
* 将锁的构建加入到 AReference 的构建中(影响性能和启动时间)*

*补丁:* clj-1888-2.patch 用 rwlock 替换 synchronized 以提高读并发性

*替代方案:**

* 使用 volatile 来处理 _meta,并使用 synchronized 处理 alter/reset。允许在 volatile 之下读取 _meta——这是否足够安全?*
* 从 ReentrantReadWriteLock 扩展 AReference 而不是保留一个——这听起来有些奇怪,但可能对内存/构建有不同(可能更好的)影响。

4 个答案

0

评论者:alexmiller

在 alterMeta 中使用 volatile 不足以读写具有原子性的原子。

但是您可以使用 ReadWriteLock 而不是 synchronized。我附上了一个补丁,它就是这样做的——如果您有一个可复现的例子,我会很感兴趣看到它在分析器中的影响。

这可能会引起需要关注的潜在问题——这会增加每个引用的内存(锁实例),并减慢构建(锁的构建),但读并发性更高。

0
_由 rkapsi 发布的评论_

嗨 Alex,

我确实有一个可重现的例子。在应用您的补丁后,阻塞确实消失了(见附件图片)。在“WorkerThreads”上的剩余阻塞代码是 {{sun.nio.ch.SelectorImpl.select(long)}} (即与clojure无关)。

您可以通过在无限循环中同时执行以下类似代码来自行重现。


(defn
  ^{:rule {:remote-address "127.0.0.1"}}
  example
  [request]
  (let [rule (:rule (meta #'example))]
    (= (:remote-address rule) (:remote-address request))))


关于补丁的建议:将元锁设置为一个 {{final}} 字段,并可能将读写锁拉入局部变量以避免双重方法调用。


alterMeta(...)
  Lock w = _metaLock.writeLock();
  w.lock();
  try {
    // ...
  } finally {
    w.unlock();
  }
}
0

评论者:alexmiller

标记为预筛过的,

0
参考: https://clojure.atlassian.net/browse/CLJ-1888 (由 rkapsi 报告)
...