我是STM的新手。我想在Haskell中做的一件事涉及大量数据,以及大量轻量级线程读取和写入所述大数据的一小部分。读取和写入的位置可以被认为是基本上随机和小的。 STM看起来很棒,但我对如何解决这个问题有一些疑问。我看到了几种可能的方法,每种方法都有一些缺点,有些方法看起来很愚蠢。关于这些或其他替代方案的一些意见将不胜感激。
为简单起见,假设大数据是Data.Vector a
,其中元素本身很小。
将整个数据作为一个TVar (Vector a)
。我想这将导致大量数据的复制,因为STM会认为每个单独的写入可能会影响整个共享数据。当然STM确定读写是非常本地化的,并且不需要大数据的一致性就没有什么神奇之处了吗?
大量TVar a
s ,基本上每个元素一个,提供完全本地化的STM,但基本上复制了整个Vector a
。这看起来很愚蠢。
通过对数据进行分段来实现1到2 之间的折衷,以便我有一个与数据子向量对应的合理数量的TVar (Vector a)
s。我觉得这个解决方案过分依赖于启发式方法,比如细分应该有多大。
消息传递。而不是每个工作人员使用STM读取和写入数据,每个人都使用请求读取数据或块来写消息要通过某种STM机制写入的数据,例如TChan
。特殊线程接收这些消息,传递通过另一个TChan
请求的数据,或者接收数据并将其写入共享数据结构。这个解决方案似乎没有困扰解决方案1-3的问题,但在我看来,它基本上放弃了使用STM的细节来保持数据的一致性。相反,它只是消息传递。当然,消息传递部分是用STM实现的,但我的真正问题是通过消息传递解决的。 STM似乎很棒,消息传递是如此...... meh。
我是否正确地考虑过这个问题?有人有任何提示或其他建议吗?
请记住,我没有使用STM的经验,也没有尝试过上述解决方案。我会离开扶手椅,但有时在尝试任何事情之前考虑这些事情会很好。
附录:第五种方法来自Nathan Howell并使用TArray
。这听起来像我想要的,但documentation说:
目前它是作为Array ix(TVar e)实现的,但它可能会被未来更高效的实现所取代(但接口将保持不变)。
我认为这意味着TArray
只是我穿着更好衣服的第2号方法。暗示“更有效”实施的文档很有意思,因为它暗示实际上有更好的方法。
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提出了一些非常有趣的观点。
答案 0 :(得分:5)
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
进行评估。如果变量(IORef
,STRef
,TVar
等)被修改了很多次但从未被强制过,那么thunk会堆积在内存中。评估生成的thunk甚至可能产生堆栈溢出。
如果您的程序需要大规模并行(即,多个内核/ CPU同时访问变量),更新这样的单个值可能效率较低,因为计算可能在处理器之间重复。但是,对于少量核心,重复是非常罕见的。
答案 2 :(得分:1)
Haskell有多个可变数组/向量的实现。 所以你可以使用最简单的方法TVar(Vector a),而不用担心复制开销(在可变数组中没有复制)