How to understand the tokio worker threads?

Based on my understanding of worker threads, there would be multiple tasks/jobs are handled at the same time when multiple workers are configured, but the below code snippet shows that those jobs/tasks are handle in sequence, it seems that there is no difference when worker_threads is configured 1 or 4:

use tokio::runtime;
use tokio::time::{sleep, Duration};
use chrono::prelude::*;

fn main() {
    let runtime = runtime::Builder::new_multi_thread()
        .worker_threads(4)
        .enable_time()
        .on_thread_start(|| {
            println!("thread worker started");
        })
        .build().unwrap();
    runtime.block_on(async {
        let f = (0..100).map(|n|{
            tokio::spawn(async move {
                let utc: DateTime<Utc> = Utc::now();
                println!("time: {} value: {}", utc, n);
                sleep(Duration::from_secs(2)).await;
            })
        });
        for f in f {
            f.await;
        }
    });
}

output below, check the timestamp of every single line:

thread worker started
thread worker started
thread worker started
thread worker started
time: 2023-01-28 11:06:26.221026 UTC value: 0
time: 2023-01-28 11:06:28.224528 UTC value: 1
time: 2023-01-28 11:06:30.227851 UTC value: 2
time: 2023-01-28 11:06:32.232228 UTC value: 3
time: 2023-01-28 11:06:34.234764 UTC value: 4
time: 2023-01-28 11:06:36.239956 UTC value: 5
time: 2023-01-28 11:06:38.245066 UTC value: 6
time: 2023-01-28 11:06:40.248575 UTC value: 7
time: 2023-01-28 11:06:42.253783 UTC value: 8
time: 2023-01-28 11:06:44.259469 UTC value: 9
time: 2023-01-28 11:06:46.260561 UTC value: 10
time: 2023-01-28 11:06:48.266170 UTC value: 11

This is because iterators are lazy. Your code is doing this:

for i in 0..100 {
    let f = tokio::spawn(...);
    f.await;
}

Since you wait for one task to finish before spawning the next one, they don't run in parallel.

To fix it, collect your iterator into a vector before starting the loop.

let f = (0..100).map(...).collect::<Vec<_>>();
3 Likes

@alice Thanks for your reply, here comes another question, when code updated within vector:

let f = (0..100).map(...).collect::<Vec<_>>();

it looks like all of the 100 tasks will started&finished at the same time no matter how many worker_threads are configured in the runtime. :frowning:

The entire point behind async/await is that you can run lots of tasks on a single thread. Even if you use the current-thread runtime that uses only a single thread, you should still see all of them running at the same time.

You may find this blog post illuminating: Async: What is blocking?

For simulating a code that blocks for a period of time because of heavy computation, use std::thread::sleep instead of tokio::time::sleep.

Thanks, this is what I expected.

@alice thanks, is there any criteria how many async tasks can be scheduled on a single thread? I mean if I don't know the criteria I can not set the worker_threads correctly either.

When it comes to choosing how many worker threads to use, the important metric is how much work your async tasks are doing. You can have four tasks that do a lot of work and overload your runtime, or you can have a million tasks that are all idle and consume no resources other than memory. In the former case, you might want several worker threads to spread out your four expensive tasks on different threads. In the latter case, a single worker thread might be enough.

1 Like

tokio-metrics can be used to monitor key metrics of tokio tasks and runtimes.

Sorry about the typo :frowning:

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.