我们可以用两个或多个无锁容器原子地做一些事情而不锁定两者吗?

Posted

技术标签:

【中文标题】我们可以用两个或多个无锁容器原子地做一些事情而不锁定两者吗?【英文标题】:Can we do something atomically with 2 or more lock-free containers without locking both? 【发布时间】:2016-08-11 11:21:35 【问题描述】:

我正在寻找Composable operations - 使用事务内存相当容易。 (感谢 Ami Tavory)

使用锁(互斥锁/自旋锁)很容易做到——但它会导致死锁——因此基于锁的算法只能通过手动调整来组合。

无锁算法不存在死锁问题,但它不可组合。需要将 2 个或更多容器设计为单个组合的无锁数据结构。

是否有任何方法、辅助实现或一些无锁算法 - 原子地与多个无锁容器一起工作以保持一致性?

检查一个项目是否同时在两个容器中 原子地将元素从一个容器移动到另一个容器

...

或者 RCU 或危险指针可以帮助做到这一点吗?

众所周知,我们可以使用无锁容器,这在其实现中很困难,例如来自并发数据结构 (CDS) 库:http://libcds.sourceforge.net/doc/cds-api/group__cds__nonintrusive__map.html

例如,我们可以使用 lock-free 有序映射,例如SkipList CDS-lib

但即使是简单的无锁算法在任何情况下都不是无锁的:

    迭代器 documentation-link

您可以仅在 RCU 锁定下迭代跳过列表集项目。只有在 在这种情况下,迭代器是线程安全的,因为当 RCU 被锁定时 套装的物品无法回收。 RCU锁的要求 迭代意味着元素的删除(即擦除)不是 可能。

    ::contains(K const &key) - documentation-link

该函数在内部应用 RCU 锁。

    ::get(K const &key)并更新我们得到的元素,我们应该使用锁:documentation-link

例子:

typedef cds::container::SkipListMap< cds::urcu::gc< cds::urcu::general_buffered<> >, int, foo, my_traits > skip_list;
skip_list theList;
// ...
typename skip_list::raw_ptr pVal;

    // Lock RCU
    skip_list::rcu_lock lock;
    pVal = theList.get( 5 );
    if ( pVal ) 
        // Deal with pVal
        //...
    

// You can manually release pVal after RCU-locked section
pVal.release();

但是如果我们使用 2 个无锁容器而不是 1 个,并且如果我们只使用始终无锁或其中一个无锁的方法,那么我们可以在不锁定两个容器的情况下做到这一点吗?

typedef cds::urcu::gc< cds::urcu::general_buffered<> >  rcu_gpb;
cds::container::SkipListMap< rcu_gpb, int, int > map_1;
cds::container::SkipListMap< rcu_gpb, int, int > map_2;

如果我们想保持原子性和一致性,我们可以原子地将 1 个元素从 map_1 移动到 map_2 而不锁定两个容器 - 即 map_1.erase(K const &amp;key)map_2.insert(K const &amp;key, V const &amp;val)

其他线程看不到第一个容器中没有元素,他还没有出现在第二个中

其他线程看不到第一个容器中有元素,而第二个容器中已有相同的元素

如果我们想保持原子性和一致性,我们可以用 2 个或更多无锁容器原子地做某事而不锁定两者吗?

回答:我们不能通过简单地使用通常的函数来一次对两个或多个无锁容器进行任何原子操作而无需锁定。

仅当我们在容器 API 中执行无锁算法提供的 1 个简单操作时,对于 2 个无锁容器来说,1 个锁就足够了,排除上述 3 种情况,即使在无锁容器中使用锁。

如果您对无锁算法进行了复杂的自定义改进,那么“但可能会有一些额外的开销”,那么您可以提供一些可组合的,例如,“两个队列相互了解,并且代码看起来正如彼得·科德斯所指出的那样,它们是经过精心设计的。

【问题讨论】:

容器不提供“检查一个项目是否同时在两个容器中”,那么您的条件甚至意味着什么?请正式一点。 (可能不是没有容器提供该操作,但正式的描述可能会导致解决方案) @Yakk 我想知道,是否有任何方法、辅助实现或一些无锁算法 - 以原子方式使用多个无锁容器?包括“检查一个项目是否同时在两个容器中”和“原子地将 1 个元素从 map_1 移动到 map_2”。 RCU 或危险指针中可能有一些吗?如果没有这些,并且如果没有外部锁(互斥锁/自旋锁)我不能完全做到这一点 - 这也是答案。 是的,以原子方式执行此操作很容易。您需要做的就是保护所有对使用互斥锁的无锁容器很重要的访问;-) IIUC,您说的是同步可组合性,即 one of the most exciting aspects about transactional memory(相对于更底层的方法,例如 LF)。 @Ami Tavory 谢谢!是的,这就是我要找的。是否有任何方法或示例准备好实现无锁可组合性?还是说无锁不适合也太难了,所以没有一个永远不会做? 【参考方案1】:

TL:DR:正如 Yakk 所指出的,你所问的没有多大意义。但是,由于您只要求一种不锁定 两个 容器的方法,因此您可以这样做。如果这不是您要查找的内容,那么也许这将有助于说明您提出问题的方式存在的问题之一。


一个容器上的multiple-readers / single-writer lock可以轻松实现,并解决观察两个容器的问题。

但是然后永远不允许对您锁定的容器进行无锁访问,因此使用无锁容器是没有意义的。

如果您在观察无锁容器时对锁定容器持有读锁,那么当您观察无锁容器时,您对锁定容器的了解仍然是正确的。


在锁定容器上进行写锁定会阻止任何读取器在您删除元素时观察锁定的数据结构。所以你会使用这样的算法:

write_lock(A);  // exclude readers from A
tmp = pop(A);
push(B, tmp);
write_unlock(A); // allow readers to observe A again, after both ops are done

向另一个方向移动节点的工作方式相同:在锁定容器上持有写锁的同时执行删除和添加。

您可以通过暂时将元素放在两个容器中来保存复制,而不是暂时在两个容器中(复制到临时)。

write_lock(A);  // exclude readers from A
B.add(A[i]);    // copy directly from A to B
A.remove(i);
write_unlock(A); // allow readers to observe A again, after both ops are done

我并不是说没有无锁方法可以做到这一点,顺便说一句。 @Ami 指出事务内存可以支持synchronization composability。

但您的规范的主要问题是 不清楚您究竟是在试图阻止潜在观察者观察什么,因为他们只能以一种顺序观察两个无锁数据结构,或者另一个,不是原子的,正如@Yakk 指出的那样。

如果您控制观察者进行观察的顺序,以及作者进行写作的顺序,那么您可能只需要这样。

如果您需要在两个容器之间建立更牢固的链接,则可能必须将它们设计为了解两个容器的单一无锁数据结构。

【讨论】:

谢谢! 1. “你所问的没有多大意义” - 你的意思是说没有锁就没有多大意义,还是根本不这样做? 2. “不清楚你到底想阻止潜在观察者观察什么” - 我正在尝试解决多个容器、任务的一致性问题,这通常很容易解决通过使用锁(互斥锁/自旋锁)(仅作为示例,通常在 RDBMS 中解决)。 3. “如果您需要在两个容器之间建立更牢固的链接,则可能必须将它们设计为了解两个容器的单一无锁数据结构。 " - 这是一个非常困难的解决方案,我从未遇到过这样的解决方案。 这正是我正在寻找的 - 任何指向此类解决方案的链接,任何关于如何做到无锁的描述。我可以使用任何事务内存低级实现方法来实现无锁可组合性而不使用事务内存。 4. “如果你控制观察者的观察顺序和作者写作的顺序,这可能就是你所需要的。” - 我不同意,任何时候任何线程都可能被中断,我不会看到一致性数据。 @Alex: 4. 如果您无法通过单个原子操作读取两个容器,那么您如何区分读取之间发生的原子移动与. 移动本身不是原子的。所以也许你可以通过控制排序得到足够强的保证。 4. 如果我猜对了,是的,它可以提供很好的保证,但不是 100%。如果 writer-thread 执行:remove->insert 或 insert->remove,那么在这两种情况下,该线程都可能在 remove 和 insert 之间中断很长时间:通过 IRQ(最多 1000 个周期)或通过切换到另一个线程(高达 10 000 000 次循环)。 ***.com/questions/16401294/… 一直以来,所有阅读器线程都会看到不一致的状态。

以上是关于我们可以用两个或多个无锁容器原子地做一些事情而不锁定两者吗?的主要内容,如果未能解决你的问题,请参考以下文章

原子操作实现无锁队列

无锁的原子函数改变两个独立的内存位置

无锁机制下的原子性操作

无锁队列的实现(陈皓)

无锁队列的实现(陈皓)

CAS 与原子操作