好的,您已经提到了我通常建议的并发跳表,但除了上述三个特定要求之外,我认为一个简单的带有每个节点互斥锁的链表就可以胜任:每个节点包含一个元素(或引用)和一个简单的互斥锁。我会假设这里使用Java,因为它有助于避免节点回收时的竞态条件问题。
通过从头部迭代节点搜索列表,无需获取任何锁(虽然您需要在某个时候确保线程间的可见性 - 您可以选择多频繁进行此操作 - 每次搜索可能已足够)。
要添加一个条目,请执行搜索,直到找到要插入值的直接前驱和后继节点,锁定与先前节点相关联的互斥锁,再次检查后继节点(它可能在您的下面更改),然后拼接新节点。
删除类似地工作-找到要删除的节点的前驱节点,锁定前驱节点,检查它仍然是前驱节点,并将其从树中取出。
该结构是排序的(在正确的范围内 - 我尚未证明!)。
该结构清楚地允许多个读取器(读取器永远不会由于任何原因而被阻塞),以及多个编写器,尽管尝试操作列表的同一部分(例如,插入具有相同拼接点的节点的两个线程)的编写器将等待彼此。
该结构似乎相对容易推理 - 基于单个链接列表,具有相当简单的锁定结构和几个简单的不变量。我没有花费更多时间来思考其正确性,但是您可以通过使锁定策略更加沉重(代价是性能)来使其更易于推理-在迭代期间为每个节点插入和删除锁定每个节点,然后在解锁前趋节点之前锁定后继节点-以这种方式,找到拼接或删除点时,您将同时锁定两个节点,因此不需要“双重检查,回溯”。
您可能可以完全消除锁定并使用无锁链表,同时保持“必须排序”的条件,但我尚未深入思考它-至少我认为这将是“更难推理的”。
在C ++中,由于不能指望GC将节点保留在读取器可能正在查看它们的时间段内,因此允许读取器以无锁方式漫游的简单策略行不通,如果您要删除节点。您可以通过使读取器获取每个访问的节点的锁来调整它,但这会对性能(显然)和并发性(因为尽管您可以以某种基本方式拥有多个读取器和编写器,但它们不能再相互传递,因此实际并发性受到严重限制)造成影响。
另一种选择是在每个节点中使用rwlocks,只有读取器需要获取读取锁定,插入/删除者需要获取读取锁定以找到要处理的位置,然后升级为写入。尽管读取器可以相互传递,但并发性仍得到改善,但写入者仍会阻止读取器(因此,在某些节点上开始写入操作之前迭代在该位置之前的所有读取器将无法迭代超过该节点,直到操作完成)。
现在,所有这些都说了(呼!),我应该提到除了“易于理解”之外,这种结构似乎在每个方面都比并发跳表差,可能稍微少用一点内存(也许)。特别是它没有对数(N)搜索行为。它不是完全无锁定的(写入者在某些情况下可能等待其他写入者)。就并发性而言,我甚至不确定它是否更容易理解,尽管底层结构很简单。
我认为,如果您真的想要,您可以将这种“每个节点”锁定扩展到类似于rb-tree的东西,但这并不容易。特别是,您需要在任何树旋转之前找到要锁定的某个节点,该节点“足够高”,以便您可以确信任何想要执行影响旋转正确性的修改的线程也将尝试锁定相同的节点。在链接列表中,这是“前导节点”-在AVL或RB树中并不那么简单。