-
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-channel #22
Conversation
text/2017-11-09-channel.md
Outdated
```rust | ||
pub mod select { | ||
pub fn send<T>(tx: &Sender<T>, value: T) -> Result<(), T>; | ||
pub fn recv<T>(rx: &Receiver<T>) -> Result<T, ()>; |
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.
Super minor nit, but I'd use a ZST error type like struct NotReady;
or something here, rather than ()
.
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 like the symmetry between Result<(), T>
and Result<T, ()>
.
If recv
returns a Result<T, SelectRecvError>
, then send
should probably similarly return a Result<(), SelectSendError<T>>
. That means we'd have to regain ownership from a failed send
operation like this:
if let Err(err) = sel.send(&tx, name) {
// Sending failed. Regain ownership of the message.
name = err.into_inner();
} else {
// Success! The message was sent.
break;
}
Calling .into_inner()
is slightly more unergonomic, but it's not too bad. Possible alternatives could be .into()
and .take()
. Do you agree with this reasoning? :)
In the Reddit comment thread, /u/lurebat argues that we should have explicit
I'm sympathetic to the argument. Here's an example of selection using hidden thread-local state: loop {
if let Ok(msg) = select::recv(&rx1) {
println!("{}", msg);
break;
}
if let Ok(msg) = select::recv(&rx2) {
println!("{}", msg);
break;
}
if select::timeout(Duration::from_millis(100)) {
break;
}
} And this is what it would like with an explicit let mut sel = Select::with_timeout(Duration::from_millis(100));
loop {
if let Ok(msg) = sel.recv(&rx1) {
println!("{}", msg);
break;
}
if let Ok(msg) = sel.recv(&rx2) {
println!("{}", msg);
break;
}
if sel.timed_out() {
break;
}
} We could require the Note that this way we can also make timeouts much less magical. Any thoughts? Please upvote if you think we should go with explicit |
One advantage to not requiring mutable is that people may want to put the |
I wish I had time to give this a proper review, but meantime, just wanted to say: thanks so much, @stjepang (and team), for what you've done for Crossbeam! I couldn't be happier. |
@stjepang I'm in favor of the explicit struct approach, though it seems as though we could also offer the implicit approach as well if people are worried about verbosity, yes? As for the need to explicitly catch {
loop {
sel.recv(&rx1, |msg|
println!("{}", msg);
)?;
sel.timed_out()?;
}
} If nothing else, this would seem to have the benefit of warning the user if a |
I'm personally in favor of the explicit struct approach as well, however I believe that a macro, which also enforces the loop structure, could work quite well and wouldn't be too complicated, for example: https://gist.github.com/TimNN/669d030cc99e4dcefc2b5bc6203a7019 |
I notice that the select algorithm as described (in the "Rendered" link) is not actually fair. Is there a strong performance reason not to go with a fair implementation? I would expect a fair implementation to either check just one option each time through the loop (instead of about half) or to check all the options in a random order at the beginning of the loop. The unfairness would come around if you have a select loop with many options, and a pair of them are always triggered simultaneously. If the two that are triggered simultaneously are next to each other in the loop, then the first one will be chosen far more often than the second one. Maybe this is unimportant, and maybe the performance cost of making it fair is not worthwhile. But it seems worth investigating. |
To make the explicit struct approach a bit neater and remove the need for explicit If you have a mutable reference or are constructing on the spot: for sel in Select::with_timeout(Duration::from_millis(100)) {
if let Ok(msg) = sel.recv(&rx1) {
println!("{}", msg);
}
if let Ok(msg) = sel.recv(&rx2) {
println!("{}", msg);
}
} or if you needed to store a let select = Select::with_timeout(Duration::from_millis(100));
for sel in select.iter() {
if let Ok(msg) = sel.recv(&rx1) {
println!("{}", msg);
}
if let Ok(msg) = sel.recv(&rx2) {
println!("{}", msg);
}
} |
@ebarnard I like that idea. The fewer invariants the API forces the user to manage (in this case, break on success of any rule), the better! |
The |
Could we allow for both the Alternatively you should still be able to wrap it in a function and use return to emulate |
Well, I'd rather choose one or the other. Having two separate, but marginally different interfaces for doing the same thing is kinda silly, isn't it? :) Let's just go with the explicit struct approach.
Interesting idea! While it does look neat, I'm worried that it just sort of pushes the problem from one place to another - now we don't have explicit
Very nice, but this way the control-flow graph loses information - it doesn't know that the body of a successful case is executed just once anymore. Consider the following: let output;
let mut sel = Select::new();
loop {
if let Ok(msg) = sel.recv(&rx1) {
output = msg.to_string();
break;
}
if let Ok(msg) = sel.recv(&rx2) {
output = msg.to_string();
break;
}
}
println!("{}", output); This code compiles because Rust knows that If you rewrite the same example using |
@stjepang However, in the case you only want to perform an operation on each received value, or there's a timeout, or some other fallible assignment the for loop approach is much nicer to use. I suppose there's a cost to explaining both ways of using the select api though. |
Good point! Although, I wonder why would someone attempt to store a Also, I'm feeling a bit uneasy about allowing to have multiple references to the same
Looks pretty good! A question: would it be possible to allow optional commas between select cases? Putting an unexpected comma (or forgetting one) in macros can blow up with poor compilation error messages. Other than that one, I don't see any quirks in the macro. Are there any I'm missing? :) Also, we should probably also support the following syntax: |
I like this alternative quite a bit. It eliminates the redundant monomorphizations, it eliminates the runtime dispatch on flavor, and it allows for non- It also doesn't preclude a dynamic wrapper that behaves the way the current prototype does, though that would potentially make things more confusing as it would provide multiple types for the same purpose. |
@rpjohnst The problem I see with having many different channel versions is that it vastly impedes the use of channels in library interfaces. A library author either needs to create N different versions of the API, or needs to decide which variant is required by their users. I expect the cost of the dynamic dispatch is negligible (testing would be wise), and you get a simple API that allows the users of libraries to patch them together however they like. |
It would of course also be possible to solve that problem by making |
I don't understand why select has to have such complicated API. I have a much simpler proposal (which might be infeasible since I have no knowledge of the internals): let select = rx1.join(&rx2).join(&rx3).with_timeout(8);
select.wait();
if let Some(msg) = rx1.recv_nb() { ... }
else if let Some(msg) = rx2.recv_nb() { ... }
...
else {
// timed out
} (Note: Edit: clarify |
@johncf That interface does not provide any fairness guarantees, so you could end up starving the second receiver if there's a steady stream on the first one and you're using the select in a loop. |
@jdm Ah, then why not use |
@TimNN's macro looks lovely, and seems far less fraught than the analogous macro from @stjepang , note that when I say that I prefer the explicit |
@stjepang: I don't think there is an easy way for optional commas, but allowing an arbitrary number of commas ( |
The |
Here is a refinement of the idea I proposed before, now having APIs for sending and possibly recovering failed-to-send messages. https://gist.github.com/johncf/81778f23da91ae928e0ee7e92fda1b48 |
Reading through this RFC, I was quite confused by what exactly I would like to propose that I feel this would be a very rustic improvement to make; it should still be easy for veterans to use, but welcomes newcomers at the same time, by optimizing for readability over familiarity. |
A few issues I can think of:
|
@stjepang I have a few more concerns regarding a
Edit: Possible recovery of a sent variable on failure. |
Probably, but IMO that would complicate the macro a lot for very little gain. |
|
@stjepang: I personally like Regarding the
|
|
|
|
Reminder that potential edge-case shortcomings of the macro form aren't the end of the world, as long as it's documented how to use this library without the macro, via manually looping and breaking. If the macro will keep people from shooting themselves in the foot for 95% of use cases, then it's still a worthwhile addition. |
What if we simply add a new method let mut sel = Select::new();
while !sel.is_done() {
if let Ok(peer) = sel.recv(rx) {
println!("{} received a message from {}.", name, peer);
}
if let Ok(()) = sel.send(tx, name) {
// Wait for someone to receive my message.
}
} This way you can still put a |
@stjepang: I believe that would have the same downside as the proposed for-loop version: the compiler could no longer guarantee that each "branch" was only taken once. (And in that case I believe the for-syntax is a bit nicer). |
- use explicit `Select` struct - introduce two new error types (used in selection) - clarify the text on wait lists - mention unfair selection as a drawback
Thank you, everyone, for the feedback! I gave all this some thought over the last week. Overall, it seems that the RFC is generally accepted quite well and there aren't any major blockers. I'll address some comments:
Some bikeshedding... Channels are constructed using functions named Finally, I'd like to push forward and publish version 0.1.0 now so that everyone can start playing with the channel. The interface as presented in this RFC will not be set in stone, so if we figure out how to improve it down the line, we'll definitely do it! But let's get some mileage first. How does that sound, @jeehoonkang? |
Thanks for this great job, let's move forward! |
I can send a PR with the initial macro to the prototype repo, which should also allow us to better discuss the implementation details there. |
Regarding the fairness, I just had the following idea (I haven't properly thought it through yet, but wanted to write it down): This approach is probably a bit more expensive but should still be manageable: The core idea is to change the "two half" iteration to something different, so that neighboring operations are not necessarily directly next to each other. This can be achieved by, instead of choosing a split point, choosing a skip count: Chose a skip count X, then skip X operations, perform one operation, skip X operations, perform one operation and repeat until every operation has been performed once (but see caveats / details below). Example: Consider six operations A - F. With a split point the "check" order would be something like C.D.E.F.A.B for a split point of 2. For skip counts between 0 - 5 the following orders are possible: A.B.C.D.E.F, B.D.F.A.C.E, C.F.B.E.A.D, D.A.E.B.F.C, E.C.A.F.D.B and F.E.D.C.B.A. Caveats / Details:
Since Fairness: If I remember my probability theory lessons correctly this system should guarantee that the probability that a specific operation B is visited directly after an operation A should be approximately equally likely for all possible operations B. This should provide some fairly strong fairness guarantees. Disadvantages:
And now I've spent way too long writing this down and it is way too late, and I just realized that this turns the whole operation essentially from O(n) to O(n^2), which is probably too slow for a default but maybe works as an "extra-fair" select. Anyway, I'll think about this some more tomorrow. Comments / Feedback are very welcome. |
I'm in favor of |
@stjepang Thanks for your thoughtful response and thorough summary. I think your response to the unfairness issue was particularly fair. (Apologies for the pun.) On the bikeshedding, I agree that What about |
@TimNN I think a simpler version of your algorithm would simply be to randomly choose one operation to attempt each time through the loop. It is still O(N^2), but is easier to reason about than the skipping version. The prefactor on the O(N^2) shouldn't be too bad if no extra work is done in the loop, an you really want no extra work to be done in the loop, or the implementation leaks. Also O(N^2) isn't too bad if N is small, and when N is large is precisely when the non-fairness gets large with the current implementation, so users might be likely to value it more when it is more expensive. That said, I think the current plan to document the unfairness and postpone any idea of fixing it is good. |
I agree!
Ah, now I understand your earlier comment. I don't think that could easily work, consider this because you aren't guaranteed to visit all operations once:
|
I think adding the following API to
This will allow checking if the same end of a specific channel was used more than once, which means that the macro can enforce / catch violations of all three "important rules one must follow in order to use selection correctly" listed in the RFC either at run- or compile-time. Edit: Well, mostly, even with the macro one can do something like |
Another thought: If this can easily be supported by the implementation it might be nice to have a |
@TimNN You are right about my idea, it would only work if the thread never parked, which would be terribly inefficient. Perhaps just creating an array with length of the number of options, and then shuffling it to determine the order of attempts? The shuffling is an O(N) operation, and then we'd have to run through the loop O(N) times, so it's still an O(N^2) approach, I think. Regarding your closure suggestion, that would seem to make the code far easier to use without side effects or repeated computation. |
PSA: To everyone interested in the |
@droundy @TimNN Regarding fairness, we could do the following. In the first iteration of the loop, while counting cases we also check which cases are ready by calling internal methods Typically we select over no more than a dozen or so cases, so we can remember which cases are ready by marking a bit for each ready case in a There is some performance cost associated with checking which cases are ready (I believe the cost is around 10%), but it's probably not too bad.
For me, names It might be nice to have a dedicated constructor for zero-sized channels, but I'm afraid at the same time it might also make constructors more complicated than is necessary. Note that there is already some precedent for constructors similar to |
I'm merging this RFC and publishing the first version. You can add extern crate crossbeam_channel;
use crossbeam_channel::unbounded;
fn main() {
let (tx, rx) = unbounded();
tx.send("Hello").unwrap();
tx.send("World").unwrap();
drop(tx);
for msg in rx {
print!("{} ", msg);
}
println!();
} I decided to include Let me know if you have any feedback or comments. I'm especially curious:
Finally, the remaining problems can be tracked in dedicated issues: |
Just one more bikesheddy question. :) I'd like to split the Do you think names So the full list of possible selection cases would be: (also, if you think |
Introduce crate
crossbeam-channel
, which aims to be an upgrade overstd::sync::mpsc
from the standard library in pretty much all aspects: features, convenience, and performance.Rendered RFC
Prototype implementation
Selection macro
Benchmarks
cc @jdm @SimonSapin @asajeffrey - we've chatted about this in
#servo
.cc @alexcrichton @aturon @mgattozzi - talked about this at RustConf.
cc @BurntSushi - this might be a replacement for
chan
.cc @insanitybit - finally! :)