简短版本:在Clojure中存储数百个数字列表的正确方法是什么,每个数字增加数百万次(可能跨越多个线程)?
长版本:程序以空向量开始,其中每个值初始化为0:
[0 0 0 0 0 0 0 0 0 ...]
然后逐行读取数百万行文件。在对一行执行一些任意计算之后,程序会增加向量中的一些值。在第一行之后,向量可能看起来像:
[1 1 1 2 0 1 0 1 1 ...]
第二行之后:
[2 2 3 2 2 1 0 2 2 ...]
在大约5000行后,它可能看起来像:
[5000 4998 5008 5002 4225 5098 5002 5043 ...]
由于Clojure的数据结构是不可变的,简单地使用assoc
来增加向量中的值似乎非常浪费,因为将为每个增量复制整个向量。
在不花费我所有CPU时间复制不可变数据结构的情况下,执行此类并发数据聚合的正确方法是什么?我应该有一个向量,其中每个元素都像ref或atom,所有线程都递增这些共享值吗?或者,是否存在某种可以存储计数的线程级数据结构,然后最后一步是整合每个线程的计数?
这可能不会在单个线程上绑定I / O,因此我猜我将跨多个线程拆分线路处理。矢量的长度没有限制(长度可能是几千个元素),但很可能长约100个元素。
答案 0 :(得分:9)
Clojure的vector
是持久数据结构。更新向量中的元素时,它不会复制整个元素,并且基本上需要时间,这意味着O(log 32 n)。
但似乎每次迭代都会更新向量中的几乎每个元素。也许您想参考Transient Data Structures。
答案 1 :(得分:4)
一种方法是将矢量创建为原子矢量(而不是值),然后同时更新矢量中的原子。
(def len 1000)
(def vec-data (into [] (repeatedly len #(atom 0))))
;Create 10 future (threads) that update the vector atoms concurrently
(doall (for [_ (range 10)]
(future (doall (map #(swap! (vec-data %) inc) (range len) )))))
答案 2 :(得分:2)
我想知道core.matrix是否可用于此目的。它有点矫枉过正,但会使更新变得容易。如果你探索这条路线,我建议你尝试不同的实现(ndarray,vectorz和clatrix支持可变性),看看哪个是你做的最快的。
答案 3 :(得分:2)
我建议如下:
core.matrix
API进行矢量操作。(zero-vector n)
(add! accumulator ...)
对累加器向量进行可变添加(如果您担心线程安全,可以使用代理或其他并发技术来序列化这些添加)答案 4 :(得分:1)
哦,你确实同时说过,直到我发布之后我就错过了。抱歉。在我的帖子中向下滚动,看看我建议如何同时进行。
您可以在Clojure中使用java字节数组或长数组。您只需要仔细控制它们的使用方式。
例如,这里有一个素数筛子,展示了两件事。
首先它使用一个字节数组,并使用aset-byte设置数组中的字节,然后使用aget来访问这些字节。请参见第一个let语句,它设置[flags(byte-array size)]。您还可以使用(长数组大小)返回长数组。但是,您应该使用(aset-long数组索引值)来设置该长数组中的值。
其次但与你的问题没有直接关系,它使用瞬态向量(标准Clojure特性)在循环内结束时构建结果,然后在返回持久向量之前将该向量转换为持久向量。
(defn sieve1
"Generate a vector of all prime numbers up to maxN.
maxN must be 2 or greater."
[maxN]
(when (< maxN 2)
(throw (java.lang.IllegalArgumentException. (str "parameter maxN (" maxN ") must be 2 or greater."))))
(let [size (inc maxN) ; because array is zero based
;nSqrt (dbmath/isqrt maxN)
flags (byte-array size)]
;(println (format "maxN: %s; size: %s; nSqrt: %s" maxN size, nSqrt))
; Set all flags.
(loop [i 0]
(when (<= i maxN)
(aset-byte flags i 1)
(recur (inc i))))
; Strike out all non primes before two.
; (zero and one are not prime.)
(aset-byte flags 0 0)
(aset-byte flags 1 0)
; Strike out multiples of 2.
;(println "strike out multiples of two.")
(loop [j 4]
(when (<= j maxN)
;(println (format "aset %s 0" j))
(aset-byte flags j 0)
(recur (+ j 2))))
; Strike out multiples of primes (only odd primes are now remaining)
;(println "strike out multiples of primes.")
(loop [i 3]
(when (<= i maxN)
(when (= 1 (aget flags i))
; found that i is prime.
;(println (format "discovered i is prime: i=%s;" i))
; Strike out multiples of i, starting with i^2.
(loop [j (* i i)]
(when (<= j maxN)
;(println (format "aset %s 0" j))
(aset-byte flags j 0)
(recur (+ j i))))
)
(recur (+ i 2))))
; Build result.
(let [primes (transient [2])]
(loop [i 3]
(when (<= i maxN)
(when (= 1 (aget flags i))
(conj! primes i))
(recur (+ i 2))
))
(persistent! primes))
))
为什么我使用字节数组作为标志,然后使用瞬态向量来创建结果向量?让它快!所有字节数组内容和瞬态向量内容完全发生在例程中,在单个线程上,并且不会泄漏。尝试(筛选1000000)一千万,看它有多快。
同时
如果将长数组放在原子中,该怎么办?然后使用Clojure的交换!获得并发。交换!将保证一次只有一个线程用原子的新值交换原子(长数组)的内容(即使你的交换函数只能返回相同的长数组,但是在改变了一些长数据之后)数组中的值)。只要你的所有线程都有绅士同意不改变长数组,除非使用swap!然后我没有看到问题。