Why do my tokio tasks quit before finishing?

Viewed 1239

The program below is supposed to print at regular intervals from multiple threads, but it is not working as I expected:

# Cargo.toml
[dependencies]
tokio = { version = "0.3", features = ["full"] }
use tokio::prelude::*; //0.3.4
use tokio::runtime::Builder;
use tokio::time::Duration;

fn main() {
    let rt = Builder::new_multi_thread()
        .enable_all()
        .thread_stack_size(3 * 1024 * 1024)
        .build()
        .unwrap();

    rt.block_on(async {
        tokio::spawn(print_thread(1));
        tokio::spawn(print_thread(2));
        tokio::spawn(print_thread(3));
        tokio::spawn(print_thread(4));
    });
}

async fn print_thread(thread_num: usize) {
    tokio::spawn(async move {
        println!("thread{}-start", thread_num);
        loop {
            tokio::time::sleep(Duration::from_millis(1000)).await;
            println!("thread{}-running", thread_num);
        }
        println!("thread{}-start", thread_num);
    });
}

When running this, I get:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.64s
     Running `target/debug/time_test`
thread1-start
thread2-start
thread3-start
thread4-start
$

I expected to see messages "threadN-running", but there is no further output. I don't know why the program suddenly quits. Can someone please tell me the cause?

1 Answers

The outer async block:

rt.block_on(async { ... })

does not join the spawned future, so it ends immediately after spawning it. This ends the block_on and also main, before the inner task has time to sleep and println!. The tasks are killed before they can finish executing.

You can solve this by wrapping the block_on in a never ending loop:

loop {
  rt.block_on(async {
    tokio::spawn(print_thread(1));
    tokio::spawn(print_thread(2));
    tokio::spawn(print_thread(3));
    tokio::spawn(print_thread(4));
  });
}

Now the tokio executor has all the time it needs to spawn the tasks, sleep, and print the messages to the console. However, this loop will be costly cpu wise. Another option is to use std::future::pending which was stabilized as of Rust 1.48. This will create a future which never resolves, representing a computation that never finishes:

rt.block_on(async {
 ...
});

std::future::pending().await;
unreachable!();

Also, as @AlexLarionov mentioned, you are actually spawning a task twice:

// 1st task
tokio::spawn(print_thread(1))

// 2nd task
async fn print_thread(thread_num: usize) {
    tokio::spawn(async move { ... });
}

This second spawn is unnecessary, and your print_thread function can be simplified to the following:

async fn print_thread(thread_num: usize) {
    println!("thread{}-start", thread_num);
    loop {
        tokio::time::sleep(Duration::from_millis(1000)).await;
        println!("thread{}-running", thread_num);
    }
    // Note that this is unreachable
    println!("thread{}-start", thread_num);
}

Note that for use case tokio::time::sleep is probably not the best option. To run something regularly on a schedule, you can use tokoi::time::interval:

async fn print_thread(thread_num: usize) {
    println!("thread{}-start", thread_num);
    let mut interval = tokio::time::interval(Duration::from_millis(100));
    loop {
      interval.tick().await;
      println!("thread{}-running", thread_num);
    }
}
Related