Async: Awaiting a future doesn't continuously poll, hangs program

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:

  1. Defined the struct Dog
  2. Implemented the std:::future::Future trait on Dog
  3. Instantiated the Dog future
  4. 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.

async fn test_dog() {
    let get_dog = Dog{position: 0, name: "Cheyenne"};
    
    get_dog.await;
    println!("{:#?}", get_dog);
}
Current position is: 0
Advancing to next character ...

Expected Result

The Dog future instance should be .poll()ed by the async executor (Tokio) until it is completed.

Question

What am I doing wrong here, that causes my future to not continuously be polled?

Additional Information

Rust 1.72.0
Ubuntu Server 22.04 LTS
Dependencies from Cargo.toml

[dependencies]
futures = "0.3.28"
sysinfo = "0.29.10"
tokio = { version = "1.32.0", features = ["full"] }
1 Like

You need a task Future that tells the executor when it should poll again via a Waker.

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.

1 Like

If I remove the Copy, Clone derivation, I get this error:

cannot move out of dereference of Pin<&mut Dog<'_>>
move occurs because value has type Dog<'_>, which does not implement the Copy trait

On this line:

let mut dog = *self;

Your failing to understand what async is.

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.

Maybe try
https://rust-lang.github.io/async-book/

You don't want to move out of the reference, or you'll be modifying a local variable (you'll never update the future that gets polled).

Use get_mut.

3 Likes

Thank you!!! That worked perfectly!

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.

1 Like

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.

1 Like

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? :laughing: That's actually a really good tip to watch out for, when evaluating how to fix errors.

1 Like

If you read carefully, the compiler isn't intending to tell you that lack of Copy is 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.

4 Likes

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).

1 Like

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.

2 Likes

Thanks for the correction.