Should `Pin<&mut T>` be `Send`?

Confidence level: have no idea what I am talking about

Would the following impl make sense?

unsafe impl<T: Sync> Send for Pin<&mut T> {}

Intuitively, it feels like it should be true. We have

impl<T: Send> Send for &mut T

because an &mut can be used to smuggle values between the threads using mem::replace, but that's exactly the thing that is prevented by Pin. Note that we need T: Sync, because one can get an &T out of Pin<&mut T>, so Pin<&mut T> can be used for sending &Ts.

If this does make sense, can we also change compiler generated futures to have unsafe impl Sync? I don't think there's any API on the generated futures which is accessible only via &, so the futures seem to be trivially sync?

If both Pin: Send and async fn: Sync are true, does that mean that work-stealing executors can change

pub fn spawn<T>(future: T) -> JoinHandle<T::Output> 
where
    T: Future + Send + 'static,
    T::Output: Send + 'static

to

pub fn spawn<T>(mk_future: F) -> JoinHandle<T::Output> 
where
    F: FnOnce() + Send,
    F::Output: Future + Sync + 'static,
    F::Output::Output: Send + 'static

and allow interior interior mutability in async fns?

That is, once an executor gets a Pin<&mut Task>, they are free to poll the task from whatever thread, even if the Task holds something like &Rc across an .await?

(Inspired by all the recent discussion around local executors and TPC, but, most crucially, by https://blaz.is/blog/post/lets-pretend-that-task-equals-thread/)

As discussed here, I believe it should with some additional restrictions. Honestly, it's really baffling how some people defend the current system as something fundamentally needed for safety.

Pin doesn’t enforce anything for an Unpin type. (Pin also doesn’t enforce anything for a !Unpin type when you’re the one defining the type, so involving Unpin in the Send impl won’t help.[1])


On that note, typically (for most types) a T: Sync bound is considered more restrictive than T: Send, so I’m not entirely sure what is won here.[2] Types that implement Sync but not Send are relatively rare. (Notable exceptions include MutexGuard, as those need to be unlocked (via Drop) from the same thread that created them, to comply with restrictions on some OSs. Another exception would be types using something like SyncWrapper.)


  1. For some very reasonable example: some Foo<T>: !Unpin can contain a T-field not considered structurally pinned, so Pin<&mut Foo<T>> allows access to &mut T, and thus mem::replace on that field is possible. Of course the whole struct still implements Foo<T>: Send or Foo<T>: Sync on nothing else but T: Send or T: Sync, respectively ↩︎

  2. I haven’t tried understanding the spawn API you discuss at the end of your post yet, though :slight_smile: ↩︎

1 Like

Not if T: Unpin.

Without changing how thread-locals work? I don't think &Rc versus Rc makes a difference here. Both types are neither Sync nor Send, and &Rc is equally dangerous to Rc, since you could use Box::leak to get &'static Rc and stick that in a thread-local. Even in the intended(?) use case where the Rc is kept internal to the future, there wouldn't be a use for holding &Rc across an await unless the original Rc was also held across the await.

1 Like

Isn't that only in the same way that &mut T can give you &T? You can use as_ref on &Pin<&mut T>, so passing that around depends on Pin: Sync. Otherwise, if you Send the Pin<&mut T> directly, then you can convert to Pin<&T> just like &mut T can downgrade.

1 Like

Besides T: Unpin, another reason this doesn't work is Pin::set(), which is equivalent to assigning to the underlying &mut T.

3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.