是否存在针对这些特定多线程数据结构要求的现有解决方案?

时间:2009-12-06 20:18:49

标签: language-agnostic data-structures concurrency parallel-processing binary-tree

我需要支持这些声明的多线程数据结构:

  • 允许多个并发读者和作者
  • 已排序
  • 很容易推理

实现多个读者和一个作家要容易得多,但我真的不想允许多个作家。

我一直在研究这个领域,我知道ConcurrentSkipList(由Lea基于Fraser和Harris的工作),因为它是在Java SE 6中实现的。我还实现了我自己的并发版本基于Herlihy,Lev,Luchangco和Shavit的A Provably Correct Scalable Concurrent Skip List跳过列表。

这两个实现是由比我更聪明的人开发的,但我仍然(有点惭愧,因为它是惊人的工作)不得不问这个问题是否是并发多读者的两个唯一可行的实现/作者数据结构今天可用吗?

3 个答案:

答案 0 :(得分:3)

对我而言,就像你为自己制造这个问题太难了。请考虑以下事项:

  • 很容易实现许多数据结构的不可变版本,尤其是树。不可变数据结构的好处是,由于是不可变的,一个线程不能修改另一个线程下的集合。不变性=没有竞争条件=没有锁=没有死锁。迷死。

    参见Okasaki's Purely Functional Data Structures,它提供了堆和平衡树,堆栈,队列和其他一些数据结构的ML和Haskell实现。

  • 线程无法看到在其他线程中对不可变数据结构所做的更改。但是,它们可以使用消息传递并发明确地相互通知更改。

锁和互斥锁太低级,可变状态几乎是多线程编程的敌人。如果你想到你试图在不变性和消息传递方面解决的任何问题,那么你将变得更容易1000倍。

答案 1 :(得分:3)

好吧,你已经确定了我通常建议的那个 - 并发跳转列表,但除了上面三个之外没有其他特定要求,我认为一个简单的链接列表与每个节点的互斥量将起作用:

每个节点包含一个元素(或引用)和一个简单的互斥锁。我会在这里假设Java,因为它有助于避免节点回收周围的竞争条件。

通过列表搜索包括从头部迭代节点,无需获取任何锁定(尽管您需要确保线程之间的可见性 - 您可以选择这种情况发生的频率 - 每次搜索一次足够好了。

要添加项目,请执行搜索,直到找到要插入的值的直接前导节点和后继节点,锁定与先前节点关联的互斥锁,再次检查后继节点(它可能已从下面更改)你),然后拼接新节点。

删除工作原理类似 - 找到要删除的节点的前任节点,锁定前任节点,检查它是否仍然是前任节点,并将其从树中取出。

对这个结构进行排序(在正确的范围内 - 我还没有证明这一点!)。

这种结构显然允许多个读者(读者永远不会因任何原因被阻止)和多个编写者,尽管编写者试图操纵列表的相同部分(例如,插入具有相同拼接点的节点的两个线程)将等待在彼此身上。

结构似乎相对容易推理 - 基于单个链表,具有相当简单的锁定结构和一些简单的不变量。但是,我没有花费超过几分钟的时间来推断它的正确性。您可以通过使锁定策略更加重要 - 在迭代期间插入和删除锁定每个节点,然后在解锁前一个节点之前锁定后继者 - 以这种方式使您更加轻松地进行推理,从而以性能为代价进行推理 - 这样您就可以了找到拼接点或删除点时,两个节点都被锁定,因此不需要“仔细检查,回溯”。

您可以完全摆脱锁定并使用无锁列表,同时保持“必须排序”状态,但我没有深入考虑 - 至少我怀疑它会是“更难以推理”。

在C ++中,结构更加复杂,因为只要读者可能正在查看它们,就不能依赖GC来保持节点,所以允许读者在锁定中使用的简单策略 - 如果你想删除节点,自由方式不会飞。您可以通过让读者锁定每个访问过的节点来调整它,但这对于性能(显而易见)和并发性都很糟糕(因为虽然您可以以某种基本方式拥有多个读者和编写者,但他们不能再相互通过了,所以实际并发性受到很大限制。)

另一种方法是在每个节点中使用rwlocks,读取器仅使用读取锁,读取器/删除器使用读取锁来查找要处理的位置,然后升级到写入。由于读者可以通过读者,因此货币至少得到改善,但是作者仍然会阻止读者(所以所有在写操作开始的某个节点之前迭代到某个位置的读者将无法在该操作完成之前迭代该节点)。

现在,所有这些(哇!)我应该提到其他“容易推理”这个结构似乎与每种物质方式的并发跳过列表相差甚远,可能的例外是内存使用量略有减少(也许)。特别是它没有log(N)搜索行为。它不是完全无锁的(在某些情况下,作者可能会等待作者)。尽管底层结构很简单,但我甚至不确定在并发性方面更容易推理它。

我想如果你真的想要你可以将这种“每节点”锁定扩展到类似rb-tree的东西,如果你想要的话,但这并不容易。特别是,您需要找到某种节点,以便在任何树“旋转”之前锁定“足够高”,以确保任何想要执行将影响旋转正确性的修改的线程也会尝试锁定同一个节点。在链表中,这是“前任节点” - 在AVL或RB树中它不是那么简单。

答案 2 :(得分:0)

我在F#创建了一个无锁数据结构,最近对我的工作提出了类似的要求。具体来说,它是一个排序字典,将int个键映射到int值,其中值是计数器,两个基本操作正在递增与给定键关联的计数并获得当前键的数组 - 价值对。

我在F#中将其实现为Map<int, int ref> ref类型的值,它是对从int键到int值的可变引用的不可变映射的可变引用。读者同时读取引用以获取当前映射,查找其中的键并取消引用以获取关联的int值。作者同时读取引用,查找键并以原子方式递增它(如果存在)或使用新键值对创建新映射并使用CAS替换映射的根引用。

这种设计依赖于以原子方式读取和写入引用的能力(.NET保证),但只有在Map的更新很少时才有效。在我的情况下是这样的,因为大多数写入增量计数器已经存在,并且在稳定状态下,新计数器的创建很少。