Clojure - 有效且同时增加列表中的数字

时间:2014-01-08 23:26:32

标签: multithreading performance data-structures concurrency clojure

简短版本:在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个元素。

5 个答案:

答案 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进行矢量操作。
  • 使用可变的向量实现,也许是vectorz-clj(假设双精度数对你来说没问题?)
  • 使用(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!然后我没有看到问题。

相关问题