Skip to content

Design of async channels #212

Closed
Closed
@ghost

Description

It's time to port crossbeam-channel to futures.

Previous discussions:

cc @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:

fn new(cap: usize) -> (Sender<T>, Receiver<T>);

struct Sender<T>;
struct Receiver<T>;

impl<T> Sender<T> {
    async fn send(&self, msg: T);
}

impl<T> Receiver<T> {
    fn try_recv(&self) -> Option<T>;
    async fn recv(&self) -> Option<T>;
}

impl<T> Clone for Sender<T> {}
impl<T> Clone for Receiver<T> {}

impl<T> Stream for Receiver<T> {
    type Item = Option<T>;
}

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 and tokio expose the mpsc::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 the futures crate that could be used as follows (this example is adapted from our a-chat tutorial):

loop {
    select! {
	    msg = rx.recv() => stream.write_all(msg.unwrap().as_bytes()).await?;
	    shutdown.recv() => break,
    }
}

What does everyone think?

Metadata

Metadata

Assignees

No one assigned

    Labels

    api designOpen design questions

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions