< 返回版块

yuyidegit 发表于 2025-02-21 14:21

use tokio::time::sleep;
use std::io::Read;

#[tokio::main]
async fn main() {
    let h = tokio::spawn(async move {
        let _ = tokio::task::spawn_blocking(|| {
            let mut buf = [0, 0, 0, 0];
            std::io::stdin().read_exact(&mut buf)
        })
        .await;
    });
    sleep(std::time::Duration::from_secs(3)).await;
    println!("1234");
}

这段代码会在打印后hang住进程,这是为什么呢? 好像tokio的runtime一直没退出

好像spawn_blocking就是会卡住不让退出,这也太坑了.程序里用了个tokio::io::stdio,给我整的怀疑人生了

评论区

写评论
Unic 2025-02-23 17:17

专业解析(技术术语版)

这段代码的卡死问题本质是 Tokio运行时生命周期管理阻塞操作未完成 的冲突,具体流程如下:

代码执行流程分解

#[tokio::main] // (1) 启动多线程Runtime(默认worker_threads=CPU核数)
async fn main() {
    let h = tokio::spawn(async move { // (2) 创建子任务h
        let _ = tokio::task::spawn_blocking(|| { // (3) 生成阻塞任务
            let mut buf = [0, 0, 0, 0];
            std::io::stdin().read_exact(&mut buf) // (4) 同步阻塞读
        }).await; // (5) 等待阻塞任务完成
    });
    sleep(std::time::Duration::from_secs(3)).await; // (6) 主任务休眠3秒
    println!("1234"); // (7) 打印输出
} // (8) 主任务结束,Runtime尝试关闭

关键问题节点

  1. Runtime关闭机制:当最后一个异步任务结束时,Tokio会开始关闭Runtime,但会等待所有 spawn_blocking 线程池中的任务完成(源码见 tokio/src/runtime/blocking/pool.rsshutdown 方法)
  2. 阻塞读卡死stdin().read_exact() 会无限期阻塞等待输入,导致 spawn_blocking 线程无法结束
  3. 任务依赖链:主任务退出后,子任务 h 仍在等待 spawn_blocking,而 spawn_blocking 又卡在同步IO

解决方案代码

#[tokio::main]
async fn main() {
    let h = tokio::spawn(async move {
        let handle = tokio::task::spawn_blocking(|| {
            let mut buf = [0; 4];
            std::io::stdin().read_exact(&mut buf)
        });
        
        // 方案1:设置超时中断
        tokio::select! {
            _ = handle => {}
            _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {
                handle.abort(); // 5秒后强制终止
            }
        }
    });

    tokio::time::sleep(std::time::Duration::from_secs(3)).await;
    println!("1234");
    
    // 方案2:主动关闭Runtime
    tokio::runtime::Handle::current().shutdown_timeout(std::time::Duration::from_secs(1));
}

通俗解释(生活化类比版)

想象你开了一家快递站(Tokio Runtime),主线程是老板,spawn_blocking 是专门处理重物的搬运工小组。

事件还原:

  1. 老板接到个任务:要等用户输入4个数字(read_exact),于是专门派了个搬运工去门口等着(spawn_blocking
  2. 老板自己先睡3小时(sleep(3).await),然后贴出"1234"的公告(println
  3. 老板准备下班关店,但发现:
    • 搬运工还在大门口傻等用户输入(没有实际输入发生)
    • 根据公司规定:必须等所有搬运工收工才能锁门

卡死原因:

  • 搬运工的工作性质:一旦开始等用户输入,就不能强行拉走(同步阻塞操作不可取消)
  • 公司制度问题:默认要等所有搬运工完成工作才能关门(Runtime默认等待所有阻塞任务)

解决方案:

  1. 给搬运工配个闹钟(超时机制):

    // 最多等5分钟,不来人就撤
    tokio::select! {
        _ = 搬运任务 => {},
        _ = 定个5分钟闹钟 => { 通知搬运工别等了 }
    }
    
  2. 修改关店规则(主动关闭Runtime):

    // 到点直接拉闸,最多等1分钟让搬运工收拾
    runtime.shutdown_timeout(60秒);
    
  3. 换用更灵活的快递站(使用async-std等替代方案):

    // 改用能随时喊停的异步IO
    async_std::io::stdin().read_exact(&mut buf).await;
    

关键理解:异步框架中的阻塞操作就像不听话的员工,需要特别的管理策略才能避免"全体等人"的尴尬局面。

langzi.me 2025-02-22 11:33

When you shut down the executor, it will wait indefinitely for all blocking operations to finish. You can use shutdown_timeout to stop waiting for them after a certain timeout. Be aware that this will still not cancel the tasks — they are simply allowed to keep running after the method returns. It is possible for a blocking task to be cancelled if it has not yet started running, but this is not guaranteed. 当您关闭执行程序时,它将无限期地等待所有阻塞作完成。您可以使用 shutdown_timeout 在特定超时后停止等待它们。请注意,这仍然不会取消任务 — 它们只是被允许在方法返回后继续运行。如果阻塞任务尚未开始运行,则可以取消该任务,但不能保证这一点。

langzi.me 2025-02-22 11:31

spawn_blocking, 文档中写的

Be aware that tasks spawned using spawn_blocking cannot be aborted because they are not async. If you call abort on a spawn_blocking task, then this will not have any effect, and the task will continue running normally. The exception is if the task has not started running yet; in that case, calling abort may prevent the task from starting. 请注意,使用 spawn_blocking 生成的任务无法中止,因为它们不是异步的。如果您在 spawn_blocking 上调用 abort task,那么这不会有任何影响,任务将继续正常运行。例外情况是任务尚未开始运行;在这种情况下,调用 Abort 可能会阻止任务启动。

rayw0ng 2025-02-22 07:23

看这里https://docs.rs/tokio/latest/tokio/attr.main.html#using-the-multi-thread-runtime 你的h没执行完,tokio的runtime就block了。如果想强制退出,可以abort h。

1 共 4 条评论, 1 页