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

Ability to advance the Runtime or LocalSet until all tasks are Pending? #2443

Open
EkardNT opened this issue Apr 25, 2020 · 11 comments
Open
Labels
A-tokio Area: The main tokio crate C-feature-request Category: A feature request. M-runtime Module: tokio/runtime S-waiting-on-author Status: awaiting some action (such as code changes) from the PR or issue author.

Comments

@EkardNT
Copy link

EkardNT commented Apr 25, 2020

I would like to use tokio in a game I am making to implement coroutines that can execute across multiple game ticks. The way I would like this to work is that each tick of the main game loop, I would tell tokio's runtime to process all woken tasks until all futures have returned Pending, aka there is no more work to do at the present time. Then I would proceed with the rest of the game's update logic for that loop iteration. On the next loop iteration, the game would advance tokio's runtime again in the same manner. This would keep happening over and over until the program exits.

This might only make sense to do with a LocalSet, given that the full Runtime could potentially have multiple threads. There are also possibilities for more advanced control knobs around the basic all_pending stop condition, such as advancing until all_pending || num_tasks_processed > some_configurable_threshold, or having a timeout, etc. These could be useful in the context of a game where you want to tightly control the runtime of a single frame.

One possibility I've considered for achieving this using tokio's existing feature set is to call LocalSet::run_until with a future that simply awaits a yield_now call before completing.

// Totally untested code
fn every_game_tick() {
    runtime.block_on(local_set.run_until(async {
        yield_now().await;
    }));
}

The idea is that the yield_now() call would put the future at the back of the queue, allowing any other pending tasks to execute first. The problem with this approach is that it only does one pass through tokio's list of tasks, which may not be enough. For example, if one of the tasks spawns another one, I'm not sure whether this solution would cause that new task to be polled during the current game tick.

@Darksonn Darksonn added A-tokio Area: The main tokio crate C-feature-request Category: A feature request. M-runtime Module: tokio/runtime labels Apr 29, 2020
@Darksonn
Copy link
Contributor

This is probably not the best way to go around that. Using Tokio in a game can be a decent way to handle network connections, but it should probably go on a different thread than the main game loop.

@carllerche
Copy link
Member

There is currently no way to tick the runtime until all tasks are pending.

Assuming you are using I/O, one problem is, how do you wake up your game loop when an I/O event has been received?

@carllerche carllerche added the S-waiting-on-author Status: awaiting some action (such as code changes) from the PR or issue author. label May 12, 2020
@thombles
Copy link

I would find this feature very useful for simplified testing. I've spent considerable time trying alternative strategies to know when things are "done" but they've all proven unsatisfactory one way or another.

With this you could:

  1. Send into a channel then await quiescence. When that returns you know the value has been received and processed, without going to pains to expose that or being forced to use a fixed time delay.
  2. Advance mock time and know that the task that was waiting for it has completed, as have any cascading wakeups or tasks spawned off that one.

Conventionally, for testing without real delays you would mock out all this async stuff and interrogate each system synchronously but I feel like there's a real opportunity here for testing the async system as-is. Being able to wait for an idle runtime is a blunt tool but I keep coming back to it as the easiest way to reach this particular nirvana.

If this is not an appropriate thing for the real tokio Runtime (for good reasons I'm doubtless ignorant of), is there a way we could build such a thing ourselves out of the tools tokio gives us?

@dggmez
Copy link

dggmez commented Nov 4, 2020

@thombles @EkardNT Switch to smol. It includes two functions try_tick and tick to accomplish what you guys want:

https://docs.rs/smol/1.2.4/smol/struct.LocalExecutor.html#method.try_tick

https://docs.rs/smol/1.2.4/smol/struct.LocalExecutor.html#method.tick

@Darksonn
Copy link
Contributor

Darksonn commented Nov 4, 2020

There's no need to switch executor just to use LocalExecutor as a replacement for LocalSet. You can do that through the async-executor crate, and its tick method can be given to Runtime::block_on on a Tokio runtime and it will work similarly a Tokio LocalSet.

@hawkw
Copy link
Member

hawkw commented Nov 4, 2020

@thombles @EkardNT Switch to smol. It includes two functions try_tick and tick to accomplish what you guys want:

https://docs.rs/smol/1.2.4/smol/struct.LocalExecutor.html#method.try_tick

https://docs.rs/smol/1.2.4/smol/struct.LocalExecutor.html#method.tick

I don't think that smol::LocalExecutor's tick and try_tick methods actually have the desired behavior. The documentation for LocalExecutor::tick states (emphasis mine):

Run a single task.

Running a task means simply polling its future once.

This suggests that LocalExecutor::tick will poll the next task in the local executor's run queue, if one is scheduled. This is different from polling all tasks in the run queue until they are all pending, which is the behavior described above.

It's also worth noting Tokio's LocalSet implements Future, and its Future::poll implementation polls tasks spawned on the LocalSet:

fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
// Register the waker before starting to work
self.context.shared.waker.register_by_ref(cx.waker());
if self.with(|| self.tick()) {
// If `tick` returns true, we need to notify the local future again:
// there are still tasks remaining in the run queue.
cx.waker().wake_by_ref();
Poll::Pending
} else if self.context.tasks.borrow().owned.is_empty() {
// If the scheduler has no remaining futures, we're done!
Poll::Ready(())
} else {
// There are still futures in the local set, but we've polled all the
// futures in the run queue. Therefore, we can just return Pending
// since the remaining futures will be woken from somewhere else.
Poll::Pending
}
}

To advance the LocalSet until all tasks on it are pending, you could poll the LocalSet future.

Note that there is a limit on the number of tasks that the LocalSet will poll per tick:

const MAX_TASKS_PER_TICK: usize = 61;

Therefore, if you want to ensure that all tasks on the LocalSet are polled to Pending, you may need to track the number of tasks spawned on the LocalSet and poll it multiple times if that number is greater than the number of tasks polled per tick.

@piegamesde
Copy link

Tokio not being able to integrate with external event loops is a real show stopper :(

One thing that would already help me a lot would be a tokio::runtime::Runtime::block_on_async so that I can easily wrap futures in a Tokio context while keeping it single-threaded.

@notgull
Copy link
Contributor

notgull commented Jul 4, 2022

Seconding this. Having a method like Executor::run() on the Runtime or Handle type would be a boon for integration.

@alshdavid
Copy link

Also need this functionality to be able to embed Tokio into an external event loop.

@coder137
Copy link

By using smol AsyncTask have written a small module here: https://github.com/coder137/ticked-async-executor

The only limitation with TickedAsyncExecutor is that it does not have access to the tokio runtime internally, and cannot use tokio libraries which depend on that runtime (for example the Timer functionality etc)

This has been useful for me when I want to run/test an async task one tick at a time and synchronized with the main thread.

Feel free to review and suggest how the tokio runtime can be added to this

@alshdavid
Copy link

alshdavid commented Jun 28, 2024

That's awesome. Likewise I made something similar here.

I am using an adapted single threaded futures executor from the futures crate. I have an off-thread waker that ensures the main thread never blocks on waiting for futures to wake.

You cannot use tokio utilities though it does supports utilities that use std::futures like async_std

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-tokio Area: The main tokio crate C-feature-request Category: A feature request. M-runtime Module: tokio/runtime S-waiting-on-author Status: awaiting some action (such as code changes) from the PR or issue author.
Projects
None yet
Development

No branches or pull requests

10 participants