在「Rust学习笔记 - mpsc 与消息通信」中,通过 tokio::spawn 创建了一个负责发送消息的异步任务。当时主要学习任务之间的消息通信,这里继续展开任务本身的执行、等待和退出。
前言
1 | let agent_task = tokio::spawn(agent.run()); |
这里通过 tokio::spawn 创建任务,最后又通过 agent_task.await 等待它结束。这两个操作分别做了啥?
tokio::spawn 做了什么
先看创建任务的这行代码:
1 | let agent_task = tokio::spawn(agent.run()); |
可以拆开写:
1 | let future = agent.run(); |
这两种写法是等价的。调用 agent.run() 会得到一个 Future,tokio::spawn 接收这个 Future,为它创建一个异步任务,并交给 Tokio 运行时调度。
注意,通过 tokio::spawn 创建的任务,其执行不依赖后续是否调用 agent_task.await。Tokio spawn 源码
那么,tokio::spawn 返回的 agent_task 是什么,最后的 .await 又在等什么?
JoinHandle 的作用
tokio::spawn 返回的是一个 JoinHandle<T>,其中 T 是任务的返回值类型。可以通过它等待任务结束,并取得执行结果。
前面的 agent.run() 没有显式定义返回类型,因此返回的是 (),对应的句柄类型就是 JoinHandle<()>:
1 | let agent_task: tokio::task::JoinHandle<()> = tokio::spawn(agent.run()); |
再看最后这行代码:
1 | agent_task.await.unwrap(); |
agent_task.await 会等待任务结束,并得到一个 Result<T, JoinError>。
- 任务正常结束时,得到
Ok(value),其中value是任务返回的值,类型为T。 - 任务发生 panic 或被取消时,得到
Err(JoinError)。
结合前面的代码,drop(out_tx) 释放了最后一个发送端。等 channel 中剩余的消息被接收后,recv().await 返回 None,消息接收循环结束,agent.run() 随之返回。此时,agent_task.await 就能得到 Ok(())。
不过,如果直接丢弃 agent_task,任务又会怎样?
丢弃 JoinHandle 之后
先说结论,直接丢弃 agent_task 不会取消已经创建的任务。比如:
1 | let agent_task = tokio::spawn(agent.run()); |
任务仍然由 Tokio 运行时调度,只是之后无法再通过这个 JoinHandle 等待它结束、取得返回值或检查 JoinError。Tokio 将这种行为称为 detach。Tokio JoinHandle 源码
不过,任务要继续执行,还需要运行时继续运行和调度。前面使用 #[tokio::main] 的例子中,如果注释掉最后的 agent_task.await.unwrap(),main 就可能在发送消息、释放发送端后直接返回。随后运行时关闭,尚未完成的异步任务也会被丢弃,因此没法保证 Agent 已经处理完 channel 中的消息。
所以,这里的 agent_task.await 也有让 main 等到 Agent 结束后再返回的作用。
如何主动取消 Task
如果希望主动取消任务,可以调用 JoinHandle 的 abort() 方法:
1 | agent_task.abort(); |
不过,abort() 只是发出取消请求,调用返回时任务不一定已经结束。如果需要确认任务结束,还需要继续等待:
1 | agent_task.abort(); |
这里没有直接使用 .unwrap(),因为任务被取消后,等待结果会是一个表示取消的 JoinError,可以通过 is_cancelled() 判断。不过,调用 abort() 后,如果任务先一步正常结束,仍然可能得到 Ok(())。
还有就是,abort() 不会在任意代码执行到一半时强行打断任务。对于正在执行的任务,运行时需要等它交还控制权后才能处理取消。例如,在某个 .await 处等待并返回 Pending 时,就会交还控制权,但如果任务一直执行同步循环这类操作,取消就无法及时完成。
最后
JoinHandle 和其关联的任务有各自的生命周期。丢弃 JoinHandle 后,任务仍可以由运行时继续调度,但运行时关闭后,尚未完成的异步任务也会被丢弃。