我们可以用2个或更多无锁容器原子地做一些事情而不用同时锁定它们吗?

时间:2016-08-11 11:21:35

标签: c++ multithreading concurrency lock-free libcds

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

很容易使用锁(互斥锁/自旋锁) - 但它可能导致死锁 - 所以基于锁的算法只能通过手动调整来组合。

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

是否有任何方法,辅助实现或一些无锁算法 - 原子使用几个无锁容器来保持一致性?

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

...

或RCU或危险指针有助于做到这一点吗?

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

例如,我们可以使用无锁有序地图,例如SkipList CDS-lib

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

  1. 迭代器 documentation-link
  2.   

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

    1. ::contains(K const &key) - documentation-link
    2.   

      该功能在内部应用RCU锁定。

      1. ::get(K const &key) 并更新我们获得的元素,我们应该使用锁documentation-link
      2. 示例:

        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 &key)map_2.insert(K const &key, V const &val)如果我们想维护原子性和一致性:

        • 其他线程没有看到第一个容器中没有元素,并且他仍然没有出现在第二个

        • 其他线程没有看到第一个容器中有元素,第二个

        • 中已经存在相同的元素

        如果我们想保持原子性和一致性,我们可以使用2个或更多无锁容器以原子方式执行某些操作而不会同时锁定它们吗?

        答案:我们无法通过简单的常用功能一次性使用两个或更多无锁容器进行任何原子操作。

        只有当我们在容器-API中执行1个无锁算法提供的简单操作时,对于2个无锁容器,它就足够1个锁,即使在无锁容器中使用锁也不包括上述3种情况。

        另外&#34;但也许有一些额外的开销&#34;如果您对无锁算法进行了复杂的自定义改进,那么您可以提供一些可组合,例如,&#34;两个队列彼此了解,并且查看它们的代码是经过精心设计的。正如Peter Cordes所说。

1 个答案:

答案 0 :(得分:2)

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

我并没有声称没有无锁的方法来做到这一点,BTW。 @Ami指出事务内存可以支持synchronization composability

但是你的规范的主要问题是目前还不清楚你究竟想要阻止潜在的观察者观察,,因为他们只能在一个订单中观察两个无锁数据结构或另一个,不是原子地,正如@Yakk指出的那样。

如果你控制观察者观察的顺序,以及作家写作的顺序,那可能就是你所需要的。

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