设计由许多线程读取并由少数编写的高性能排序数据结构

时间:2013-11-28 13:29:40

标签: multithreading algorithm delphi data-structures thread-safety

我有一个有趣的数据结构设计问题,超出了我目前的专业知识。我正在寻找关于解决这个问题的数据结构或算法答案。

要求:

  • 在一个位置存储合理数量的(pointer address, size)对(实际上是两个数字;第一个用作排序键)
  • 在高度线程化的应用程序中,许多线程将查找值,以查看特定指针是否在(address, size)对之一内 - 即,如果对定义了内存范围,如果指针位于列表中的任何范围。线程很少会添加或删除此列表中的条目。
  • 阅读或搜索值必须尽可能快,每秒发生数十万到数百万次
  • 添加或删除值,即变更列表,更少发生;表现并不那么重要
  • 列表内容过时是可以接受但不理想的,即线程的查找代码找不到应该存在的条目,只要在某个时刻条目将存在。

我热衷于避免一种天真的实现,比如有一个关键部分来序列化对排序列表或树的访问。哪些数据结构或算法可能适合此任务?


  

因为我正在使用该语言而使用Delphi标记   这个任务。语言无关的答案非常受欢迎。

     

但是,我可能无法使用任何标准   任何语言的图书馆都没有多少关心。原因是内存访问   (对象的分配,释放等及其内部记忆,例如   树节点等)严格控制,必须通过我自己的   功能。我当前在同一程序中的其他代码使用   红色/黑色的树和有点特里,我自己写了这些。宾语   节点分配通过自定义内存分配例程运行。   这超出了问题的范围,但这里提到要避免   像'使用STL结构foo'这样的答案。我热衷于算法或   结构答案,只要我有正确的参考或教科书,   我可以自己实现。

5 个答案:

答案 0 :(得分:3)

我会使用TDictionary<Pointer, Integer>(来自Generics.Collections)和TMREWSync(来自SysUtils)进行多读独占写访问。只要没有编写器处于活动状态,TMREWSync就允许多个读者同时访问字典。字典本身提供O(1)指针查找。

如果您不想使用RTL类,答案就变成:使用散列映射与多读取独占写同步对象结合使用。

编辑:刚刚意识到您的对实际上代表了内存范围,因此哈希映射不起作用。在这种情况下,您可以使用排序列表(按内存地址排序),然后使用二进制搜索快速查找匹配范围。这使得查找O(log n)而不是O(1)。

答案 1 :(得分:2)

探索一下复制的想法......

从正确的角度来看,读/写锁将完成工作。然而, 在实践中,虽然读者可以同时并行地进行 通过访问结构,他们将在锁上创建一个巨大的争用 即使是读取访问锁定的明显原因也包括写入到锁本身。 这将破坏多核系统中的性能,甚至更多地插入多插槽中 系统

性能低下的原因是缓存行无效/传输流量 核心/套接字之间。 (作为旁注,这是一项非常近期且非常有趣的研究 关于主题Everything You Always Wanted to Know About Synchronization but Were Afraid to Ask)。

当然,我们可以通过制作来避免读者触发的核心间缓存传输 每个核心上的结构副本,并限制读取器线程仅访问 他们当前正在执行的核心本地副本。这需要一些线程获取其当前核心ID的机制。它还依赖于操作系统调度程序,以便不在核心上无偿地移动线程,即在某种程度上维持核心亲和性。 AFACT,大多数当前的操作系统都是这样做的。

对于作者来说,他们的工作是通过获取每个写入锁来更新所有现有的副本。一次更新一棵树(显然结构应该是一些树)确实意味着复制品之间的临时不一致。从问题 描述这种接缝可以接受。当作家工作时,它会阻止读者一个人 核心,但并非所有读者。缺点是作家具有相同的工作 很多次 - 系统中有核心或套接字的时间很多。

PS。

也许,只是也许,另一种选择是某种RCU - 类似方法,但我不知道 这很好,所以我会在提到之后停止:)

答案 2 :(得分:1)

通过复制,您可以拥有: - 您的数据结构的一个副本(列表w /二进制搜索,提到的间隔树,...)(例如,“原始”一个)仅用于查找(读取访问)。 - 当要更改数据(写访问)时,创建第二个副本“更新”。因此写入更新副本。

写完成后,将“当前”指针从“原始”更改为“更新”版本。将访问计数器与“原始”副本相关联,当计数器减少回零读者时,可以销毁该副本。

在伪代码中:

// read:
data = get4Read();
... do the lookup
release4Read(data);

// write
data = get4Write();
... alter the data
release4Write(data);


// implementation:            
// current is the datat structure + a 'readers' counter, initially set to '0'
get4Read() {
  lock(current_lock) {              // exclusive access to current
    current.readers++;              // one more reader
    return current;
  }
}

release4Read(copy) {
  lock(current_lock) {              // exclusive access to current
   if(0 == --copy.readers) {        // last reader
     if(copy != current) {          // it was the old, "original" one
       delete(copy);                // destroy it
     }
   }
  }
}

get4Write() {

   aquire_writelock(update_lock);  // blocks concurrent writers!

   var copy_from = get4Read(); 
   var copy_to = deep_copy(copy_from);
   copy_to.readers = 0;

   return copy_to;
}    

release4Write(data) {

   lock(current_lock) {              // exclusive access to current
     var copy_from = current;
     current = data; 
   }

   release4Read(copy_from);

   release_writelock(update_lock);  // next write can come
}

要完成有关要使用的实际数据结构的答案: 鉴于数据条目的固定大小(两个整数元组),也非常小,我将使用数组进行存储,并使用二进制搜索进行查找。 (另一种选择是评论中提到的平衡树)。

谈论绩效:据我所知,'地址'和'大小'定义了范围。因此,对这个范围内的给定地址的查找将涉及一遍又一遍地对“地址”+“大小”(用于比较所查询的地址与范围上限)的加法运算。为了避免重复添加,明确地存储开始和结束地址而不是开始地址和大小可能更有效。

答案 3 :(得分:1)

http://symas.com/mdb/阅读LMDB设计论文。具有无锁读取和写时复制写入的MVCC B +树。读取始终为零复制,写入也可以选择为零复制。在C实现中可以轻松处理每秒数百万次读取。我相信你应该可以在你的Delphi程序中使用它而不需要修改,因为读者也没有内存分配。 (作家可能会做一些分配,但可以避免大部分分配。)

答案 4 :(得分:0)

作为旁注,这里有关于记忆障碍的好读物:Memory Barriers: a Hardware View for Software Hackers


这只是为了回答@fast的评论,评论空间不够大......

  

@chill:你认为有必要设置任何“记忆障碍”吗?

无处不在,您可以从两个不同的核心访问共享存储。

例如,作家来,制作数据的副本然后调用 release4Write。在release4write内,作者完成了作业 current = data,用新的位置更新共享指针 数据,将旧副本的计数器减少为零,然后继续删除它。 现在,读者介入并调用get4Read。在get4Read内,它copy = current。由于没有内存障碍,这恰好会读取current的旧值。据我们所知,写入可能在删除调用后重新排序,或者current的新值可能仍然驻留在编写器的存储队列中,或者读者可能还没有 看到并处理了相应的缓存失效请求等等...... 现在,读者很乐意继续搜索该数据副本 作者正在删除或刚刚删除。糟糕!

但是,等等,还有更多! :d

  

如果&gt;使用propper get ..()和release ..()函数,在哪里可以看到访问已删除数据或多次删除的问题?

请参阅以下读取和写入操作的交错。

Reader                      Shared data               Writer
======                      ===========               ======
                             current = A:0            

data = get4Read()
   var copy = A:0
   copy.readers++;
                             current = A:1
   return A:1
data = A:1
... do the lookup
release4Read(copy == A:1):
    --copy.readers           current = A:0
   0 == copy.readers -> true

                                                      data = get4Write():
                                                           aquire_writelock(update_lock)
                                                           var copy_from = get4Read():
                                                                  var copy = A:0
                                                                  copy.readers++; 
                             current = A:1
                                                                  return A:1
                                                           copy_from == A:1
                                                           var copy_to = deep_copy(A:1);
                                                           copy_to == B:1
                                                           return B:1
                                                      data == B:1
                                                      ... alter the data
                                                      release4Write(data = B:1)
                                                           var copy_from = current;
                                                           copy_form == A:1
                                                           current = B:1
                             current = B:1 
     A:1 != B:1 -> true
     delete A:1
                                                           !!! release4Read(A:1) !!!

编写器访问已删除的数据,然后再次尝试删除它。双哟!