Skip to content
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

Zero-capacity channels #436

Closed
ghost opened this issue Nov 1, 2019 · 4 comments
Closed

Zero-capacity channels #436

ghost opened this issue Nov 1, 2019 · 4 comments
Labels
api design Open design questions

Comments

@ghost
Copy link

ghost commented Nov 1, 2019

While we can't implement zero-capacity channels based on futures as faithfully as they are in crossbeam-channel and Go, we can do something very similar. Here's a proposal.

Our zero-capacity channel could be implemented as a channel that has the capacity of 1, except that every send operation does not complete until its message is received.

The problem here is that send operations are not cleanly cancelable. Once a send operation has put its message into the channel, a receive operation can take it. If the Send future is then dropped, the send operation is not really canceled because someone has already picked up the message.

This issue will come up if we're selecting over send operations:

futures::select! {
    _ = s1.send(msg1) => {}
    _ = s2.send(msg2) => {}
}

Here it's possible for both send operations to be executed simultaneously. But maybe that's fine and not a big issue since selection over send operation is rare.

Fortunately, selection over receive operation is still cleanly cancellable:

futures::select! {
    msg1 = r1.recv() => {}
    msg2 = r2.recv() => {}
}

Exactly one receive operation will complete here, which is what the user would probably expect. We can't make the same guarantee about send operations on zero-capacity channels, though.

Is this an acceptable compromise? What does everyone think?

cc @matklad

@ghost ghost added the api design Open design questions label Nov 1, 2019
@ghost ghost mentioned this issue Nov 1, 2019
@matklad
Copy link
Member

matklad commented Nov 1, 2019

@stjepang you obviously know better, but I find it quite amusing that there has to be a difference between threads and tasks here. Like, the threads don't magically rendezvous at a single moment in time (you might have only a single CPU).

Could you point out exactly why can't we deem the operation completed only after both sides made all the required moves?

EDIT: go read the sources is also a great answer here, if that's the easiest way to explain :)

@ghost
Copy link
Author

ghost commented Nov 1, 2019

I find it quite amusing that there has to be a difference between threads and tasks here. Like, the threads don't magically rendezvous at a single moment in time (you might have only a single CPU).

It's the same reason why scoped threads are easy to design, but scoped tasks (impossibly?) difficult.

The crucial difference is that we can force threads to complete operations, whereas we can't force futures:

  • We can force a thread to join spawned threads at the end crossbeam::scope() but can't do the same with tasks.
  • We can force a thread to complete a send()/recv() operation once the function has been called, but a future can always be dropped before its poll() returns Ready.

Could you point out exactly why can't we deem the operation completed only after both sides made all the required moves?

The guarantees I want to uphold are these:

  1. A send operation has completed iff SendFuture::poll() returns Ready.
  2. A receive operation has completed iff RecvFuture::poll() returns Ready.

That's not possible with zero-capacity channels, as far as I can tell. What I'm proposing in this issue is that we drop the first guarantee and just live with that decision.

The problem is that as both sides are making moves, we'll always reach a point where one side says "ok I've polled the future and it has returned Ready, which means the operation has completed" and the other side can then reply with "no wait, I changed my mind and am dropping the future rather than polling it, which means it is canceled".

@Matthias247
Copy link

Matthias247 commented Nov 2, 2019

zero capacity channels are possible with futures. You just need to rendezvous on the poll of the receiving side. futures-intrusive implements them. It's not even a special channel type - it's just the regular bounded channel configured with 0 capacity and exactly the same code.

Edit: I agree that it doesn't seem possible using the current definition of the Stream and Sink traits, if that is what you aim to support

@ghost
Copy link

ghost commented Mar 7, 2020

How about faithfully simplify the mpmc channel for the capacity = 1 case, then just call it one-capacity channel? Then send will complete as soon as the Sender's message is put in the channel, and there will be no extra guarantee or extra semantics to consider.

@ghost ghost closed this as completed Mar 15, 2020
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api design Open design questions
Projects
None yet
Development

No branches or pull requests

2 participants