Tokio运行时关闭后spawn任务未终止及嵌套spawn问题咨询
背景
我在测试Rust中使用tokio::spawn创建的任务在主线程终止时的行为。根据Tokio官方文档说明:
无法保证已生成的任务会执行完成。当运行时关闭时,所有未完成的任务都会被丢弃,无论该任务的生命周期如何。
但实际执行测试代码后,发现任务并未随运行时终止。
测试代码
Cargo.toml
[package] name = "tokio_test" version = "0.1.0" edition = "2021" [dependencies] tokio = { version = "1", features = ["rt", "test-util", "macros", "rt-multi-thread"]}
main.rs
use std::{thread, time}; #[tokio::main] async fn main() { println!("tokio test - main thread..."); tokio::spawn(async move { let mut i = 0; loop { println!( "tokio test - tokio spawn(worker) thread... sleep(1): {}/10", i ); thread::sleep(time::Duration::from_millis(1000)); i += 1; if i > 3 { break; } } }); thread::sleep(time::Duration::from_millis(1000)); println!("tokio test - main thread... closed "); }
输出结果
tokio test - main thread... tokio test - tokio spawn(worker) thread... sleep(1): 0/10 tokio test - main thread... closed tokio test - tokio spawn(worker) thread... sleep(1): 1/10 tokio test - tokio spawn(worker) thread... sleep(1): 2/10 tokio test - tokio spawn(worker) thread... sleep(1): 3/10
问题与解答
Q1:若希望任务随运行时关闭自动终止,是否必须通过任务的JoinHandle直接终止?
不是必须的。问题出在你使用了阻塞式的thread::sleep,它会完全占用Tokio的工作线程,导致Tokio运行时无法中断该线程或调度其他任务,自然也无法在运行时关闭时终止任务。
Tokio运行时只能对异步可取消的任务执行自动终止,这要求你使用Tokio提供的异步原语(比如tokio::time::sleep),而不是同步阻塞操作。只要任务能主动让出CPU(通过await),运行时关闭时就会自动取消所有未完成的任务。
修改后的任务代码示例:
use tokio::time; #[tokio::main] async fn main() { println!("tokio test - main thread..."); tokio::spawn(async move { let mut i = 0; loop { println!( "tokio test - tokio spawn(worker) thread... sleep(1): {}/10", i ); time::sleep(time::Duration::from_millis(1000)).await; // 异步sleep,让出CPU i += 1; if i > 3 { break; } } }); time::sleep(time::Duration::from_millis(1000)).await; // 同样替换为异步sleep println!("tokio test - main thread... closed "); }
运行这段代码,你会看到主线程关闭后,异步任务也会立即终止。
如果确实需要在任务中执行阻塞操作,应该使用tokio::task::spawn_blocking将其放到专门的阻塞线程池,这样不会影响Tokio调度器的正常工作,运行时关闭时也能正确终止这些阻塞任务。
Q2:在tokio::spawn内部嵌套调用tokio::spawn时任务无效,是否不允许此类嵌套调用?
Tokio完全允许嵌套调用tokio::spawn。你觉得任务无效,核心原因还是外层任务用thread::sleep阻塞了工作线程,导致Tokio调度器无法处理新生成的内层任务——调度器被阻塞线程卡住,根本没机会调度内层任务执行。
解决方法同样是把所有同步阻塞操作替换为Tokio的异步原语。修改后的嵌套Spawn代码示例:
use tokio::time; #[tokio::main] async fn main() { println!("tokio test - main thread..."); tokio::spawn(async move { let mut i = 0; loop { println!( "tokio test - tokio spawn(worker) thread... sleep(1): {}/10", i ); time::sleep(time::Duration::from_millis(1000)).await; i += 1; // 嵌套spawn tokio::spawn(async move { println!("worker thread in worker, sleep(1): 10"); time::sleep(time::Duration::from_millis(1000)).await; }); if i > 3 { break; } } }); time::sleep(time::Duration::from_millis(1000)).await; println!("tokio test - main thread... closed "); }
运行这段代码,内层任务就能正常被调度执行了。另外需要注意:内层tokio::spawn创建的任务是独立于外层任务的,外层任务结束后,只要运行时还在,内层任务会继续执行;当运行时关闭时,所有未完成的任务(包括内层)都会被终止。
内容的提问来源于stack exchange,提问作者yjlee

