Pattern for async methods

TL;DR: Has anybody figured out a nice pattern for having async object methods that need &mut self?

I'd like to do something like the following:

struct Foo { ... }

impl Foo {
    async fn bar(&mut self, ...) -> Result<...> { ... }
    async fn baz(&mut self, ...) -> Result<...> { ... }

    pub async fn run(self) {
        ...
        // Note that I don't even need multi-threaded parallelism, 
        // running all of Foo's futures on the same thread is fine.
        while let Some(event) = receive_event().await {
            match event {
                Event::Bar(args) => task::spawn_local(self.bar(args)),
                Event::Baz(args) => task::spawn_local(self.baz(args)),
            }
        }
        ...
    } 
}

Obviously, that isn't going to fly as written above, because the very first future will have borrowed self mutably (and also because spawn wants a 'static future).

The only thing that comes to mind is moving all mutable state into another struct, then writing code like:

async fn bar(self: Rc<Foo>) {
    let state = self.mutable_state.borrow_mut();
    ... // mutate state
    drop(state)
    nested_async_call_1().await?;
    
    let state = self.mutable_state.borrow_mut();
    ... // mutate state some more
    drop(state)
    nested_async_call_2().await?;
    ... // and so on
}

The above seems... not very elegant and boilerplate-y. I am wondering if anyone has figured a better was of writing this sort of code?
Perhaps there's a some clever crate out there, that can move all that borrow_mut() business outside of the async method's body?

1 Like

Perhaps the two futures only need access to different fields in self? If so, you can borrow fields separately and just use the join! macro. Alternatively if you can formulate the two operations as repeated calls to some sort of poll_foo and poll_bar call in the spirit of AsyncRead::poll_read, you can use poll_fn to work on both tasks simultaneously.

let mut a_state = None;
let mut b_state = None;
let (a, b) = poll_fn(|ctx| {
    if a_state.is_none() {
        if let Poll::Ready(a) = self.poll_foo(ctx) {
            a_state = Some(a);
        }
    }
    if b_state.is_none() {
        if let Pol::Ready(b) = self.poll_bar(ctx) {
            b_state = Some(b);
        }
    }
    if a_state.is_some() && b_state.is_some() {
        Poll::Ready((a_state.take().unwrap(), b_state.take().unwrap())
    } else {
        Poll::Pending
    }
}).await;

This works by each of the two halves only having mutable access to self while that half is being polled, meaning that they can both have exclusive access to all of self during their own poll.

It isn't just those two calls, the run() method is an event loop dispatcher. And state updates are pretty complex. Let's just assume that the object state cannot split into independently mutable parts. (Updated my post to make this clear).

Consider creating a separate object for each task you want to spawn, and use synchronization primitives to access shared data.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.