Unique reference doesn't require Sync, but shared reference does

In your case this involves the code generated by the compiler when unsugaring the async move future:

  • async move { // <-- captured env here: `{ c_my_trait: MT, my_num: MyNum }`
        let _ =
            c_my_trait
                .method(my_num)
                .await // <-- captured env here: `+= { &c_my_trait: &MT, temp: BoxFuture<'_, MyNum>}`
                       // `&c_my_trait` captured because `&self`
        ;
        // <- captured env here: just `()`
    }
    
  • I have annotated the three main control flow points here:

    1. Right when starting,

    2. At each .await point (here there is just one),

    3. Return point,

  • Each of these points require capturing some environment, which lead to an anonymous structure being automagically defined by the compiler, having some Future logic by translating the async sugar into .poll() calls based on this structure (this is similar (but more complex) to closures and their captured environment, see @RustyYato's blog post presenting it: Closures: Magic functions ).

  • You can glance at some quick-and-dirty approximation of this unsugaring in the Playground, which is quite ugly (I haven't factored in the different enum and just used a bunch of Options instead, but that was just to get the Pin-ning guarantees right (at least I hope so)).

Now, the interesting point here, is that because the value returned by .method() keeps borrowing the input self: &MT, Rust decides that it needs to keep a &MT around.

  • See the type mentioned in the error message containing &'r MT

    the type `[static generator@src/main.rs:24:59: 26:10 c_my_trait:MT, my_num:MyNum for<'r, 's, 't0> {&'r MT, MT, MyNum, std::pin::Pin<std::boxed::Box<(dyn core::future::future::Future<Output = MyNum> + std::marker::Send + 's)>>, std::pin::Pin<std::boxed::Box<(dyn core::future::future::Future<Output = MyNum> + std::marker::Send + 't0)>>, ()}]`
    
  • Personally, unless I have forgotten something and my playground link is wildy unsound, I don't think that capturing &MT in the between yield points is really necessary, but hey, that's what the compiler seems to be doing.

Finally, your whole Future, that is, the structure containing all this captured environment, needs to be Send-able to other threads. This, in turn, requires that each and every member of the structured / captured expression also be Sendable.
So it requires that the reference &MT be Send-able.

And a shared reference to something can be Sent to another thread if and only if this "something" is safe to share across threads, i.e.,

&T : Send ⇔ T : Sync

And since your generic MT isn't bounded by Sync, it could be !Sync which could make your code unsound for those "bad" choices of MT, hence the compile error here. You could make the code fail to compile only when someone used a !Sync MT by adding Sync to the bounds of MT.


Now, when you change .method() to take self: &mut MT, then the captured variable in the anonymous compiler-generated struct becomes &mut MT, and,
because one can trivially go from T to &mut T (and the other way around with ::core::mem::replace) within one thread, then:

&mut T : Send ⇔ T : Send

And you did have a Send bound on your generic MT.


Finally, regarding the intuition behind Sync and Send,

  • A type T is Sync if and only if it is sound to share across threads. This can only not be the case when there is shared mutation that does not use multi-threaded synchronization, e.g., Cell, RefCell.

  • A type T is Send if and only if it is sound to make it cross thread boundaries. In practice, this is the case:

    • when T = &U (or some wrapper around it) and U : Sync,

    • and when it is sound to interact (e.g., .drop()) with the T within a thread different from the one where it was created (imagine a struct interacting dealing thread-local stuff).


For a more detailed insight on sharing vs. mutation and the meaning of Sync, see this blog post of mine.

4 Likes