Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add higher level start_blocking api #191

Open
databasedav opened this issue Oct 15, 2023 · 3 comments
Open

add higher level start_blocking api #191

databasedav opened this issue Oct 15, 2023 · 3 comments

Comments

@databasedav
Copy link
Contributor

databasedav commented Oct 15, 2023

while the current web worker wrapper is much higher level than the gloo api, i think the callback channel semantics of start_blocking_with_output_task is still too low level, instead the api should simply take a blocking function and allow the user to .await its completion in a separate thread i.e. Task::start_blocking(blocking_function).await, which allows users to manage responses at a higher level using .await semantics instead of callbacks

this was actually pretty straightforward to implement using the existing api

async fn start_blocking<T: Send + 'static>(f: impl Fn() -> T + Send + 'static) -> T {
    let (sender, receiver) = futures_channel::oneshot::channel::<T>();
    Task::start_blocking_with_output_task(
        move |_, send_to_output| async move {
            send_to_output(f());
        },
        move |mut from_blocking| async move {
            if let Some(output) = from_blocking.next().await {
                sender.send(output);
            }
        },
    );
    receiver.await.unwrap_throw()
}
@databasedav databasedav changed the title add higher level spawn_blocking api add higher level start_blocking api Oct 15, 2023
@databasedav
Copy link
Contributor Author

databasedav commented Oct 15, 2023

just noticed there's already a Task::start_blocking that the other api's delegate to, we could call this new function spawn_blocking or to_thread instead

@MartinKavik
Copy link
Member

MartinKavik commented Oct 16, 2023

It's called start_blocking instead of spawn_blocking because spawn seems to be a special word just for starting a thread, process, command or task and I don't like special words too much and also programming languages and libraries use different names for starting/spawning such things (spawn, start, to_thread, run, exec, ..) so spawn is not a clear winner just because of consistency.
Another reason is that Futures aren't automatically started/run/executed like Promises and it was a bit confusing for me so I wanted to emphasize by the name that the Task isn't just created but actually started immediately by naming it start.
Also I've drawn inspiration from Elixir, where you have the method Task::start to just start a task, Task::async to start a task that has to be awaited and the function spawn that is basically a lower-level function that Task::start uses internally - so in MZ Task::start uses wasm_bindgen_futures::spawn_local internally as well.
I know Rust libs and std mainly use the word spawn but I think the reasons mentioned above are a little bit more important than that and MZ users can use both names without problems.

There is one bigger problem with Task::start_blocking - you can't use Mutables inside it that come from the main thread. It means basically all Mutables you would like to pass into blocking tasks and the same rule applies for everything potentially blocking the main thread (i.e. all locks, Mutexes, etc.). The app would just panic in runtime if you are a bit unlucky. And in many cases you just want to extract something from a global mutable or a signal, process it in the background task and then set the processed data to a mutable - then you very often have to use a regular task and channels to help you with it. However, I can imagine dropping such channels, "helper" tasks and the background task correctly won't be a super simple (because of a worker pool, messages just passing through channels, etc.) so I decided to include them all in the api to cover all possible cases now well enough. Then I can add an alternative to Elixir's Task::async and introduce something like Task::start_blocking_awaitable so you can move the data inside the blocking task and then wait for the result but even in this case you still have to use a Task::start as a wrapper to be able to call .await at all that makes the code very similar to already implemented start_blocking_with_output_task.

@databasedav
Copy link
Contributor Author

databasedav commented Oct 23, 2023

There is one bigger problem with Task::start_blocking - you can't use Mutables inside it that come from the main thread. It means basically all Mutables you would like to pass into blocking tasks and the same rule applies for everything potentially blocking the main thread (i.e. all locks, Mutexes, etc.). The app would just panic in runtime if you are a bit unlucky. And in many cases you just want to extract something from a global mutable or a signal, process it in the background task and then set the processed data to a mutable - then you very often have to use a regular task and channels to help you with it.

ya i think that's a fair concern, it should be made clear to the user that this is less of a thread, with the ability to communicate with other threads, and more of a background worker pool which can execute "atomic" (self-contained, no threading semantics, etc.) blocking operations; then the user would use the api with the mutable semantics expected e.g.

let value = Mutable::new(None);
Task::start(clone!((value) async move {
    value.set(Task::start_blocking_awaitable(blocking_function).await);
}))
value.signal().map(...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants