Do i still need "Send" if a Future is "Pin"

i think i've seen code like Pin<Box<dyn Future<Output=Result<...>> + Send + 'static>>

if you Box::pin() a future, does that future still need 'Send' ? I imagine "pinning" ensures the future will not be moved - so there's no "send" of anything between threads?

Or if i pin a future, i just have ensure it's Sync?

Pin is entirely unrelated to Send. Pin is concerned with where the future is located in memory, Send is concerned with which thread is using the future and which thread will free the future. A pinned future can be either Send or !Send depending on whether another thread is allowed to start using it.

Because all the methods of the Future trait take &mut self, whether a future is Sync or not is irrelevant - Sync only comes in to play when there are methods that take &self. You can trivially make a non-Sync future Sync by using a crate like sync_wrapper.

3 Likes

Does it mean if i do not specify "Send", another thread is not allowed to use the future and does this mean it won't be multi-threaded - i.e. only usable in a single thread?

Yes, that is correct. So unless you are using a single-threaded executor you should always be specifying Send in a dyn Future.

I recently encountered the example of certain locks being !Send, because the thread that unlocks a mutex or rw-lock must be (depending on implementation) the same thread which locked it. Thus, you cannot "send" a future that holds such a lock guard to a different thread. (Note: You still can use other locks that are "thread-safe" in an async context, such as tokio::sync::Mutex.)

This isn't related to synchronizing access to memory at all, but has to do with the operating system's API, in this example.

Interestingly, when you use the async keyword to automatically create a function that returns a future, your function's body will determine whether the returned Future will be Send or !Send. See also Complication #2 in "why async fn in traits are hard".

Heh, you have kind of a point but, despite the name, those are not the exact semantics of Send:

For T to be Send, it must be "safe" for any form of unique access to instances of type T to cross the thread boundaries.

So, given an instance: T, there are many ways to have a unique access to instance:

  • by value, T, which is what you meant by sending something across threads,

  • but also through indirection, such as Box<T>, Pin<Box<T>>, or &mut T, &mut &mut T, Pin<&mut T>, etc.

So, given that definition, it would be perfectly valid for anybody to feature the following custom Future type:

mod silly_lib {
    use ::std::{
        future::Future,
        hint::{unreachable_unchecked as trigger_undefined_behavior},
        pin::Pin,
        task,
    };
    mod thread { pub use ::std::thread::{*, ThreadId as Id}; }
    
    type PhantomNotSend =
        ::core::marker::PhantomData<
            ::std::sync::MutexGuard<'static, ()>,
        >
    ;

    pub
    struct MyFuture {
        src_thread_id: thread::Id,
        _not_send: PhantomNotSend,
    }
    
    impl MyFuture {
        pub
        fn new ()
          -> Self
        {
            Self {
                src_thread_id: thread::current().id(),
                _not_send: <_>::default(),
            }
        }
    }
    
    impl Future for MyFuture {
        type Output = ();
        
        fn poll (
            self: Pin<&'_ mut MyFuture>,
            _: &'_ mut task::Context<'_>,
        ) -> task::Poll<()>
        {
            if thread::current().id() != self.src_thread_id {
                unsafe {
                    ::static_assertions::assert_not_impl_all! {
                        MyFuture : Send
                    }
                    // SAFETY:
                    // Since `Self : !Send`, unique access to a `Self` can only
                    // happen from within the thread where it was created
                    trigger_undefined_behavior();
                }
            }
            ().into()
        }
    }
}

So, if you had a Pin<Box<dyn Future<…>>>, if it happened to be holding an instance of silly_lib::MyFuture, then it would be unsound to send that boxed future across threads.

While silly_lib::MyFuture may look contrived, there are perfectly valid examples of this pattern:

  • futures which would be smuggling safety-critical data through thread-local storage, which would thus have to be in sync in between the creation of the future and the moment it is polled;

  • futures which would be wrapping / bridging a low-level non-thread-safe C/FFI implementation (probably because the C implementation would be doing something along the lines of the previous point).

  • or, quite simply, non-thread-safe async bodies, such as:

    use ::core::cell::Cell;
    use ::futures::future::{FutureExt, BoxFuture, LocalBoxFuture};
    use ::std::sync::Arc;
    
    let counter1 = Arc::new(Cell::new(0));
    let counter2 = Arc::clone(&counter1);
    let future1: LocalBoxFuture<'static, ()> = async move {
        counter1.set(counter1.get() + 1);
    }.boxed_local();
    let future2: LocalBoxFuture<'static, ()> = async move {
        counter2.set(counter2.get() + 1);
    }.boxed_local();
    
    // BAD STUFF HERE: let's use `unsafe` to assert that
    // our futures are `Send` because they have been pinned:
    let future2: BoxFuture<'static, ()> = unsafe {
        ::core::mem::transmute(future2)
    };
    
    // Potential data race!
    {
        let join_handle = ::tokio::spawn(future2);
        let () = future1.await;
        let () = join_handle.await.unwrap();
    }
    

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.