The Future: Send, and maybe move between threads by scheduler like tokio, but Pin<Future> forbids moving...
So, it is not Future itself getting moved, but &mut Future?
The Future: Send, and maybe move between threads by scheduler like tokio, but Pin<Future> forbids moving...
So, it is not Future itself getting moved, but &mut Future?
It's unclear what you are asking. If you are having trouble expressing yourself in English, please seek help from a native speaker.
If you are thinking that &mut T: Send
shouldn't require that T: Send
, then you are missing the fact that it is possible to move out of a mutable reference, so &mut T: Send where T: !Send
would be unsound, as it could be used to smuggle a non-Send
value through a reference to another thread.
No, it doesn't.
Sending and Moving are different concepts.
Send
is a trait that indicates that the ownership of the underlying type can be passed between threads and, more importantly, types that have not implemented it, can not.This is important for e.g. OS Mutexes, as they need to be unlocked on the same thread or mutable references, to avoid race-conditions.
Move
on the otherhand is not a Trait (yet?). The concept is used to talk about the moving of the underlying data to a different memory address. Pin
simply guarantees that the underlying data stays on the same address, so that it cannot be made invalid in between executions of the future. So even if the pointer Pin
is holding onto moves, the address it points to cannot change.
first of all, Pin
can only be used with pointers (or pointer-like types, Deref
), not futures themselves, so Pin<&mut Fut>
makes sense, but Pin<Fut>
does not.
well, technically the type definition of Pin
doesn't have a bound, but if P
is not a pointer-like type, then Pin<P>
is useless since all the impl blocks have trait bounds on P anyway.
and, Pin
doesn't forbits moving ownership between threads, at all.
I think you are conflating the transferring of ownership (Send
) and the moving of the piontee in memory location (Pin<Ptr>
).
given some pointer-like type P
, what Pin<P>
really prevents is the moving of the pointee
(i.e. <P as Deref>::Target
), but it doesn't say anything about the pointer itself. particularly, if P
is Send
, then Pin<P>
is also Send
.
your word is not clear to me, but if you are talking about the memory address, then yes, once the future gets pinned, it's memory location will never change. but I feel like saying the reference is "being moved" is not quite right either.
Unfortunately, we often use the word "move" for different concepts in Rust. It can be talking about the specific operation of moving a value from one place to another (as e.g. the assignment operator, or passing arguments to a function will to), which is implemented with a little memcpy; it can also be used to just mean "transfer of ownership".
Conflating the two is usually not an issue; the technical meaning of moving (i.e. the specific operation, on a value, involving a memcpy) does transfer ownership, and on the other hand, transfer of ownership is usually the only thing the programmer cares about, so it makes sense to use "move" more generally like that. E.g. you "move" a Box<Foo>
, which also transfers the ownership of the Foo
it contains, even though that value doesn't literally move into a different place in memory. Also even the move operation in the stricter sense is often optimized away.
But with pinning, stable memory addresses are something we care about. Well.. it depends; usually we-the-user don't really care either but just have to deal with the resulting limitations; but we-the-implementor-of-the-low-level-implementation-details-of-a-Future
-type might care. (The implementor is very often the compiler itself; specifically if you construct the future using the async
keyword.)
Values of type Pin<Box<SomeFutureType>>
are a very typical example of a value that can still be moved around freely, even though pinning is involved. The indirection of Box
makes sure that "transfer of ownership" can still easily happen, while the contained value of type SomeFutureType
does never actually move to a different place in memory. It's like you put your fragile valuable panting always-handle-with-care into a bank safe, and then get a key to the safe for access; you can give that key to a friend, and handle it with no care at all; you can sell the key (perhaps in combination with some piece of paper proving ownership) and thus transfer ownership of the painting, without any risk of damaging it.
I saw this in articles about work stealing, too, which is used by tokio I remember. However, here is better to be send
instead of move
.
There are 3 points:
Pin
: A wrapper, related to !Unpin
.!Unpin
:fn foo<T: Unpin>(t: &mut T); // Type is not pinned, type can be moved.
fn foo<T: !Unpin>(t: &mut T); // Type is not pinned, type can be moved.
fn foo<T: Unpin>(t: Pin<&mut T>); // Type is pinned, type can be moved.
fn foo<T: !Unpin>(t: Pin<&mut T>); // Type is pinned, type can't be moved.
from here
Move
: In the context of pin
, move
is about address in memory, not the ownership.To draw a conclude, Tokio
just needs the Fut: Future
to be + Send
.
As for Pin
, Future
trait needs it due to self-referential
types, or more concrete, the state machine of the Fut
needs being pinned. You can read this brilliant article to understand why Fut: Future
should be a state machine and need pinning.
Yes, that is what I mean, the future does not change it's address, but the &mut reference send to the other thread (if you are not comfortable about the "move").
But there is still problem left; Send between thread requires the &'a mut Future : 'static, which means 'a : 'static, in turn, means Future having a static lifetime.
This means the Future can only be stored at heap, not stack.
But I have test the following code:
struct XXX;
imp future for XXX {
fn poll(Pin<&mut XXX>, Context<'_>) -> Poll<Output> {
let n1 = 0;
println!("{:p}", &n1);
}
};
async {
let n2 = 0;
println!("{:p}", &n2);
tokio::task::sleep().await;
println!("{:p}", &n2);
XXX.await;
}
I am sure n1 locates at stack, the result shows n2 is stored near n1, e.g. n2 is stored at stack too.
Because there is tokio::task::sleep().await;
, there is a yield, e.g. n2 must have been captured into the future.
From all these, should it be correct to state that:
It is not impossible, but I don't feel comfortable about these, it is too complex for the compiler to get the whole picture of variables reference relationship and adjust the address when they are restored back to stack
I think the real implentation should be different, and what is more:
async {
let n2 = 0;
println!("{:p}", &n2);
let mut fut1 = tokio::task::sleep();
let fut2 = unsafe { Pin::new_unchecked(&mut fut1) };
fut2.await;
println!("{:p}", &n2);
XXX.await;
}
Then where is the fut1 stored given n2 is at stack?
I think it is reasonable fut1 is at stack too.
Then a yield happens at fut2.await, then both the fut1, fut2 have to be saved into the future generated from the async block because the stack will be popped, thus a movement occurs.
But fut1 has been pined, it can't be moved anymore
There are barriers to having non-blocking scoped tasks that people desire.
But Send
does not require a 'static
lifetime. We have (blocking) scoped threads in std
now. You can send non-'static
futures to another thread and then, say, poll it to completion in a loop. (For now at least, you'll have to give up on parallelism or similar, as talked about in the article.)
That code won't compile, because fut
is an undeclared variable, and if you meant fut1
, Pin::new
doesn't work on types that are not Unpin
, such as tokio::time::Sleep
. You'd need to use tokio::pin!
or similar, which creates a hidden variable for the pinned value, which definitely will be put in the Future
and not moved. Data in a Future
is not implicitly moved between stack and heap (except insofar as this is permitted for the optimizer to change, e.g. simple integers).
I modified the code you point out.
But the time of be put in the Future
is in the middle of fut2.await, when the underlying poll returns a Pending, is this right?
If it is, then the pointee of the fut2 can't be put into the Future, because fut2 has pinned it.
fut2
is of type Pin<&mut Future>
, which has a delegating Future
implementation:
impl<P> Future for Pin<P>
where
P: DerefMut,
<P as Deref>::Target: Future,
When fut2.await
is executed, the Pin<&mut Future>
is moved into the async block future and becomes pinned. But that's just moving a pointer, which in this case points to the already pinned Sleep
future fut1
that's already stored in that same async block future. So, nothing that is !Unpin
is getting moved.
when sleep.await returns Pending at first time, the executing yieds, then in turn the asyn block containing fut1, fut2 yields ... and all parent async block yield.
That is, not just the stack of seep,poll popped up, all the parent stack in path popped up too. So when sleep yieds, there is no room for fut1, it has to move to some place else --- and that happens when fut2.await, after fut1 getting pinned.
fut1
was never on the stack. fut1
is a variable that crosses an await point, so the compiler knows it has to allocate space for it in the future, and it stays put there for its entire existence. In async, let
does not necessarily mean a stack allocation.
You can learn more about the actual contents of async block futures by getting the compiler to dump their layouts — just run
cargo +nightly rustc -- -Zprint-type-sizes
and look for the string "print-type-size type: `{async " in the output to find your async block and async fn types. This will show you exactly what data has been put in the future.
See
I have comment the variables address from 1 to 7, where 1 having the largest address.
The result shows, all the variables at executing time are at stack.
You’re using Runtime::block_on()
, which doesn't move the future it is given to the heap (because it doesn’t need to). If you use Runtime::spawn()
instead, you will get different addresses.
But the real question is: Did the stack popped up when sleep, the following is the call stack before and during sleeping, the trace shows the stack did popped up -- then, the question is: where is the fut1 now?
Thread 1 "proxy" hit Breakpoint 1, proxy::main::{{closure}}::{{closure}} () at src/main.rs:21 21 let mut f1 = tokio::time::sleep(tokio::time::Duration::from_millis(10000)); (gdb) bt #0 proxy::main::{{closure}}::{{closure}} () at src/main.rs:21 #1 0x00005555555767ab in proxy::main::{{closure}} () at src/main.rs:34 #2 0x000055555556fa54 in tokio::runtime::park::CachedParkThread::block_on::{{closure}} () at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.22.0/src/runtime/park.rs:272 #3 0x000055555556f417 in tokio::runtime::coop::with_budget (budget=..., f=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.22.0/src/runtime/coop.rs:102 #4 tokio::runtime::coop::budget (f=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.22.0/src/runtime/coop.rs:68 #5 tokio::runtime::park::CachedParkThread::block_on (self=0x7fffffffbcff, f=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.22.0/src/runtime/park.rs:272 #6 0x0000555555578c20 in tokio::runtime::context::BlockingRegionGuard::block_on (self=0x7fffffffc280, f=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.22.0/src/runtime/context.rs:255 #7 0x000055555557931d in tokio::runtime::scheduler::multi_thread::MultiThread::block_on (self=0x7fffffffd140, handle=0x7fffffffd168, future=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.22.0/src/runtime/scheduler/multi_thread/mod.rs:66 #8 0x0000555555577436 in tokio::runtime::runtime::Runtime::block_on (self=0x7fffffffd138, future=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.22.0/src/runtime/runtime.rs:281 #9 0x000055555557890b in proxy::main () at src/main.rs:9 (gdb) c Continuing. 0x7fffffffb700 ^C Thread 1 "proxy" received signal SIGINT, Interrupt. syscall () at ../sysdeps/unix/sysv/linux/x86_64/syscall.S:38 38 ../sysdeps/unix/sysv/linux/x86_64/syscall.S: 没有那个文件或目录. (gdb) bt #0 syscall () at ../sysdeps/unix/sysv/linux/x86_64/syscall.S:38 #1 0x00005555556252f7 in parking_lot_core::thread_parker::imp::ThreadParker::futex_wait (self=0x7ffff7c254d0, ts=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/parking_lot_core-0.9.5/src/thread_parker/linux.rs:112 #2 0x00005555556250f4 in <parking_lot_core::thread_parker::imp::ThreadParker as parking_lot_core::thread_parker::ThreadParkerT>::park (self=0x7ffff7c254d0) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/parking_lot_core-0.9.5/src/thread_parker/linux.rs:66 #3 0x000055555562e76e in parking_lot_core::parking_lot::park::{{closure}} (thread_data=0x7ffff7c254b0) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/parking_lot_core-0.9.5/src/parking_lot.rs:635 #4 0x000055555562d61f in parking_lot_core::parking_lot::with_thread_data (f=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/parking_lot_core-0.9.5/src/parking_lot.rs:207 #5 parking_lot_core::parking_lot::park (key=93824993828360, validate=..., before_sleep=..., timed_out=..., park_token=..., timeout=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/parking_lot_core-0.9.5/src/parking_lot.rs:600 #6 0x0000555555627279 in parking_lot::condvar::Condvar::wait_until_internal (self=0x5555556d9e08, mutex=0x5555556d9e10, timeout=...) at src/condvar.rs:333 #7 0x00005555555e512e in parking_lot::condvar::Condvar::wait (self=0x5555556d9e08, mutex_guard=0x7fffffffb2d8) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/parking_lot-0.12.1/src/condvar.rs:256 #8 0x00005555555c752c in tokio::loom::std::parking_lot::Condvar::wait (self=0x5555556d9e08, guard=...) at src/loom/std/parking_lot.rs:150 #9 0x00005555555b0846 in tokio::runtime::park::Inner::park (self=0x5555556d9e00) at src/runtime/park.rs:106 #10 0x00005555555b1277 in tokio::runtime::park::CachedParkThread::park::{{closure}} (park_thread=0x7ffff7c25478) at src/runtime/park.rs:244 #11 0x00005555555b13d6 in tokio::runtime::park::CachedParkThread::with_current::{{closure}} (inner=0x7ffff7c25478) at src/runtime/park.rs:258 #12 0x00005555555c96b0 in std::thread::local::LocalKey<T>::try_with (self=0x5555556c5528, f=...) at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/std/src/thread/local.rs:270 #13 0x00005555555b1326 in tokio::runtime::park::CachedParkThread::with_current (self=0x7fffffffbcff, f=...) at src/runtime/park.rs:258 #14 0x00005555555b123a in tokio::runtime::park::CachedParkThread::park (self=0x7fffffffbcff) at src/runtime/park.rs:244 #15 0x000055555556f50e in tokio::runtime::park::CachedParkThread::block_on (self=0x7fffffffbcff, f=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.22.0/src/runtime/park.rs:276 #16 0x0000555555578c20 in tokio::runtime::context::BlockingRegionGuard::block_on (self=0x7fffffffc280, f=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.22.0/src/runtime/context.rs:255 #17 0x000055555557931d in tokio::runtime::scheduler::multi_thread::MultiThread::block_on (self=0x7fffffffd140, handle=0x7fffffffd168, future=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.22.0/src/runtime/scheduler/multi_thread/mod.rs:66 #18 0x0000555555577436 in tokio::runtime::runtime::Runtime::block_on (self=0x7fffffffd138, future=...) at /home/zylthinking/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.22.0/src/runtime/runtime.rs:281 #19 0x000055555557890b in proxy::main () at src/main.rs:9
Interesting part is here:
As you can see, these stack frames are identical (well, almost - the instruction pointer on the top frame is, of course, different, but the frame itself is the same). And future is stored in block_on
's stack frame, so it isn't moved until block_on
returns (and at that moment it is deallocated with the frame).
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.