withoutboats / romio

asynchronous networking primitives

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Need example for working with shared mutable states.

kvinwang opened this issue · comments

I was trying to make a supervisor app with async/await and failed.
I can express my thoughts in Python as following code, but i don't know how to do it in Rust.

import asyncio


class App:
    def __init__(self):
        self.api = ControlService(self)
        self.business = MyBussinessService()

    async def serve(self):
        await asyncio.wait([
            self.api.serve(),
            self.business.serve()
        ])


class ControlService:
    def __init__(self, app):
        self.app = app

    async def serve(self):
        while True:
            print('[ControlService   ] Waiting for user cmd...')
            cmd = await simulate_user_request()
            print("[ControlService   ] Received user cmd: {}".format(cmd))
            if cmd == "reset":
                # here we need to mutate the business and get the result
                ok = self.app.business.reset()
                if ok:
                    pass
                else:
                    pass
            else:
                pass


class MyBussinessService:
    def __init__(self):
        self._value = 0

    def reset(self):
        self._value = 0
        return True

    async def serve(self):
        while True:
            print('[BussinessService ] Business value: {}'.format(self._value))
            self._value += 1
            await asyncio.sleep(1)


async def simulate_user_request():
    await asyncio.sleep(5)
    return "reset"


if __name__ == '__main__':
    app = App()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(app.serve())
    loop.close()
commented

@kvinwang you could give memdb a try. It's a shared key-value store (dictionary) behind a read-write lock. Perhaps it's helpful!

you could give memdb a try. It's a shared key-value store (dictionary) behind a read-write lock. Perhaps it's helpful!

@yoshuawuyts Thanks for your info. But I don't think memdb will help much.

  1. The app is signle-threaded, so it's not necessary to use locks.
  2. Using memdb will erase the type info, and is not zero cost.
  3. It would need some other stuff to communicate between coroutines. Codes will become complicated.

I am wondering is it possible to write it as simple as the Python version I pasted above?

commented

@kvinwang ah okay, looks like I misinterpreted what you were going for. Apologies!


I am wondering is it possible to write it as simple as the Python version I pasted above?

I don't read Python much, so I'm not exactly sure -- but at a quick glance most of the snippet above would be possible with Rust's async/await syntax. There might be some minor differences because of the borrow checker, but nothing too major I reckon.

edit: having some rust code to look at might be useful if you're running into trouble. Also consider joining discord, and asking on the support channels!

commented

Also consider joining discord, and asking on the support channels!

Here is the link https://discord.gg/rust-lang

I suppose we should close the issue because it's not related to romio itself but to Rust lang

Also consider joining discord, and asking on the support channels!

Unfortunately, discord as well as many other net services is banned in our country. So I have to ask for help here.

having some rust code to look at might be useful if you're running into trouble.

At beginning, I was supposing the code should look like this:

#![feature(async_await, await_macro, futures_api, pin)]

use futures::{join};
use futures::executor::{self};

struct BusinessService {
    state: i32
}

struct ControlService<'a> {
    business: &'a mut BusinessService
}

impl BusinessService {
    async fn serve(&mut self) {
        loop {
            await!(async {
                /* do some business here */
            });
            self.state += 1
        }
    }

    fn reset(&mut self) {
        self.state = 0
    }
}

impl<'a> ControlService<'a> {
    async fn serve(&'a mut self) {
        loop {
            let cmd = await!(async {
                // wait for user request here
                "reset".to_owned()
            });
            match &cmd as &str {
                "reset" => {
                    self.business.reset()
                },
                _ => ()
            }
        }
    }
}

async fn serve() {
    let mut business = BusinessService { state: 0 };
    let mut api = ControlService { business: &mut business };
    let fut0 = business.serve();
    let fut1 = api.serve();
    join!(fut0, fut1);
}

fn main() {
    executor::block_on(serve());
}

Obviously, it wont compile.
Then, I put the state into a RefCell.

#![feature(async_await, await_macro, futures_api, pin)]

use futures::{join};
use futures::executor::{self};
use std::cell::RefCell;

struct BusinessService {
    state: RefCell<i32>
}

struct ControlService<'a> {
    business: &'a BusinessService
}

impl BusinessService {
    async fn serve(&self) {
        loop {
            await!(async {
                /* do some business here */
            });
            *self.state.borrow_mut() += 1
        }
    }

    fn reset(&self) {
        *self.state.borrow_mut() = 0
    }
}

impl<'a> ControlService<'a> {
    async fn serve(&'a self) {
        loop {
            let cmd = await!(async {
                // wait for user request here
                "reset".to_owned()
            });
            match &cmd as &str {
                "reset" => {
                    self.business.reset()
                },
                _ => ()
            }
        }
    }
}

async fn serve() {
    let business = BusinessService { state: RefCell::new(0) };
    let api = ControlService { business: &business };
    let fut0 = business.serve();
    let fut1 = api.serve();
    join!(fut0, fut1);
}

fn main() {
    executor::block_on(serve());
}

Now it compiles. But I don't think the RefCell working with async/await is a good pattern:

  1. Functions such as BusinessService::reset loss mutating semantic.
  2. Along with async/await, using RefCell will likely make panics easy in unmeant. Codes as following for example.
impl BusinessService {
    async fn serve(&self) {
        loop {
            // keep borrowing the state here will cause some other coroutine who call reset to panic.
            // It is easier to make this kind of mistake in async function than normal function.
            let state = self.state.borrow();
            await!(do_something_with_state(&state));
            *self.state.borrow_mut() += 1
        }
    }

    fn reset(&self) {
        *self.state.borrow_mut() = 0
    }

So, What's the best practice?

commented

RefCell working with async/await is a good pattern

Actually RefCell is a good pattern for a single-threaded code.

Yup, I assume that you can replace RefCell<T> with a Mutex<T> for multi-threaded executors. Though you will want to use the Mutex<T> that is provided in futures 0.3 instead of the one in std if you are using async/await.