一句话总结
Tokio 是 Rust 异步生态的事实标准运行时.#[tokio::main] 把同步 main 接入异步世界;tokio::spawn 把一个 Future 交给运行时作为独立任务并发执行,角色等同于 Go 的 go f().
但因为 Future 惰性,二者的"何时开始跑"略有不同.
引入 Tokio
Tokio 是第三方 crate,先加依赖.features = ["full"] 开启全部功能,入门期最省心:
|
最小可运行程序:
|
第 04 篇说过,#[tokio::main] 在背后创建运行时并 block_on async main.展开来看它等价于:
|
多数时候用宏即可;只有需要精细配置运行时(线程数,名字等)时,才手动构造 Runtime.
tokio::spawn:启动并发任务
只在 main 里顺序 .await 谈不上并发.要让多个任务同时推进,用 tokio::spawn 把 Future 提交给运行时,它会成为一个独立调度的任务(task):
|
tokio::spawn(fut) ≈ Go 的 go f():都把一段工作丢到后台并发执行,立即返回一个句柄继续往下走.
Go 的句柄是隐式的(用 channel/WaitGroup 等结果),Tokio 给出一个 JoinHandle,.await 它就能拿到任务的返回值--更接近"带返回值的 goroutine".
JoinHandle:等待与取返回值
spawn 返回 JoinHandle<T>,它本身也是个 Future..await 它会等到任务完成,得到 Result<T, JoinError>:
|
为什么 await 的结果是 Result
任务可能在执行中 panic,或被取消.JoinHandle 把这种"任务没能正常跑完"封装成 JoinError,因此 handle.await 返回 Result.
这与 Go 不同:Go 里一个 goroutine panic 默认会拖垮整个进程;Tokio 的 spawn 任务 panic 默认只影响该任务,由 JoinError 上报,运行时和其他任务继续存活.
spawn 与直接 await 的区别
初学者常困惑:fut.await 和 tokio::spawn(fut).await 看着都"等一个 Future",有何不同?区别在并发性:
| 写法 | 行为 | 并发? |
|---|---|---|
a().await; b().await; |
先把 a 跑完,再开始 b | 否,顺序执行 |
spawn(a()); spawn(b()); |
a,b 作为独立任务同时推进 | 是,真正并发 |
一个反直觉点:直接 await 不产生并发
两个相邻的 .await 是顺序的--第一个没完成,第二个不会开始.这和 Go 不同:在 Go 里必须显式 go 才并发,直觉上不易错.
在 Rust 里"写了两个 await 以为它们并发"是常见误解.要并发,要么 spawn,要么用第 10 篇会讲的 join!.
两种调度器:多线程 vs 当前线程
呼应第 05 篇的"单线程 or 多线程",Tokio 提供两种运行时风格:
| 调度器 | 构造 | 特点 | 适用 |
|---|---|---|---|
| 多线程(默认) | #[tokio::main] |
线程池 + 工作窃取,任务可跨线程,利用多核 | 服务器,绝大多数场景 |
| 当前线程 | #[tokio::main(flavor = "current_thread")] |
单线程跑所有任务,无跨线程开销 | 测试,CLI,嵌入式,wasm |
多线程调度器埋下的伏笔
默认的多线程调度器会把任务在不同线程间搬运(工作窃取).这就要求被 spawn 的 Future 必须能安全地跨线程移动--也就是满足 Send.
这正是下一篇要专门攻克的"跨 await 约束"的根源.先记住这个因果:多线程调度 → spawn 的任务需 Send.
异步的 sleep:别用错了
最后一个高频新手坑.异步任务里要"暂停一段时间",绝不能用 std::thread::sleep--它会阻塞整个线程(连带卡住该线程上的其他任务).要用 Tokio 的异步 sleep:
|
区别正是本系列的核心主题:tokio::time::sleep 返回一个 Future,.await 时让出执行权;thread::sleep 是同步阻塞调用,直接冻结线程.这个"阻塞 vs 让出"的对立会在第 14 篇系统展开.
快速回顾
- 入口:
#[tokio::main]= 创建运行时 +block_on(async main);需精细配置时手动构造Runtime. - spawn:把 Future 提交为独立任务并发执行,类比 Go 的
go f(),返回JoinHandle. - JoinHandle:
.await取任务返回值,结果是Result(任务可能 panic / 被取消). - 并发来源:相邻
.await是顺序的;真正并发需spawn或join!. - 两种调度器:多线程(默认,利用多核)vs 当前线程(单线程,无跨线程开销).
- 异步 sleep:用
tokio::time::sleep().await,绝不用thread::sleep阻塞运行时.
动手练习
- 最小项目:建一个 Tokio 项目,用
#[tokio::main]打印一行,再手动用Runtime::block_on改写一次. - 并发 spawn:用
tokio::spawn启动两个各 sleep 不同时长的任务,await两个JoinHandle并打印返回值. - 串行对比:把两个任务改成相邻
.await(不 spawn),用计时观察总耗时变化,验证"顺序 vs 并发". - 观察 panic:在一个 spawn 的任务里故意
panic!,观察handle.await得到的Err(JoinError),确认其他任务不受影响. - sleep 对比:把某个任务里的
tokio::time::sleep换成thread::sleep,观察对并发的破坏(为第 14 篇预热).