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();
}