Running Tokio on current thread for time-sliced IO

Recently I was playing with C++ coroutines. Boost::asio has interface that works pretty good with co_await for networking but using boost::asio is painful. So I want something like this in Rust.

I want time-sliced IO. Imagine a game where I run every step of the pipeline (input, physics, scripts, rendering) and with all the time I have left I run IO handling.

I want to use tokio::task::LocalSet to simulate this behaviour. I'd local.run_until with a future made from time::sleep() to define how big the time-slice is (might vary every frame). I'd spawn two tasks, one for handling input and the other one for output.

First question - does it even makes sense to use tokio like this?

Second question - performance... I expect tokio is super smart but I want to make sure - if there are 2 tasks hanged on IO (no data is coming) and 1 task that was just spawned for timing run_until(time::sleep(50ms)) tokio is going to sleep right? I expect that time-slice to be pretty large, most likely larger than time required to process everything. So I hope thread will sleep and won't be scheduled for the entire duration unless something really comes on the IO.

Here's some code that I tested this on:

use tokio::{task, time};


#[tokio::main(flavor = "current_thread")]
async fn main() {
    

    let local = task::LocalSet::new();

    local.spawn_local(async move {
        loop {
            time::sleep(time::Duration::from_millis(10)).await;
            println!("Task 1. working! Doing some crazy IO.");
        }
    });

    local.spawn_local(async move {
        loop {
            time::sleep(time::Duration::from_millis(20)).await;
            println!("Task 2. working! Doing even crazier IO!");
        }
    });

    local.run_until(async move {
        time::sleep(time::Duration::from_millis(50)).await;
    }).await;

    println!("Break!");
    println!("Do other things, we don't care about IO!");

    local.run_until(time::sleep(time::Duration::from_millis(50))).await;

    println!("Finish");
}

I think the main idea is good, but I would move the blocking stuff out of the runtime on principle. This way Tokio is aware of when it will not be running. I'm not sure if this is any different in practice, but it'll help keep things organized.

use tokio::{task, time};

fn main() {
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap();
    let local = task::LocalSet::new();

    local.spawn_local(async move {
        loop {
            time::sleep(time::Duration::from_millis(10)).await;
            println!("Task 1. working! Doing some crazy IO.");
        }
    });

    local.spawn_local(async move {
        loop {
            time::sleep(time::Duration::from_millis(20)).await;
            println!("Task 2. working! Doing even crazier IO!");
        }
    });

    rt.block_on(async {
        local
            .run_until(time::sleep(time::Duration::from_millis(50)))
            .await
    });

    // This part is not running inside the runtime
    println!("Break!");
    println!("Do other things, we don't care about IO!");

    rt.block_on(async {
        local
            .run_until(time::sleep(time::Duration::from_millis(50)))
            .await
    });

    println!("Finish");
}

Normally when you have block_on(async { stuff.await }) you can translate that into block_on(stuff) (like you did with run_until), but run_until needs to be called while block_on is running, so the async block is necessary.

For the second question: yes, tokio will generally have the OS wake up the thread when the next event is available.

All that being said, I wouldn't expect this to work any better than just running Tokio on its own thread, std::thread::sleeping the main one in between ticks, and using channels.

1 Like

All that being said, I wouldn't expect this to work any better than just running Tokio on its own thread, std::thread::sleeping the main one in between ticks, and using channels.

I only want it to run as good as it can in this scenario, and as cheap as it can. I want to use single OS thread per one instance.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.