I am trying to understand how to implement the Future trait on my own custom Rust struct. To emulate some "work", I am "iterating" over each character of the dog's name, before finally returning the Dog instance. All I'm doing is advancing the position field until it's equal the length of the dog's name field.
So far, I have:
Defined the struct Dog
Implemented the std:::future::Future trait on Dog
Instantiated the Dog future
Awaited the Dog future
#[derive(Clone, Copy, Debug)]
struct Dog<'a> {
name: &'a str,
position: u8,
}
impl<'a> Future for Dog<'a> {
type Output = Dog<'a>;
fn poll(self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {
let mut dog = *self;
println!("Current position is: {}", dog.position);
if dog.position < (dog.name.len()-1) as u8 {
println!("Advancing to next character ...");
dog.position += 1;
return Poll::Pending;
}
return Poll::Ready(dog);
}
}
Actual Result
When I .await the future, in an async function, I only get one iteration of the .poll() method being called, and the program hangs execution. I have to use ctrl+c to exit the program.
I believe you can get away with just cx.waker().clone().wake() before returning to signal "wake me up again as soon as convenient".
You have another problem in that you're implicitly copying your Dog and modifying the copy. Try removing the Copy implementation and cloning when Ready.
The infrastructure is there to allow to CPU to do work while at same time not waiting (blocking) for a notification (input available, output can be sent without blocking, timer has happed. etc)
If you don't have such notification then Future is not for you.
If you have a notification then a Future poll that returns Poll::Pending will also be linking the Context->Waker so wake can get called when notification happens.
I'm still confused about when exactly a "move" happens in Rust. I thought that by using as_ref() that I would just get a mutable reference to the Context. get_mut() works as expected though!
Here's the working implementation:
fn poll(self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {
let dog = self.get_mut();
println!("Current position is: {}", dog.position);
if dog.position < (dog.name.len()-1) as u8 {
println!("Advancing to next character ...");
dog.position += 1;
_cx.waker().wake_by_ref();
return Poll::Pending;
}
return Poll::Ready(dog.clone());
}
Result:
Current position is: 0
Advancing to next character ...
Current position is: 1
Advancing to next character ...
Current position is: 2
Advancing to next character ...
Current position is: 3
Advancing to next character ...
Current position is: 4
Advancing to next character ...
Current position is: 5
Advancing to next character ...
Current position is: 6
Advancing to next character ...
Current position is: 7
FYI, both _cx.waker().wake_by_ref() and _cx_.waker().clone().wake() work equivalently. I'm guessing that the former implementation is more efficient, because we aren't constantly cloning the waker(), but the result is the same.
The best way to figure it out is to go to the documentation for the method(s). From the signatures you can tell whether you will get a unique, mutable reference or a shared, "immutable" reference.
If you use an IDE, you can jump right into the documentation for any method.
The Waker is a vtable basically, a handful of pointers, so probably it doesn't really matter. But yeah wake_by_ref makes more sense, given that you're not storing it. Edit:It does matter more than that.
As @jonh noted, the main motivation for async are things that wait in the background (for data to arrive or whatever); those would hold on to a clone of the Waker until some progress could be made.
The move-or-not problems weren't really async related, other than having to deal with Pin I suppose. This has the same problem for example. Speedbumps like that are why one typically doesn't implement Copy on state-mutating structures, iterators being the canonical example. I.e. you can less-visibly/accidentally/unmindfully create a copy in a position that would be a move otherwise.
So basically, just because the compiler is complaining that the Copy trait isn't implemented, is NOT an invitation for me to go implement it? That's actually a really good tip to watch out for, when evaluating how to fix errors.
If you read carefully, the compiler isn't intending to tell you that lack of Copyis the problem: it tells you that the problem is you tried to move something that couldn't, and lack of Copy is why there was a move. This is a frequent confusion and perhaps there would be some way to better phrase the message — but it does need to mention Copy because sometimes the right answer is to implement Copy.
Sometimes it is, at other times, it isn't. It doesn't depend on what one particular compiler error message happens to tell you. It depends on whether implementing Copy makes sense for your type as a whole.
Any time a value is assigned to a new place. The new place may be a local variable, or an argument of a called function, or the referent of a reference (or other pointer-like type).
It does kinda matter because cloning a Waker involves calling one of those function pointers, thus dynamic dispatch. This is needed because it contains a data pointer whose ownership needs to be managed somehow, and that's done in that function call, usually with reference counting.