-
Notifications
You must be signed in to change notification settings - Fork 14
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
Introduce crossbeam-deque #21
Conversation
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.
I'm almost approving this RFC. I have only one comment on the function signature of Stealer::steal()
.
shrink as elements are inserted and removed. By specifying a large minimum capacity | ||
it is possible to reduce the frequency of reallocations. | ||
|
||
The `steal` method returns a `Steal<T>`. Instead of retrying on a failed CAS operation, the |
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.
I'm slightly inclined to like the following interface better than Steal
:
enum StealError {
LostRace,
}
fn Stealer<T>::steal(&self) -> Result<Option<T>, StealError>;
Here, Ok(Some(val))
means it returns the value val
and Ok(None)
means the deque is empty; Err(LostRace)
means the thread couldn't get any useful information from the deque since it lost a race over the deque. I think this application of Result
and Option
types aligns with the intended use cases of these types.
A drawback is it's a little bit verbose.
What do you think?
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.
Hmm, I don't know - both versions look fine to me. Let's see what others think.
However, one aspect of your method signature feels a bit unusual. Typically, the empty case is considered to be an error, not a special case of the success case. For example, in the mpsc
channel, try_recv
method has the following signature:
pub fn try_recv(&self) -> Result<T, TryRecvError>;
pub enum TryRecvError {
Empty,
Disconnected,
}
So the Ok
case happens when an element is successfully received, and the Err
case happens when the channel is either empty or disconnected. In your signature, steal
results with Ok
even if the deque is empty.
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.
Typically, the empty case is considered to be an error
@stjepang I've always considered this to be a mistake in the case of try_recv
. The method itself suggests that receiving both Some
and None
are expected behaviours and could be considered successful cases - after all, the point of calling try_recv
instead of recv
is because we know that there may not be value waiting.
I've found that this tends to make error handling frustrating. I often find myself wanting to write code like this:
while let Some(msg) = rx.try_recv()? {
// handle msg
}
However the closest I can think of with the current API is significantly more verbose:
loop {
match rx.try_recv() {
Ok(msg) => {
// handle msg
},
Err(mpsc::TryRecvError::Empty) => break,
Err(err) => return Err(MyError::Disconnected),
}
}
Also, notice how I use a custom error type in the second example too - this is because I rarely find myself wanting to wrap TryRecvError
in my own error type, as the Empty
variant is never really an "error" that I want to propagate up through my results in practise as it is almost always expected behaviour.
Anyway, this is just my two cents following personal experience - I could very well be using this wrong! Also I just want to quickly add I really appreciate all the work you're putting into crossbeam at the moment :)
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.
@mitchmindtree Thanks for chiming in - you're presenting a very good argument!
While I agree that returning Ok(None)
instead of Err(Empty)
would make implementing some patterns easier, it's still slightly odd to return an Ok
when a receive operation fails.
I mean, try_recv
didn't recv a message, but it still returned an Ok
? Hmmm... :)
Also, what if you want to receive a message from a channel, while asserting that it's not disconnected nor empty? You'd have to write:
let msg = rx.try_recv().unwrap().unwrap();
Aren't there too many layers of wrapping?
Perhaps this is a sign that we should keep the Steal
enum instead of returning a Result
? The steal
operation is pretty unusual, so enumerating the entire list of cases on each call would make the intent of the code more obvious.
Furthermore, concurrent deques and, consequently, calls to steal
are not very common in Rust programs (unlike channels and try_recv
), so I believe there isn't a strong incentive to make the method as ergonomic as possible.
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.
I mean, try_recv didn't recv a message, but it still returned an Ok? Hmmm... :)
Hmmm I find it interesting that you bring this up as a point against, as this seems like the intuitive behaviour to me 😄
In my view, try_recv
exists in order to avoid blocking when no values are present, so receiving None
is often the expected, "successful" (Ok
) result - I haven't personally come across a case yet where I'd describe the channel being empty as an "error" in the flow of my code or unexpected behaviour, as it seems to me this is the point of using try_recv
instead of recv
in the first place.
let msg = rx.try_recv().unwrap().unwrap();
Aren't there too many layers of wrapping?
In this case I'd argue that perhaps the user should be using recv
if they are expecting a value to exist and panic!
ing otherwise, e.g. rx.recv().unwrap()
?
On the other hand, if a user really did come across a use-case for this, I'd imagine this would be more common in practise rx.try_recv()?.unwrap()
.
Perhaps this is a sign that we should keep the Steal enum instead of returning a Result?
Admittedly I'm not very familiar with this proposal - I just happened to come across your reference to the try_recv
API and it sparked some frustrating memories :) Perhaps I'd be better off raising this as an alternative ergonomic approach in #22?
I don't have strong opinions about the API as far as Rayon is concerned, as long as we can still do the same things and the performance is similar or improved. The minimum capacity is interesting. We could expose that indirectly in our |
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.
Looks great! One thing from me, now that we have a generic Collector
-based API, maybe it would be good to add a constructor for deque
from a custom Collector
or Handle
?
wanted a few additional methods: | ||
|
||
1. [A method that steals more than one value at a time.](https://github.com/stjepang/coco/issues/11#issuecomment-339785208) | ||
2. [A `steal_when_greater` method.](https://github.com/stjepang/coco/issues/10#issuecomment-339785563) |
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 is pretty low in priority IMO. As you pointed you can approximate this with the len
API.
When building `futures-pool`, **[@carllerche](https://github.com/carllerche)** | ||
wanted a few additional methods: | ||
|
||
1. [A method that steals more than one value at a time.](https://github.com/stjepang/coco/issues/11#issuecomment-339785208) |
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.
Definitely interesting, but I agree that this isn't a requirement for moving forward and could be explored later.
I still believe we should keep the When you call
The first case is clearly a success ( The situation is even more confusing with concurrent deques. When you call
Again, the first case is a clear success. The second case is, again, a success or a failure depending on how you think about it. Regarding the third case, you might say it's a failure because essentially nothing happened. But, I would argue, the third case can also be viewed as a "neutral" outcome (nothing happened, you should just retry), while the second case is a failure (failed to steal an element because the deque is empty). In the end, whatever we decide is a success ( let msg = stealer.steal()?; But if you're matching on an enum, there's no confusion at all: let msg = match stealer.steal() {
Steal::Empty => None,
Steal::Data(d) => Some(d),
Steal::Inconsistent => return Err(whatever),
}; Of course, this is much more verbose. I appreciate @mitchmindtree's concerns about ergonomics. Since channels are very popular, it makes sense to discuss how to make their use as ergonomic as possible. But concurrent deques are different - they are used very infrequently, so in their case, I believe we should pursue clarity over ergonomics instead. There is already some precedent for choosing an explicit enum instead of relying on |
I thought the empty case is definitely a success, but it turns out that someone (you, at least) disagrees with me. In that case, I agree with you in choosing clarity over ergonomics or doing someone's "right" thing. Let's leave the Anyway, I'd like to ask if you could put @mitchmindtree's and my argument for the changes to the interface in the RFC. Here are two more cents from me. I think |
This seems quite similar to the questions around (Going by memory; I should dig it up but I'm lazy.) Anyway, I think we could use a similar solution here as what I remember the resolution there being: go with the 3-way enum, and also have convenience methods on it to translate it to the different possible |
Will add to the text.
Sounds like a great idea - best of both worlds! I've managed to dig up a few relevant links to similar discussion around @alexcrichton Do you perhaps have an opinion here? Here's a quick explanation of the dilemma. Currently, we have the following API for the steal operation in a concurrent deque: impl<T> Stealer<T> {
/// Steals an element from the top of the deque.
pub fn steal(&self) -> Steal<T>;
}
/// Possible outcomes of a steal operation.
pub enum Steal<T> {
/// The deque was empty.
Empty,
/// Some data was stolen.
Data(T),
/// Inconsistent state was encountered. Try again.
Inconsistent,
}` We're wondering whether method |
@stjepang the primary use case for We actually started out with |
Let's go with the explicit enum then. I've added a remark on Also, after chatting with @jeehoonkang, I've renamed |
This is a proposal to introduce crate
crossbeam-deque
with a work-stealing Chase-Levdeque implementation that uses Crossbeam's new epoch-based GC.
If you're using a similar deque implementation right now and have any thoughts, comments, or requests for new functionality, now is a great time to say so. :)
Rendered
Implementation
cc @carllerche @nikomatsakis @cuviper