-
Notifications
You must be signed in to change notification settings - Fork 469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a MarkableArcCell to allow building reference-counted concurrent collections #80
Conversation
// Locks the internal spinlock. | ||
fn take(&self) -> Arc<T> { | ||
loop { | ||
match self.0.swap(0, Ordering::Acquire) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't lock free, a suspended thread between a take/put operation can block others here arbitrarily.
While this isn't inherently wrong, this does mean that collections written with this won't be lock-free either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, and we should consider other ways of creating a markable pointer.
What's the use case for this? The only literature that I found used this as some sort of atomic Optional. Are there uses for this where the nullness of the pointer and the marker status differ? Is there a reason this must be reference counted instead of using the epoch memory management? If there are any guarantees on heap pointer alignment in rust, this could also be done by using unused bits and would be implementable in a lock-free manner. If/when double word atomics are added they could make this current implementation lock free as well. |
Yes, a tagged pointer would be an optimization over two values, but the reason I use a spinlock is not that there are two values. When you look at Consider the situation: The ability to make this lock-free therefore depends not on squashing the boolean into the unused bits of the
I wouldn't count on hardware support for this, unfortunately, but as explained above a DCAS is not necessary.
The problem I am trying to solve is that we need a stronger guarantee than just knowing that the memory we have a pointer to can be accessed, because while the memory may be valid, it might've been logically deleted from the list. When holding a In other words, we need a way to make sure that the pointer we hold is still accessible from the list when modifying it. My explanation is probably a bit wonky, so please see 4.3.3.1, p. 57 in this.
Probably not, but to my knowledge epoch memory management would require a similar addition of a pointer-boolean pair for this to work, unless it already contains something like this. Due to the EDIT: In fact, I think this could be done with some additions |
I'm at work right now so this will be short, but I think that this commit is really doing two things at once - it's adding the idea of a markable pointer, and also the idea of an Atomic Arc. Would it make sense to separate them? If only a markable pointer was needed it would be far cheaper than the AtomicMarkableArc for loads/stores, and the AtomicArc also wouldn't have to deal with the overhead of being markable. |
If by AtomicArc you mean a type that provides atomic storage and retrieval of an Arc, then, well, it's already in crossbeam and it's called A markable pointer type is something that is indeed not yet present, and it's the aim of this PR to add one.
The aim of this PR is to build a markable pointer that is already integrated with a cleanup scheme (not simply a ptr-boolean pair that has to be freed like a normal ptr) and the current choice is ref-counting or the epoch GC. It's built on top of An alternative to ref-counting, as you mentioned, is a GC (e.g. the epoch GC), and a similar marked pointer type might be added to |
Finally got around to writing a more concurrency-friendly version of With these guarantees, the guarantees that a skiplist as described in "The Art of Multiprocessor Programming" by M. Herlihy and N. Shavit implemented using this type would provide are: What do you think? |
I think this would be more appealing if you could link the code of the use cases you said to have. Code can be enlightening. |
@arthurprs You're right. I have uploaded a repository containing a concurrent linked list based on the |
That's great. But I'm confused, you say the linked list is terribly slow so how the skip list would fare better? |
Oh I see it's not actually a linked list, just similar name. |
Yes, skiplists are essentialy made by using several linked lists internally, each one with a different number of elements. Thanks to this and some other properties, skiplists scale much better than linked lists. |
I see the use case more clearly now, thanks. Does this scheme perform reasonably well in practice though? |
The linked list itself is not that useful, but good as a starting point. I used it in that way to write a map implemented as a skiplist using pretty much the same techniques as the linked list does. The problem with that map is that it is only lock-free if the underlying primitive ( Now, even though the skiplist is more or less fast, we want it to perform well under contention, which can only be guaranteed by lock-freedom. I can see two ways of achieving this. One is to fix The other way is to replace the memory recollection scheme with TL;DR concurrency is hard |
@arthurprs This primitive is integrated with a different memory-recollection scheme - reference counting. If the authors of |
More general types have been proposed in crossbeam-rs/rfcs#28. |
What?
This PR adds a new type to
crossbeam::sync
- theMarkableArcCell
. This type provides all the functionality ofArcCell
and more. In particular, it allows one to mark the cell with a boolean value and then perform atomiccompare_exchange
operations on the(Arc<T>, bool)
pair. It is based on a type from thejava.util.concurrent.atomic
module called theAtomicMarkableReference
(docs) and is supposed to provide the same kind of functionality.I also made a tiny addition to
ArcCell
-ArcCell::with_val(v)
is equivalent toArcCell::new(Arc::new(v))
. I think it's nicer not to have to expliciitly initialize theArc
each time, but if the change is unwanted I don't mind removing it as it's not too important.Why?
This type allows us to build concurrent lock-free collections as described in "The Art of Multiprocessor Programming" by M. Herlihy and N. Shavit. The book provides code in Java, which is a garbage-collected language. The big problem in writing such structures in non-GC languages is memory cleanup. In my opinion, the easiest to use equivalent of a GC is atomic reference counting, provided by
Arc<T>
.I have already written a lock-free linked-list-based map and will publish it after this PR or its equivalent is merged, as the map depends on the
MarkableArcCell
. This will allow us to then create more performant maps, such as skiplist-based ones and stabilize the interface for a concurrent map in Rust, as discussed in #41, after which work on a lock-free hashmap could begin incrossbeam
.I have made some decisions regarding the interface of
MarkableArcCell
which might raise questions, so please ask and I will try to justify them.