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 hook to be called when new pending task is available #44

Closed
Lupus opened this issue May 30, 2023 · 7 comments
Closed

Add hook to be called when new pending task is available #44

Lupus opened this issue May 30, 2023 · 7 comments

Comments

@Lupus
Copy link

Lupus commented May 30, 2023

I'm integrating LocalExecutor into OCaml single-threaded event loop, so far it works great, but what I'm missing is the ability for me to register some non-async thread-safe function that will notify OCaml single-threaded event loop (via a pipe) that it needs to call LocalExecutor::try_tick, I have to resort to calling LocalExecutor::try_tick "often enough", but that's obviously a suboptimal solution...

Maybe there is something in the API that already allows me to do this without busy-polling?

@notgull
Copy link
Member

notgull commented Jul 2, 2023

If you wait on tick() or run() using a custom waker you can integrate async-executor pretty well into external loops. Example:

// Create a waker that wakes up the Ocaml loop when it's ready.
fn wake_up_the_ocaml_loop() { /* ... */ }
let my_waker = waker_fn::waker_fn(|| wake_up_the_ocaml_loop());

// Block on the run() future using the Ocaml loop.
let tick = executor.run(futures_lite::future::pending::<()>());
futures_lite::pin!(tick);

loop {
    // Poll the tick future.
    tick.poll(&mut Context::new(&my_waker));

    // Block on the Ocaml loop until it's awake.
    block_on_ocaml_loop();
}

@Lupus
Copy link
Author

Lupus commented Jul 4, 2023

OCaml event loop is the main application that is being executed, Rust executor is auxilary and I want to notify OCaml event loop when there are runnable tasks on the Rust executor, so that OCaml side calls tick() to get progress on Rust tasks.

I've ended up with forking LocalExecutor from async-executor, inlining the multi-threaded executor, removing all the thread-related stuff, which made it really small and it started to work significantly faster on microbenchmark of promise ping-pong between OCaml and Rust. Also the forked version is now capable of running some callback when tasks are added to the queue.

It can be found here in ocaml-lwt-interop library that allows binding asynchronous Rust to asynchronous OCaml (original copyright notice for LocalExecutor is retained of course as this is clearly a derived work).

@notgull
Copy link
Member

notgull commented Jul 4, 2023

It seems to me from a brief glance that this is just a poor man's Waker. If you still wanted to use the mainline async-executor you could just make a waker that calls whatever you need to use to notify the OCaml loop, then poll() the run() future with that waker whenever the event source is invoked.

@Lupus
Copy link
Author

Lupus commented Jul 10, 2023

you could just make a waker that calls whatever you need to use to notify the OCaml loop, then poll() the run() future with that waker whenever the event source is invoked

Sorry, I do not quite follow this part. With mainline async-executor I can spawn tasks and greedily call try_tick() on the executor until it tells me that there're no more active tasks to run. Executor provides each task with scheduling function, that gets called when that task gets awakened, and scheduling function adds the task to active task queue. I need to catch this moment - when any of the tasks on the executor become runnable.

Can you please expand how I can do this with an additional waker and vanilla async-executor?

@valadaptive
Copy link

valadaptive commented Sep 23, 2023

I'm also running into this issue when trying to tie the executor into egui's event loop.

I have the following function which is run on each iteration of the event loop:

fn tick(&self) {
    loop {
        let tick = self.executor.tick();
        futures_lite::pin!(tick);
        let poll_result = tick.poll(&mut Context::from_waker(&self.waker));
        if let Poll::Pending = poll_result {
            // No tasks are scheduled. Exit the task loop.
            break;
        }
    }
}

self.waker is a waker function which wakes up the egui event loop when called.

However, if I spawn a new task onto the executor after all the other tasks have been run, it seems like that task won't be called until the above tick function is called by the event loop and sets up its custom waker.

Do I also need to wrap spawn to immediately call tick after the future is spawned? Is that what you mean by the "event source" being "invoked"? Or am I going about this wrong?

It's starting to feel less like I'm hooking up the executor to an external event loop and more like I'm writing my own half-baked executor on top of yours.

My mental model--correct me if I'm wrong--is that you run futures by giving them to an executor to schedule. A future needs to be executed using an executor.

So when we call tick on the executor and it returns a future itself, we then need another executor to execute that future. But if we had that executor, we could just spawn all our futures onto it in the first place.

@notgull
Copy link
Member

notgull commented Sep 25, 2023

You probably want something like this:

struct EguiExecutorBridge(Box<Pin<dyn Future<Output = ()> + Send + 'static>>);

impl EguiExecutorBridge {
    // Replace with Arc<Executor<'static>>, or however you're storing the executor
    fn new(ex: &'static Executor<'static>) -> Self {
        Self(Box::pin(async move {
            ex.run(futures_lite::future::pending::<()>()).await;
        })
    }

    fn tick(&mut self) {
        // Poll with your egui waker.
        let waker = waker_fn::waker_fn(|| wake_up_my_egui_loop());
        let mut context = Context::new(&waker);
        let _ self.0.as_mut().poll(&mut cx);
    }
}

This way, it sets up a local queue and keeps it running. Then, you keep the future around and poll it using the egui event loop as a reactor. For more information on how this can be done with GUI event loops, see winit-block-on's source code.

However, if I spawn a new task onto the executor after all the other tasks have been run, it seems like that task won't be called until the above tick function is called by the event loop and sets up its custom waker.

Yes, you need to call tick() or run() to drive the executor, especially after a new task has been queued.

Do I also need to wrap spawn to immediately call tick after the future is spawned? Is that what you mean by the "event source" being "invoked"? Or am I going about this wrong?

The task being pushed should call the waker and wake up the event loop.

My mental model--correct me if I'm wrong--is that you run futures by giving them to an executor to schedule. A future needs to be executed using an executor.

Not necessarily. Futures can be blocked on anywhere with block_on. However, if you want to run a lot of futures, then it's a good idea to use an executor.

@notgull
Copy link
Member

notgull commented Feb 17, 2024

Closing as I feel that this question has been sufficiently answered.

@notgull notgull closed this as completed Feb 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants