软件事务内存,包含大量共享数据

时间:2012-03-03 15:37:33

标签: haskell stm

原始问题

我是STM的新手。我想在Haskell中做的一件事涉及大量数据,以及大量轻量级线程读取和写入所述大数据的一小部分。读取和写入的位置可以被认为是基本上随机和小的。 STM看起来很棒,但我对如何解决这个问题有一些疑问。我看到了几种可能的方法,每种方法都有一些缺点,有些方法看起来很愚蠢。关于这些或其他替代方案的一些意见将不胜感激。

为简单起见,假设大数据是Data.Vector a,其中元素本身很小。

  1. 将整个数据作为一个TVar (Vector a) 。我想这将导致大量数据的复制,因为STM会认为每个单独的写入可能会影响整个共享数据。当然STM确定读写是非常本地化的,并且不需要大数据的一致性就没有什么神奇之处了吗?

  2. 大量TVar a s ,基本上每个元素一个,提供完全本地化的STM,但基本上复制了整个Vector a。这看起来很愚蠢。

  3. 通过对数据进行分段来实现1到2 之间的折衷,以便我有一个与数据子向量对应的合理数量的TVar (Vector a) s。我觉得这个解决方案过分依赖于启发式方法,比如细分应该有多大。

  4. 消息传递。而不是每个工作人员使用STM读取和写入数据,每个人都使用请求读取数据块来写消息要通过某种STM机制写入的数据,例如TChan。特殊线程接收这些消息,传递通过另一个TChan请求的数据,或者接收数据并将其写入共享数据结构。这个解决方案似乎没有困扰解决方案1-3的问题,但在我看来,它基本上放弃了使用STM的细节来保持数据的一致性。相反,它只是消息传递。当然,消息传递部分是用STM实现的,但我的真正问题是通过消息传递解决的。 STM似乎很棒,消息传递是如此...... meh。

  5. 我是否正确地考虑过这个问题?有人有任何提示或其他建议吗?

    请记住,我没有使用STM的经验,也没有尝试过上述解决方案。我会离开扶手椅,但有时在尝试任何事情之前考虑这些事情会很好。

    附录:第五种方法来自Nathan Howell并使用TArray。这听起来像我想要的,但documentation说:

      

    目前它是作为Array ix(TVar e)实现的,但它可能会被未来更高效的实现所取代(但接口将保持不变)。

    我认为这意味着TArray只是我穿着更好衣服的第2号方法。暗示“更有效”实施的文档很有意思,因为它暗示实际上有更好的方法。

    Vagif Verdi回答的后续行动

    Vagif Verdi的answer非常有趣,所以我做了little experiment来试试。我现在没有时间减少代码,所以对此感兴趣的人将不得不承担代码而不仅仅包含基本要素。我决定使用一个带有10 ^ 8 Int s的可变向量作为“大共享数据”,让多个读者/写入者对应于网络套接字上的线程。

    请注意,the code甚至不会读取或写入共享数据。它就在那里,每个线程都有一个TVar

    那会发生什么?我运行程序,并立即占用大约780 MB的RAM,这是预期的(它大致是10 ^ 8 Int需要的)。现在,如果我使用netcat连接几个客户端并编写一些文本,程序应该打印出来,甚至不写入共享数据,那么进程的CPU使用率会高达100%,持续时间超过一秒!在显示文本之前有明显的延迟。从好的方面来看,内存使用率保持不变(根据Vagif Verdi的回答)。如果我删除向量和TVar,即取出所有STM和共享数据,一切都非常快速和响应,并且每当客户端写入内容时,没有明显的CPU使用率。

    所以,虽然很高兴看到共享数据实际上并不重复(虽然我认为我应该实际写入共享数据以便完全验证),但是维护连贯性会导致非常严重的性能损失州。对我来说,我的问题仍然是:如何在保持STM的细节的同时正确地解决这个问题?

    感谢Vagif Verdi提出了一些非常有趣的观点。

3 个答案:

答案 0 :(得分:5)

STM并不神奇。如果你有一个巨人TVar,它将不得不在任何给定时间阻止所有线程,但是一个。这相当于“粗粒度锁定”,并且具有易于使用的优点,但STM的全部要点是不要强制使用粗粒度锁定。实际上,只有一个线程可以使用这种方法一次写入。您的“消息传递”系统也是如此 - 一个线程是限制可扩展性的瓶颈。

我不认为使用TVar数组有一个大问题,我不知道你为什么把你的方法2描述为“愚蠢”。这正是STM发明的目的。

编辑:我鼓励感兴趣的各方观看this视频,或者至少是视频的开头,以讨论STM的一些动机。它已经有几年的历史了,交易提升的内容并不是真正相关的,但是Herlihy非常出色,也是计算机科学家之一,即使不是你的事情,也能让这个领域变得有趣。

答案 1 :(得分:3)

首先,Vector是一个不可变的数据结构。每当你“修改”Vector时,你就会创建一个全新的副本,这意味着每次修改都需要O(n)时间(其中n是向量的长度)。

其次,Haskell中的值是不可变的。每次修改TVar时,都要用新值替换旧值。

我认为你想要的是一个支持高效更新的纯功能数据结构。两个例子:

  • Data.Map:键值字典。这类似于C ++中的std::map

  • Data.Sequence:像一个可变阵列,但更好。

每次“修改”其中一个数据结构时,实际上都在构建一个新值,该值在内部指向旧值的一部分。

一对准则:

  • 如果您只修改单个值,atomicModifyIORef可能就足够了。如果除了原子更新之外还需要更复杂的同步,STM将是更好的选择。

  • 使用可变变量时要注意懒惰。每次修改共享状态时,请务必强制它。这可以使用seq完成。更方便的方法是使用bang patterns。例如:

    !x <- atomically $ do
        x <- readTVar shared_state
        writeTVar shared_state (changeSomething x)
        return x
    

    这会在事务完成后强制x进行评估。如果变量(IORefSTRefTVar等)被修改了很多次但从未被强制过,那么thunk会堆积在内存中。评估生成的thunk甚至可能产生堆栈溢出。

如果您的程序需要大规模并行(即,多个内核/ CPU同时访问变量),更新这样的单个值可能效率较低,因为计算可能在处理器之间重复。但是,对于少量核心,重复是非常罕见的。

答案 2 :(得分:1)

Haskell有多个可变数组/向量的实现。 所以你可以使用最简单的方法TVar(Vector a),而不用担心复制开销(在可变数组中没有复制)

这是一个这样的库:http://hackage.haskell.org/package/vector-0.9.1