我们可以在不锁定两个或更多无锁容器的情况下原子地做一些事情吗?

Can we do something atomically with 2 or more lock-free containers without locking both?

本文关键字:情况下 锁定 两个 我们      更新时间:2023-10-16

我正在寻找可组合操作 -使用事务性内存很容易做到。(感谢Ami Tavory)

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

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

是否有任何方法,帮助实现或一些无锁算法-使自动地与多个无锁容器一起工作以保持一致性?

  • 检查物品是否同时在两个容器中
  • 自动将元素从一个容器移动到另一个

或者RCU或者危险指针能帮上忙吗?

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

例如,我们可以使用无锁有序映射,如SkipList cd -lib

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

  1. 迭代器文档链接

只能在RCU锁下迭代。只有在这种情况下迭代器是线程安全的,因为当RCU被锁定时Set的项不能回收。RCU锁的需求迭代意味着不删除元素(即擦除)可能的。

  • ::contains(K const &key) - document -link
  • 函数内部应用RCU锁

  • 对于 ::get(K const &key) 和更新我们得到的元素,我们应该使用lock: document -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;
    

    如果我们想要保持原子性和一致性,我们可以在不锁定容器(即map_1.erase(K const &key)map_2.insert(K const &key, V const &val))的情况下,将一个元素从map_1原子移动到map_2 吗?

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

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

    如果我们想要保持原子性和一致性,我们可以在不锁两个容器的情况下原子地处理两个或更多的无锁容器吗?

    ANSWER:我们不能在没有锁的情况下对两个或两个以上的无锁容器同时进行任何原子操作。

    如果我们只在容器api中使用无锁算法提供的一个简单操作,那么对于2个无锁容器来说,1个锁就足够了,排除上述3种情况,即使在无锁容器中也使用锁。

    如果你对无锁算法做了复杂的自定义改进,那么你可以提供一些可组合的,例如,正如Peter Cordes所指出的,"两个队列相互知道,并且查看它们的代码是精心设计的"。

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


    一个容器上的多读/单写锁可以很容易地实现这一点,并且解决了同时观察两个容器的问题。

    但是不允许对被锁的容器进行无锁访问,所以使用无锁容器是没有意义的。

    如果在观察无锁容器时对锁定容器持有读锁,那么在观察无锁容器时,关于锁定容器的任何知识仍然有效。


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

    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
    

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

    可以通过在两个容器中临时放置元素来节省复制,而不是在两个容器中都临时放置元素(复制到临时容器中)。

    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指出事务性内存可以支持同步可组合性。

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

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

    如果你需要两个容器之间更强的链接,它们可能必须被设计成一个了解两个容器的无锁数据结构。