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

1

1

I'm looking for Composable operations - it fairly easily to do using transactional memory. (Thanks to Ami Tavory)

And it easily to do using locks (mutex/spinlock) - but it can lead to deadlocks - so lock-based algorithms composable only with manual tuning.

Lock-free algorithms do not have the problem of deadlocks, but it is not composable. Required to designed 2 or more containers as a single composed lock-free data structure.

Is there any approach, helper-implementation or some lock-free algorithms - to atomically work with several lock-free containers to maintain consistency?

  • To check if if an item is in both containers at once
  • To move element from one container to another atomically

...

Or can RCU or hazard-pointers help to do this?

As known, we can use lock-free containers, which is difficult in its implementations, for example from Concurrent Data Structures (CDS) library: http://libcds.sourceforge.net/doc/cds-api/group__cds__nonintrusive__map.html

And for example we can use lock-free ordered-map like SkipList CDS-lib

But even simple lock-free algorithm is not lock-free for any cases:

  1. Iterators documentation-link

You may iterate over skip-list set items only under RCU lock. Only in this case the iterator is thread-safe since while RCU is locked any set's item cannot be reclaimed. The requirement of RCU lock during iterating means that deletion of the elements (i.e. erase) is not possible.

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

The function applies RCU lock internally.

  1. To ::get(K const &key) and update element which we got, we should use lock: documentation-link

Example:

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();

But if we use 2 lock-free containers instead of 1, and if we use only methods wich is always lock-free, or one of it lock-free, then can we do it without locking both containers?

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;

Can we atomically move 1 element from map_1 to map_2 without locking both containers - i.e. map_1.erase(K const &key) and map_2.insert(K const &key, V const &val) if we want to maintain atomicity and consistency:

  • that other threads do not see that there is no element in the first container, and he still had not appear in the second

  • that other threads do not see that there is element in the first container, and the same element already in the second

Can we do something atomically with 2 or more lock-free containers without locking both - if we want to maintain atomicity and consistency?

ANSWER: We can't do any atomically operations with two or more lock-free containers at once without locks by using simply its usual functions.

Only if we do 1 simply operation provided by lock-free algorithm in containers-API then for 2 lock-free containers it is enough 1 lock, exclude 3 cases described above when even in lock-free containers uses locks.

Also "but maybe something with a bunch of extra overhead" if you made complicated custom improvements of lock-free algorithms then you can provide some composable, for example, as "the two queues know about each other, and the code looking at them is carefully designed" as Peter Cordes noted.

Semitropical answered 11/8, 2016 at 11:21 Comment(5)
The containers do not provide "check if if an item is in both containers at once", so what do your conditions even mean? Please be formal. (Probably not without the containers providing that operation, but the formal description may lead to a solution)Lyssa
@Yakk I interested to know, is there any approach, helper-implementation or some lock-free algorithms - to atomically work with several lock-free containers? Including "check if if an item is in both containers at once" and "atomically move 1 element from map_1 to map_2" at once. May be is there some in RCU or Hazard-Pointers? If there are nothing of these, and if I can not do this exactly without external lock (mutex/spinlock) - it will also be the answer.Semitropical
Yes, it's easy to do this atomically. All you need to do is protect all access where this matters to both lock-free containers with a mutex ;-)Bodily
IIUC, you're talking about synchronization composability, which is one of the most exciting aspects about transactional memory (as opposed to more low-level approaches, e.g., LF).Shied
@Ami Tavory Thank you! Yes, this is what I'm looking for. Are there any approaches or examples ready to implement lock-free composability? Or is it not suitable and too difficult for lock-free, so this is none never does?Semitropical
A
2

TL:DR: what you're asking doesn't make a lot of sense, as Yakk points out. But since you only asked for a way to do it without locking both containers, here's something you can do. If this isn't what you're looking for, then maybe this will help illustrate one of the problems with how you've posed the question.


A multiple-readers / single-writer lock on one of the containers would allow it easily, and solve the problem of observing both containers.

But then lock-free access to the container you lock is never allowed, so it's pointless to use a lock-free container.

If you hold a read-lock on the locking container while you observe the lock-free container, then whatever you learned about the locking container is still true while you observe the lock-free container.


Taking a write-lock on the locking container stops any readers from observing the locked data structure while you remove an element. So you'd use an algorithm like:

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

Moving a node in the other direction works the same way: do both the remove and add while holding a write-lock on the locking container.

You can save copying by temporarily having the element in both containers, instead of temporarily in neither (copied to a temporary).

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

I'm not claiming that there is no lock-free way to do this, BTW. @Ami points out that transactional memory can support synchronization composability.

But the major problem with your specification is that it's not clear what exactly you're trying to stop potential observers from observing, since they can only observe two lock-free data structures in one order or another, not atomically, as @Yakk points out.

If you control which order the observers do their observing, and which order the writers do their writing, that might be all you need.

If you need stronger linking between two containers, they probably have to be designed as a single lock-free data structure that knows about both containers.

Advertent answered 11/8, 2016 at 16:50 Comment(8)
Thank you! 1. "what you're asking doesn't make a lot of sense" - Do you mean that doesn't make a lot of sense to do it without locks, or not to do it at all? 2. "it's not clear what exactly you're trying to stop potential observers from observing" - I'm trying to solve the problem of consistency of several containers, task, which is very often and easily solved by using locks (mutex/spinlocks) (and only for example, is often solved in the RDBMS).Semitropical
3. "If you need stronger linking between two containers, they probably have to be designed as a single lock-free data structure that knows about both containers." - This is a very difficult solution, and I have never met such. And that's exactly what I'm looking for - any links to such solutions, any description of how to do it lock-free. Any approach to transactional memory low-level implementations that can be used by me to implement lock-free composability without using transaction memory.Semitropical
4. "If you control which order the observers do their observing, and which order the writers do their writing, that might be all you need." - I don't agree, there in any time any of threads may be interrupted, and I will see not consistency data.Semitropical
@Alex: 4. If you can't read both containers with a single atomic operation, how can you tell the difference between an atomic move happening between your reads, vs. the move itself not being atomic. So perhaps you can get a strong enough guarantee by controlling the ordering.Advertent
4. If I get you right, yes, it can give a great guarantees, but not 100%. If writer-thread does: remove->insert or insert->remove, then in both cases this thread may be interrupted between remove and insert, for a long time: by IRQ (up to 1000 cycles) or by switching to another thread (up to 10 000 000 cycles). #16401794 And all this time, all reader-threads will see an inconsistent state.Semitropical
@Alex: but how can reader-threads tell the difference between that and the reader-thread context-switching between looking at A and looking at B. Without atomic reads, you can't even tell that you're observing non-atomic writes.Advertent
Reader-threads can't tell the difference between that and the reader-thread context-switching between looking at A and looking at B. But if required that my program to understand, for example, whether there is a at all task, and looks in two queues: 1-completed tasks, 2-not yet completed tasks. Then my program will tell that there is not such a task - but that would be a logical fallacy of the program, due to non-compliance with consistency. As I understand it can't be solved by using lock-free-only algorithms, isn't it?Semitropical
@Alex: Right, not unless the two queues know about each other, and the code looking at them is carefully designed (I don't have anything specific in mind, but maybe something with a bunch of extra overhead, like your hazard pointer idea). I don't know, but it's not an area I've looked at. You can also use transactional memory to make both reads together an atomic operation. The point of this answer was mostly to point out flaws in the question, not to definitively say "no", or explain what the state of the art in lock-free data structures is.Advertent

© 2022 - 2024 — McMap. All rights reserved.