5

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?

kmdreko
  • 42,554
  • 6
  • 57
  • 106
nkkr
  • 101
  • 5
  • 1
    By the way, you `tokio::spawn` twice, once in main, and once more in the thread function itself – Alexey S. Larionov Nov 26 '20 at 11:35
  • task are not thread. I advice you to read the tokio documentation entirely tokio is not a thing you can make work without read a lot about it – Stargateur Nov 26 '20 at 12:09
  • 1
    Your `print_thread` function spawns a new task, then returns immediately without waiting for this task to run. Your main function exits as soon as all the `print_thread` functions have returned. – Jmb Nov 26 '20 at 12:46
  • 1
    Thank you. Actually, the code originally did some other processing, and I erased the parts not related to the question. This has resulted in code that calls tokio::spawn twice for no reason. As you all have pointed out, I don't seem to have a good understanding of tokio. I will read the documentation carefully. – nkkr Nov 27 '20 at 01:30
  • Related: [How can I stop an async program from terminating when any task is running?](/q/66995937/2189130) – kmdreko Sep 11 '22 at 15:23

1 Answers1

3

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);
    }
}
Ibraheem Ahmed
  • 11,652
  • 2
  • 48
  • 54
  • 1
    Thank you for sharing the cause as well as best practices. The methods you have taught me have worked well. As others have pointed out, I realized that my misunderstanding about tokio. I will read the tokio documentation carefully. – nkkr Nov 27 '20 at 01:29