-
Notifications
You must be signed in to change notification settings - Fork 342
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
Design of async channels #212
Comments
Lgtm! What is the design for bridging sync/async worlds via channels? is this explicitly out of scope? Should the sync side just use block_on? |
Yes. I guess in theory we could add |
I really like this proposal! Though I think we can do without enum Event<T> {
Message<T>,
Shutdown,
}
let events = rx.recv().map(|ev| Event::Message(ev));
let shutdown = shutdown.recv().map(|_| Event::Shutdown);
let s = stream::join!(events, shutdown);
while let Some(ev) = s.next().await {
match ev {
Event::Message(msg) => stream.write_all(msg.unwrap().as_bytes()).await?,
Event::Shutdown => break,
}
} |
Bridging sync & async seems like a relatively important use-case though. I guess we can later add impl<T> Sender<T> {
fn into_blocking(self) -> blocking::Sender<T>
} I also like yosh's selectless approach to selection, because I think that putting large expressions into macros (what select encourages) is not super ergonomic, because those lazy IDE writers can't properly support code inside macros. |
I'm just a bit sad that the The |
@stjepang using This version I showed off is mostly how one could define a version that's suitable for even a public API. edit with some future lang support it could probably become a bit more concise also: enum Event<T> {
Message<T>,
Shutdown,
}
let events = rx.recv().map(|ev| Event::Message(ev));
let shutdown = shutdown.recv().map(|_| Event::Shutdown);
for match ev.await in stream::join!(events, shutdown) {
Event::Message(msg) => stream.write_all(msg.into()).await?,
Event::Shutdown => break,
} |
I still find the select macro is still a lot better in terms of distractions vs essence of what is trying to be accomplished, even if my ide doesn‘t format it well. |
Nice work! I really like this simplified API and I happen to have reached similar conclusions about bounded channels and infallible send operations. I'm out of the loop on if
I think this is relevant here because I think it's a closely related design choice, and affects whether Here's a sketch of a proposed revamped trait Sink<Item> {
fn poll_send(
self: Pin<&mut Self>,
with: &mut dyn FnMut() -> Item,
cx: &mut Context<'_>,
) -> Poll<()>;
} This would enable Sender to then implement struct Sender<T> { ... };
impl<T> Sender<T> {
fn try_send(&self, t: T) -> Option<T> {
let mut t = Some(t);
self.send_with(|| t.take().unwrap()).nonblocking();
t
}
fn try_send_with(&self, f: impl FnOnce() -> T) -> bool {
self.send_with(f).nonblocking().is_some()
}
async fn send(&self, t: T) {
SinkExt::send(self, t).await
}
async fn send_with(&self, f: impl FnOnce() -> T) {
let mut f = FnWrapper::new(f);
self.with(|()| f()).send(()).await
}
}
impl<T> Sink<T> for &Sender<T> {
fn poll_send(
self: Pin<&mut Self>,
with: &mut dyn FnMut() -> T,
cx: &mut Context<'_>,
) -> Poll<()> {
...
}
}
impl<T> Sink<T> for Sender<T> {
fn poll_send(
self: Pin<&mut Self>,
with: &mut dyn FnMut() -> T,
cx: &mut Context<'_>,
) -> Poll<()> {
Pin::new(&mut &*self).poll_send(with, cx)
}
} See here for a more developed, compiling version of this: https://play.rust-lang.org/?version=nightly&edition=2018&gist=4a11fe30cc3ea8d685b42400b88365c5 On another note, should impl<T> Receiver<T> {
fn try_recv(&self) -> Result<T, TryRecvError>;
async fn recv(&self) -> Option<T>;
}
enum TryRecvError {
Empty,
Disconnected,
} And regarding bridging sync/async, I've found the following to be useful: trait FutureExt: Future {
fn block(self) -> Self::Output where Self: Sized;
fn nonblocking(self) -> Option<Self::Output> where Self: Sized;
}
impl<T: ?Sized> FutureExt for T where T: Future {} this could replace a call to I'm also keen to nail down doing select-like operations with |
Instead of returning a match rx.try_recv() {
Ok(msg) => println!("message: {}", msg),
Err(TryRecvError::Disconnected) => println!("disconnected"),
Err(TryRecvError::Empty) => println!("empty"),
} we could also take the approach from crossbeam-channel v0.2: select! {
msg = rx.recv() => match msg {
Some(msg) => println!("message: {}", msg),
None => println!("disconnected"),
}
default => println!("empty"),
} Go doesn't even have a |
@stjepang ohh, I like that first variant a lot! @alecmocatta thanks for the detailed writeup. We've observed similar problems with the Perhaps it may be worthwhile to spin it out into its own crate so we can revisit this later? It seems like having a general interface for writing data beyond just |
I think the proposed API looks good to me! I appreciate the small API surface area. |
Are you planning to introduce optimized oneshot channels? It's an important use case that crossbeam doesn't handle very well now. As oneshot and normal channels have different use cases, maybe async-std could provide a separated api/type for optimized oneshot channels if it's not possible to make them as fast as mpsc with the current design. And this might be even more ergonomic for this use case. What do you think? |
Oneshot channels with the fastest possible performance seem like a pretty niche use case. Why not prototype them in a separate crate first? |
As a note aside, another interesting kind of channel is MPMC broadcasters, where each message sent is delivered to all receivers (not just the first). The use broadcaster::BroadcastChannel;
let mut chan = BroadcastChannel::new();
chan.send(&5i32).await?;
assert_eq!(chan.recv().await, Some(5));
let mut chan2 = chan.clone();
chan2.send(&6i32).await?;
assert_eq!(chan.recv().await, Some(6));
assert_eq!(chan2.recv().await, Some(6)); This reminds me a lot of Node.js's I think after the initial channels are done, it may be interesting to have an "unstable" broadcaster variant available too. |
@yoshuawuyts I'm glad you brought up the MPMC use case. That is definitely one area which still needs some love in the ecosystem, IMHO. I've had a lot of good experiences with https://github.com/ReactiveCocoa/ReactiveSwift building iOS apps and such. Rx/Reactive patterns are extremely useful when building large event-driven systems (data stores, apps &c); however, without MPMC/SPMC, it is difficult to implement some of these patterns. |
Related follow-up thread: #436 |
I'm skeptical that we'd want As an example, I tried to use the unstable channels to implement a naive version of a broadcast queue. It uses a single This is built with the intention of having many receivers being constantly added and removed over time as subscriptions are only temporarily needed. Thus, it is important to prune the collection when receivers stop subscribing. With a fallible send, I could intuitively do this while sending without requiring any extra channels to indicate shutdown:
but this is not possible with the current API. |
I 100% agree with @agausmann! I just build something using |
It's time to port
crossbeam-channel
to futures.Previous discussions:
send
in channels by default? crossbeam-rs/crossbeam#314cc @matklad @BurntSushi @glaebhoerl
Instead of copying
crossbeam-channel
's API directly, I'm thinking perhaps we should design async channels a bit differently.In our previous discussions, we figured that dropped receivers should disconnect the channel and make send operations fail for the following reason. If a receiver thread panics, the sending side needs a way to stop producing messages and terminate. If dropped receivers disconnect the channel, the sending side will usually panic due to an attempt of sending a message into the disconnected channel.
In Go, sending into a channel is not a fallible operation even if there are no more receivers. That is because Go only uses bounded channels so they will eventually fill up and the sending side will then block on the channel, attempting to send another message into the channel while it's full. Fortunately, Go's scheduler has a deadlock detection mechanism so it will realize the sending side is deadlocked and will thus make the goroutine fail.
In
async-std
, we could implement a similar kind of deadlock detection: a task is deadlocked if it's sleeping and there are no more wakers associated with it, or if all tasks are suddenly put to sleep. Therefore, channel disconnection from the receiver side is not such a crucial feature and can simplify the API a lot.If we were to have only bounded channels and infallible send operations, the API could look like this:
This is a very simple and ergonomic API that is easy to learn.
In our previous discussions, we also had the realization that bounded channels are typically more suitable for CSP-based concurrency models, while unbounded channels are a better fit for actor-based concurrency models. Even
futures
andtokio
expose thempsc::channel()
constructor for bounded channels as the "default" and most ergonomic one, while unbounded channels are discouraged with a more verbose API and are presented in the docs sort of as the type of channel we should reach for in more exceptional situations.Another benefit of the API as presented above is that it is relatively easy to implement and we could have a working implementation very soon.
As for selection, I can imagine having a
select
macro similar to the one in thefutures
crate that could be used as follows (this example is adapted from oura-chat
tutorial):What does everyone think?
The text was updated successfully, but these errors were encountered: