假设我们有一个在多个线程之间共享的映射。它表示存储在磁盘上的某个层次结构(例如,文件系统中的目录)中的节点。构建价值在时间和内存上都很昂贵。这是经典的“按需初始化”问题,有一个转折:我们在查找请求时在地图中初始化值,但我们不希望锁定整个地图这样做是为了让其他线程访问已构造的值。在这个应用程序中,现有值的查找将比其他现有值更常见
。尝试1 :抓取地图上的写锁定,检查是否存在Key,如果存在则返回,否则,构造Value,放入地图。
评估:这可以防止其他线程在构建Value时访问地图。由于读取非常常见并且非常快,因此这将表现为延迟的丑陋跳跃:不好。
尝试2 :在地图上抓取读锁,检查是否存在Key,如果存在则返回,否则释放读锁,构造值,抓写锁,检查存在,如果不存在,则放在地图中,如果存在则删除新构造的值。
评估:现在我们不会引起读取延迟的跳跃,但我们最终可能会不必要地构建具有相同值的多个内存表示形式(当多个线程尝试查找相同的值时) - 未构造的价值同时)。问题是,我们不希望这样:创建这些值真的昂贵。此外,他们可能会开始触发事件或进行I / O,这意味着现在我们必须处理一个允许短暂但重量级的Value实例存在的设计:更好地避免额外的头痛。
尝试3 :使用两级锁定。在地图上抓取读锁,查找Key,如果存在则返回,否则,在地图上释放读锁,在地图上抓写锁,检查占位符,抓住占位符的读锁,如果存在,否则,创建占位符,抓住其写锁,插入到map,在地图上释放写锁,创建Value,用Value替换占位符,释放占位符的写锁。
评估:在构造Value时将占位符插入到地图中保证只有一个线程会尝试它,从而解决了尝试2的问题。但是,这个设计留下了一个问题:什么< em> form 这个占位符应该采取什么?答案并非微不足道。首先,如果它包含一个锁并且线程等待它,就很难删除它(你怎么知道没有人拿着占位符的锁?你可以抓住它,当然,但你持有它所以,再次,你不能删除它)。其次,可以查找磁盘上没有Value的密钥。如果我们为每次查找尝试插入一个占位符(即使是最终会失败的那些),那么谁将要清理它们?最后,使用两个级别的锁,代码变得相当丑陋:我们首先获取读锁定,检查,然后获取写锁定,重新检查,我们需要在地图级别和单独的占位符级别执行此操作(简单为了创造死锁,我可以证明它。)
这个小宝宝一直在弹出,我似乎无法找到一个优雅的解决方案。尝试3是我最接近满足我的性能条件,但是一旦你弄清楚脏的细节,它很难看并且容易出错。我真的很感激任何见解或建议。
答案 0 :(得分:2)
目标似乎是以创建新对象为代价,在查找现有对象时实现快速周转。这是一个可以为你做到这一点的解决方案。
您需要在内存中维护两个映射,两者最终都具有相同的内容。你还需要一个用于写作和阅读的互斥量: - curReadMap * - curWriteMap * - writeMutex - readMutex
这两个指针很重要,我们将交换它们。现在读取你拥有的值(粗略的伪代码):
lock( readMutex )
value = checkValueIn( curReadMap, key )
unlock()
if( value ) return value
如果找不到该值,则可以进入写作部分
lock( writeMutex )
value = checkValueIn( curWriteMap, key ) //double check now
if( value ) return value
value = createNewValue()
putIn( curWriteMap, key, value )
lock( readMutex )
swap( curWriteMap, curReadMap )
unlock( readMutex )
putIn( curWriteMap, key, value ) //update old map now as well
unlock( writeMutex )
在此方案中,典型的阅读器仅承担一个互斥锁的成本和地图中的查找。如果找不到对象,它们只会承担创建成本。交换映射指针还可以确保readMutex上的锁定时间非常短。
答案 1 :(得分:1)
方案#3的问题在于如何跟踪占位符对象。如果你愿意放弃一些并行性,你可以更容易地做到这一点 - 分配一个锁,它将使用每个映射序列化值创建。然后就是:
lookup(map, key):
1. Grab read lock on map
2. lookup key in map, unlock & return if present
3. grab lock on map.create_serializer
4. lookup key in map, unlock & return if present
5. release read lock on map
6. create value for key
7. grab write lock on map
8. insert (key,value) into map
9. release write lock on map
10. release lock on map.create_serializer
create_serializer确保在任何时候只有一个线程创建要放入此地图的对象。同时查找相同缺失键的多个线程将在步骤3中序列化 - 第一个将继续构建值,其余将在步骤4中找到该值。
这将(不必要地)为同一地图序列化不同的价值创作,但在其他方面满足您的标准。