Tokio 提供任务调度、异步 I/O、计时器及同步工具。执行器轮询 Future,驱动等待操作系统事件并唤醒任务;用户代码要通过适合异步环境的操作主动让出执行权。
Tokio 任务是轻量异步任务,可以在多个线程间调度,不应写成“是真并发,所以不是协程”。任务如何保存状态与运行在几个 OS 线程上,是不同维度。
就绪任务队列 包含等待 poll 的任务 执行器 worker poll Future,执行当前可推进工作 I/O 与时间驱动 根据平台接口等待事件 注册等待 事件到达,唤醒任务 任务调度与 I/O 驱动通过唤醒协作 current_thread 与 multi_thread 使用不同调度方式;任务数量不等于线程数量。
两种主要调度方式
current_thread 在调用 block_on 的线程上驱动任务,仍可并发处理大量等待;multi_thread 使用多个 worker,并具有工作窃取等机制。默认 worker 数与可用 CPU 等配置有关,可被覆盖,不是保证一个物理核心永久绑定一个 worker。
I/O 驱动的平台实现和具体队列算法属于版本相关细节,理解概念时不必把每种 socket、文件、定时器都硬套进同一个系统调用。部分文件操作会借助阻塞池,而非直接变成内核 readiness 通知。Runtime
current_thread 的运行时在未被 block_on 驱动时,关联任务不会凭空继续执行。多线程运行时的 block_on 主 Future 也不是简单地等于某一个 worker 上的普通 spawn 任务。
一个完整的有界生产/消费程序
Cargo.toml:
[package]
name = "tokio-demo"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "time"] }
src/main.rs:
use tokio:: sync:: mpsc;
# [ tokio ::main ]
async fn main ( ) -> Result< ( ) , Box< dyn std:: error:: Error> > {
let ( tx, mut rx) = mpsc:: channel( 2 ) ;
let producer = tokio:: spawn( async move {
for value in 1_ u32 .. = 4 {
if tx.send ( value) .await.is_err ( ) {
return ; }
}
} ) ;
let mut total = 0 ;
while let Some ( value) = rx.recv ( ) .await {
total + = value;
}
producer.await? ;
assert_eq! ( total, 10 ) ;
println! ( " {total} " ) ;
Ok ( ( ) )
}
容量 2 限制排队项,send 在队列满时等待;生产者结束并释放最后一个 sender 后,消费者排空缓冲再得到 None。producer.await 显式回收任务结果,避免把 panic 等失败当成“任务自然结束”。
bounded channel 不会限制生产前已经创建的无限任务,设计时要让背压覆盖到真正消耗资源的入口。
队列有界,还要看排队项之外的内存
假设队列容量 100,单条消息最多 1 MiB,仅消息负载就可能接近 100 MiB;此外还有生产者正在构造的值、正在处理的值和任务状态。容量按项计数,不自动限制每项字节数,也不限制上游已创建的所有 Future。
限制位置 实际限制的资源 仍可能增长的部分
mpsc 容量 通道中等待消费的项 等待 send 的任务、每项大小
Semaphore 许可 同时进入某段操作的数量 在许可外排队的任务和参数
有界任务集合 已创建且尚未回收的任务 外部输入缓存与单任务内存
输入字节上限 单请求或批次负载 总并发与处理时长
如果先无限 spawn,再在每个任务内部等待信号量,资源密集区的并发可能受限,但等待任务仍可无限积累。要控制任务数量,应在创建任务之前取得许可,或等已有任务完成再接收/创建下一项工作;结果也要及时回收。Semaphore
spawn 的 Send 与生命周期
tokio::spawn 通常要求 Future 与其输出满足 Send + 'static,因为任务可独立存活并可能跨 worker 移动。即使用 current_thread 调度器,这个 API 的类型要求也不会自动放宽。
需要 !Send 局部任务时,可按 Tokio 的局部任务机制使用 LocalSet 与 spawn_local,而不是 unsafe 把 Rc 包装成 Send。临时 Rc 或同步锁 guard 不跨挂起点存活时,又可能不阻碍整个 Future 的 Send;清楚的词法作用域通常最易表达这一点。
使用局部任务处理 Rc
下面独立程序使用本篇同一 Cargo 依赖。LocalSet 保证其中 spawn_local 的任务在局部执行环境运行,但 spawn_local 仍需要任务拥有的捕获满足 'static;它并不是普通借用作用域线程的替代品。
use std:: { cell:: RefCell, rc:: Rc} ;
use tokio:: task:: LocalSet;
# [ tokio ::main ( flavor = " current_thread" ) ]
async fn main ( ) {
let local = LocalSet:: new( ) ;
local.run_until ( async {
let values = Rc:: new( RefCell:: new( vec! [ 1 ] ) ) ;
let shared = Rc:: clone( & values) ;
let task = tokio:: task:: spawn_local( async move {
tokio:: task:: yield_now( ) .await;
shared.borrow_mut ( ) .push ( 2 ) ;
} ) ;
task.await.unwrap ( ) ;
assert_eq! ( & * values.borrow ( ) , & [ 1 , 2 ] ) ;
} ) .await;
}
Rc 在这个任务中跨 await 存活,所以不适合 tokio::spawn,但适合明确的局部任务模型。RefCell 借用仍应在合理范围结束,LocalSet 不会消除单线程重入冲突。LocalSet
JoinHandle 的 Drop 不等于任务取消
丢弃 Tokio JoinHandle 通常使任务脱离等待,任务仍可继续执行。abort 请求取消任务,在合适的调度边界生效;等待 handle 可以观察取消结束。一个永不让出的长 poll 不会因为另一个任务调用 abort 就立即被抢占。JoinHandle
这与丢弃一个尚未 spawn、由当前任务直接拥有的 Future 不同。资源清理和后台工作退出需要明确的所有权与协作协议,必要时使用关闭信号并等待完成。
超时等待 JoinHandle 后,明确请求终止并回收
use std:: { sync:: { Arc, atomic:: { AtomicBool, Ordering} } , time:: Duration} ;
use tokio:: { sync:: oneshot, time:: timeout} ;
struct MarkDropped ( Arc< AtomicBool> ) ;
impl Drop for MarkDropped {
fn drop ( & mut self ) { self .0. store ( true , Ordering:: SeqCst) ; }
}
# [ tokio ::main ( flavor = " current_thread" ) ]
async fn main ( ) {
let dropped = Arc:: new( AtomicBool:: new( false ) ) ;
let flag = Arc:: clone( & dropped) ;
let ( ready_tx, ready_rx) = oneshot:: channel( ) ;
let mut task = tokio:: spawn( async move {
let resource = MarkDropped( flag) ;
ready_tx.send ( ( ) ) .unwrap ( ) ;
std:: future:: pending:: < ( ) > ( ) .await;
drop ( resource) ;
} ) ;
ready_rx.await.unwrap ( ) ; assert! ( timeout ( Duration:: from_millis( 1 ) , & mut task) .await.is_err ( ) ) ;
assert! ( ! dropped.load ( Ordering:: SeqCst) ) ;
task.abort ( ) ;
assert! ( task.await.unwrap_err ( ) .is_cancelled ( ) ) ;
assert! ( dropped.load ( Ordering:: SeqCst) ) ;
}
timeout 借用 &mut task,超时后仍保留 JoinHandle,因而可以决定继续等待、协作关闭或 abort。若把 handle 按值交给 timeout,超时会丢弃等待对象中的 handle,后台任务却通常继续存在。上例的 Pending 永远不完成,取消结果是确定的;一般任务在 abort 生效前可能已经正常完成,处理代码应允许这种竞态。
1 毫秒这里只用于触发超时分支,不是计时精度承诺。timeout 无法抢占一次始终不返回的 poll;耗时计算仍可能越过截止时间继续运行。它限制等待协议,不是操作系统级强制终止。timeout
丢掉等待者,后台任务会停吗?
相同的 drop 语法,效果取决于被丢弃的对象拥有什么。直接 Future 拥有操作状态,JoinHandle 只是 spawned 任务的等待和控制入口。先试丢弃 handle,再重置并请求 abort;对照图中任务生命的变化,便能区分“停止等待”“请求取消”和“取消已经完成”。
正在呈现知识画面 重新加载
丢掉等待者,后台任务会停吗? 丢弃直接拥有的 Future 会析构它;丢弃 JoinHandle 会分离任务,abort 还要等调度边界生效。
{"id":"rust-task-handle-cancel","title":"丢掉等待者,后台任务会停吗?","summary":"丢弃直接拥有的 Future 会析构它;丢弃 JoinHandle 会分离任务,abort 还要等调度边界生效。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"mode\"><button data-mode=\"direct\" data-i18n=\"direct\"></button><button data-mode=\"spawn\" data-i18n=\"spawn\"></button></div><div class=\"actions\"><button id=\"drop\" data-i18n=\"drop\"></button><button id=\"abort\" data-i18n=\"abort\"></button><button id=\"schedule\" data-i18n=\"schedule\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><svg id=\"map\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\" data-i18n-label=\"heading\"></svg><div class=\"metrics\"><div><span data-i18n=\"owner\"></span><strong id=\"owner\"></strong></div><div><span data-i18n=\"task\"></span><strong id=\"task\"></strong></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"explain\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:550;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:550 20px ui-monospace,monospace}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px ui-monospace,monospace;background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px ui-monospace,monospace}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 ui-monospace,monospace;white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:ui-monospace,monospace}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px ui-monospace,monospace;padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 ui-monospace,monospace;overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.chart-wrap{margin:18px 0}.chart-label{font-size:12px;color:var(--muted);min-height:3.2em}.curve{display:block;width:100%;height:auto;max-height:320px;overflow:visible}.curve text{font:11px ui-monospace,monospace;fill:var(--muted)}.gridline{stroke:var(--rule);stroke-width:1}.metrics strong{font-size:20px;min-height:3.2em}.formula{font:12px/1.8 ui-monospace,monospace;min-height:9em}.readout{min-height:8em}.presets button{flex-basis:30%}.scope p{overflow-wrap:anywhere} .unit-note{font-size:12px;color:var(--muted);margin:10px 0}\n\n.metrics span{word-break:break-all;line-break:strict}.metrics>div{padding-right:6px}\n\n.cell{min-height:72px}.metrics strong{min-height:3.6em}\n\n.metrics strong{font-size:16px;min-height:4em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';row.style.gridTemplateColumns='repeat('+r.length+',minmax(0,1fr))';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function plot(id,series,xr,yr,opts={}){const e=$('#'+id),X=x=>42+(x-xr[0])/(xr[1]-xr[0])*298,Y=y=>175-(y-yr[0])/(yr[1]-yr[0])*148;let out='<defs><clipPath id=\"clip-'+id+'\"><rect x=\"42\" y=\"27\" width=\"298\" height=\"148\"/></clipPath></defs>';for(let i=0;i<3;i++){let x=xr[0]+i*(xr[1]-xr[0])/2,y=yr[0]+i*(yr[1]-yr[0])/2;out+='<path class=\"gridline\" d=\"M '+X(x)+' 27V175M42 '+Y(y)+'H340\"/><text x=\"'+X(x)+'\" y=\"194\" text-anchor=\"middle\">'+esc(opts.xfmt?opts.xfmt(x):Number(x.toFixed(2)))+'</text><text x=\"36\" y=\"'+(Y(y)+4)+'\" text-anchor=\"end\">'+esc(opts.yfmt?opts.yfmt(y):Number(y.toFixed(2)))+'</text>';}out+='<g clip-path=\"url(#clip-'+id+')\">';for(const s of series){out+='<path fill=\"none\" stroke=\"'+(s.color||'var(--accent)')+'\" stroke-width=\"2.5\" '+(s.dash?'stroke-dasharray=\"5 4\"':'')+' d=\"'+s.pts.map((p,i)=>(i?'L':'M')+X(p[0]).toFixed(2)+','+Y(p[1]).toFixed(2)).join(' ')+'\"/>';}for(const p of opts.points||[])out+='<circle cx=\"'+X(p[0])+'\" cy=\"'+Y(p[1])+'\" r=\"4\" fill=\"var(--second)\" stroke=\"var(--paper)\" stroke-width=\"1.5\"/>';if(opts.cursor!==undefined)out+='<path d=\"M'+X(opts.cursor)+' 27V175\" stroke=\"var(--muted)\" stroke-dasharray=\"3 3\"/>';e.innerHTML=out+'</g>';e.dataset.curves=JSON.stringify(series.map(s=>s.pts));}const curve=(fn,lo,hi,n=120)=>Array.from({length:n+1},(_,i)=>{const x=lo+(hi-lo)*i/n;return[x,fn(x)]});function inputs(draw){document.querySelectorAll('input,select').forEach(e=>{e.addEventListener('input',draw);e.addEventListener('change',draw)});draw()}const n=id=>+$('#'+id).value;const show=(id,v,d=3)=>set(id,Number(v.toFixed(d)));let mode='direct',held=true,state='alive';function init(){held=true;state='alive';draw()}function draw(){pressed(mode);graph('map',[{name:'caller',x:50,y:90,cls:held?'on':''},{name:mode==='direct'?'Future':'handle',x:170,y:90,cls:held?'on':''},{name:'task',x:300,y:90,cls:state==='cancelled'?'':'gold'}],held?[[0,1,true],[1,2,mode==='direct']]:[]);set('owner',t(held?'held':'gone'));set('task',t(state));$('#drop').disabled=!held;$('#abort').disabled=mode!=='spawn'||!held||state!=='alive';$('#schedule').disabled=state!=='requested';set('explanation',t('explain'));document.body.dataset.state=state;}$('#drop').onclick=()=>{held=false;if(mode==='direct')state='cancelled';draw()};$('#abort').onclick=()=>{state='requested';draw()};$('#schedule').onclick=()=>{state='cancelled';draw()};$('#reset').onclick=init;document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;init()});draw();","audio":false,"strings":{"abort":"请求 abort","alive":"仍存活","cancelled":"已析构","direct":"直接 Future","drop":"丢弃当前持有者","explain":"切换方式会重建示例。取消请求与确认结束是两件事;真实代码应等待任务结果来回收。","gone":"已丢弃","heading":"丢掉等待者,后台任务会停吗?","held":"持有","intro":"对同一项未完成工作,选择直接拥有 Future 或持有 spawn 的 JoinHandle。","mode":"拥有方式","owner":"等待对象","requested":"取消已请求","reset":"重置","schedule":"经过调度边界","scope":"模拟可让出的普通异步任务,不适用于已开始的 spawn_blocking 闭包。","scopeLabel":"模型范围与约定","spawn":"spawn + JoinHandle","task":"工作本体"}}
select! 的取消边界看操作数所有权
select! 在当前任务中轮询多个分支,并非自动把每个分支 spawn 到不同线程。选出分支后,其他局部 Future 被丢弃;若分支只是借用了外部固定的 Future,它的底层对象仍可能由外部继续持有。
若某分支拥有一个 JoinHandle,丢弃它只会脱离任务,不自动停止该任务。若丢弃的是已经消费部分输入的 read_exact Future,进度又可能丢失。重复 select 循环必须核对各操作的取消安全性。select!
默认分支轮询顺序与 biased 模式有不同公平性考虑,biased 也不能保证业务公平。分支包含阻塞代码时会阻塞整个 select 的推进。
JoinSet 将任务创建与结果回收放在同一个循环
use tokio:: task:: { JoinError, JoinSet} ;
fn accept ( result : Result< Result< u32 , & 'static str > , JoinError> ,
values : & mut Vec< u32 > , rejected : & mut usize ) {
match result {
Ok ( Ok ( value) ) => values.push ( value) ,
Ok ( Err ( " invalid input" ) ) => * rejected + = 1 ,
other => panic! ( " 未预期的任务结果:{other:?}" ) ,
}
}
# [ tokio ::main ( flavor = " current_thread" ) ]
async fn main ( ) {
let mut tasks = JoinSet:: new( ) ;
let mut values = Vec :: new( ) ;
let mut rejected = 0 ;
for value in 0 u32 .. 5 {
if tasks.len ( ) > = 2 {
accept ( tasks.join_next ( ) .await.unwrap ( ) , & mut values, & mut rejected) ;
}
tasks.spawn ( async move {
tokio:: task:: yield_now( ) .await;
if value = = 3 { Err ( " invalid input" ) } else { Ok ( value * 2 ) }
} ) ;
assert! ( tasks.len ( ) < = 2 ) ;
}
while let Some ( result) = tasks.join_next ( ) .await {
accept ( result, & mut values, & mut rejected) ;
}
values.sort_unstable ( ) ;
assert_eq! ( values, [ 0 , 2 , 4 , 8 ] ) ;
assert_eq! ( rejected, 1 ) ;
}
这里最多保留两个尚未回收的任务。JoinSet 按完成顺序交出结果,不保证输入顺序,所以示例在验证时排序;业务若需要原顺序,可以携带序号并重排,同时考虑重排缓冲上限。输出 Vec 在这个有限例子中安全可控,面对无限输入则应改为逐项交付结果。
返回值有两层失败:外层 JoinError 表示任务取消或 panic,内层 Err 是业务主动返回的失败。一次 .await? 只传播外层,不能据此断言业务成功。JoinSet
JoinSet 被丢弃会请求中止其任务,这与丢弃单个 JoinHandle 不同;若需要确认所有析构完成,应使用并等待相应关闭/回收流程。单纯发出中止请求不等于已经完成资源回收。
阻塞工作与 CPU 工作
std::thread::sleep、阻塞库调用和长计算不应直接长时间占住异步 worker。有限阻塞任务可以用 spawn_blocking,但其已开始运行的闭包通常无法被 abort 取消;运行时关闭也要考虑它的结束行为。spawn_blocking
阻塞池有自己的线程与队列策略,不是自动为无限 CPU 工作提供合理并发上限。大量 CPU 计算应明确限流或使用合适的专用执行池,并以目标负载验证。
锁不要只按“async 里就必须异步锁”选择
短小、低争用且不跨 await 的临界区可以使用普通 Mutex。持锁跨 await 容易阻塞其他任务或引入死锁;std MutexGuard 还可能使 Future 不满足 Send。
Tokio 的异步 Mutex 允许异步等待锁,并可在需要时跨 await 持 guard,但这不免除业务死锁和长时间独占问题。共享 I/O 资源有时更适合由一个任务拥有,用消息传递发请求。Tokio 共享状态
诊断延迟时区分:排队等待、调度未获运行、CPU 执行、I/O 等待与锁等待。仅看到请求慢,无法确定是 worker 数太少还是下游能力不足。
停止接收新工作 关闭入口,通知生产者 处理已有工作 排空队列或显式拒绝 回收任务与资源 等待退出,处理剩余失败 给整个关闭流程设置预算与超时后的策略 截止前协作结束;超过预算时明确取消、丢弃或交由外部恢复 关闭服务:停止接收之后,还要处理已经接收的工作 运行时退出不是业务完成证明;后台任务必须有可观察的结束与结果处理路径。
关闭顺序与可观察性
有界消费者若等到所有 sender 都消失才退出,就必须知道每个 sender 克隆由谁持有。留在长寿命结构中的多余克隆可能让 recv 永远等不到 None。通过 Receiver::close 主动停止新发送后,仍应继续接收并处理已缓冲消息;先前取得的许可还需要按通道契约处理,不能将 close 理解为瞬间清空一切。mpsc Receiver::close
服务关闭可以依次停止入口、发出协作信号、处理已接收工作、回收任务,再结束运行时。对每步记录待处理数量、最老任务年龄与剩余预算,才能分辨是生产者没停、某个任务没让出,还是下游仍在等待。
手动构造 Runtime 时,还要启用所需的时间或 I/O 驱动;“创建成功”不意味着任何依赖驱动的 Future 都可运行。运行时句柄的进入上下文与实际持续驱动运行时也不同,尤其在 current_thread 环境中应保持明确的 block_on 所有权。