Mutating data owned by future between polls

Below is a rough outline of what I'm trying to achieve:

#![feature(future_poll_fn)]

use std::{
    future::{Future as _, poll_fn},
    pin::Pin,
    task::Poll,
};

async fn run_all() -> ! {
    let mut x = X;
    let mut running_x = Box::pin(x.run());
    let mut running_y = Box::pin(Y.run(&mut x));

    poll_fn(|ctx| {
        Pin::new(&mut running_x).poll(ctx);
        Pin::new(&mut running_y).poll(ctx);
        
        Poll::Pending
    })
    .await
}

struct X;

impl X {
    async fn run(self) -> ! {
        loop {
            // Do something important.
        }
    }
    
    async fn mutate(&mut self) {}
}

struct Y;

impl Y {
    async fn run(self, x: &mut X) -> ! {
        loop {
            x.mutate().await;
            // Do something important.
        }
    }
}

Essentially, I need to construct a Future that alternates polling between running_x and running_y. The difficulty here is that running_y mutates x when polled, for which it needs exclusive access. Intuitively, running_x only accesses x when polled itself, so I shouldn't need any locks.

My first thought was to use an observer pattern:

impl X {
    async fn run(self, access: impl Fn(&mut Self) -> impl Future<Output = ()>) -> ! {
        loop {
            access(&mut self).await;
            // Do something important.
        }
    }
}

But, it's unclear how I would call X::run. x.run(|it| async { y.run(it).await }) won't work because the futures wouldn't be polled in alternation.

My second thought was to use interior mutation all the way down. That is, all of X's fields would be wrapped in Cells, and X::run would immutably borrow self. I've implemented this solution, and it does work, but this imposes heavy implementation restrictions on the types X contains (which there are many of).

I feel like there's something simple I'm just not seeing. Maybe it's because I'm really tired. If anyone can point me in the right direction, that would be a great help. Thanks in advance!

You can do this by storing the data in a RefCell<T> and giving each future an &RefCell<T>, however this makes your future non-Send, which is usually not acceptable. Unfortunately, there's no safe way to avoid this problem because the correctness requirement is that all immutable references to the RefCell<T> must always be on the same thread, and there's no way to convince the compiler that if you move the future from one thread to another, that moves all of the immutable references simultaneously, and not just some of them.

1 Like

Ah, makes sense. I think I'll have to drop to unsafe then. Thank you for the help!

Actually, I don't think there's a good unsafe solution to this. :frowning:

I previously failed to realize why interior mutability works here. In my understanding, it's because (a) X cannot mutate itself without calling Cell::set, (b) Cell:set mutates by value and thus does not expose an exclusive reference to the underlying wrapped data---which could otherwise be held across an await point and break aliasing rules if X were to be externally mutated (e.g., by Y)---(c) X cannot even access such possibly mutable data without Cell::get, and (d) Cell:get accesses by value, which also preserves the aliasing rules. For these reasons, I think atomics are another (albeit undesirable) solution.

I guess interior mutability is the way to go, then? I'm not sure a solution exists that doesn't force run_all to expose implementation details to X and Y, which is especially unfortunate as I intend them to live in separate crates.

What I may do is introduce struct Z such that the dependency graph looks like: Y -> Z <- X. Z should be provided by the crate that implements run_all, keeping the implementation details mostly contained---of course, it is difficult to explain to X why much of (what it considers) its "owned" data is externally mediated. Fortunately, traits can at least help to obscure from X the fact that Z also exposes functionality to Y.

P.S. If you're wondering why I'm trying to write such weird code in the first place, it's because I'm trying to replicate this with async. :grin:

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.