Why to prevent Send bound leakage?

When I read The gen auto-trait problem blogpost, I'm wondering why Send bound should be relaxed there?

For example, in the following snippet, Rc is held across yield point, thus it could be dropped on another thread, which breaks the !Send requirement on Rc. Also see "Tokio tutorial: Send bould".

// Current:
let iter = gen { // `-> impl Iterator + !Send`
    let rc = Rc::new(...);
    yield 12u32;
    rc.do_something();
};

// Proposed:
let iter = gen { // `-> impl IntoIterator + Send`
    let rc = Rc::new(...);
    yield 12u32;
    rc.do_something();
};

And I also learnt there has already been a trick to make the gen (or async) block Send via closure:

use std::rc::Rc;
fn is_send<T: Send>(_: T) {}

#[tokio::main]
async fn main() {
    let x = async || {
        let x = Rc::new(());
        async {}.await;
        drop(x); // Dropping Rc in another thread breaks `Rc: !Send`
    };
    
    is_send(x);
}

Why should it be allowed?

The Future isn't Send, the async closure is. This fails:

    is_send(x());

This also works:

fn main() {
    is_send(not_a_closure);
}

fn not_a_closure() -> impl Future<Output = ()> {
    async {
        let x = Rc::new(());
        async {}.await;
        drop(x);
    }
}

Is the first part still confusing? It's the difference between

fn gen_block_ish() -> impl Iterator {
    [Rc::new(())].into_iter()
}

and

struct Example;

impl IntoIterator for Example {
    type IntoIter = std::array::IntoIter<Rc<()>, 1>;
    type Item = Rc<()>;
    fn into_iter(self) -> Self::IntoIter {
        [Rc::new(())].into_iter()
    }
}

fn gen_block_ish() -> impl IntoIterator<IntoIter = impl Iterator> {
    Example
}

where the iterator is created lazily.

1 Like

A more complete example where GenExample represents some compiler-generated coroutine state.

1 Like

Aha. That's subtle! Thank you for the complete and convincing example! :heart:

Now I can understand the desired usage in yoshuawuyts's blogpost: it's fine for a gen block to be Send and moved to a thread. Rc is instantiated and used only on that thread. So it's fine to use Rc arcoss yield points.

let iter = gen { // `-> impl IntoIterator + Send`
    let rc = Rc::new(...);
    yield 12u32;
    rc.do_something();
};

// ✅ Ok
thread::spawn(move || {   // ← `iter` is moved
    for num in iter {     // ← `iter` is used
        println("{num}");
    }
}).unwrap();