try_into_mut 返回 Err
主线位置:从共享主线(
Bytes)回到构建主线(BytesMut)的那道门。门开不开只看一件事:整块缓冲是不是只剩这一个句柄。
flowchart TB
t["try_into_mut"] --> u{"is_unique"}
u -->|true| ok["Ok,原地变 BytesMut"]
u -->|false| e["Err,原样还回"]
e --> h["还有别的句柄"]
e --> s["from_static 或 from_owner"]
h --> f1["先 drop 别的句柄"]
s --> f2["只能拷贝"]
try_into_mut 怎么经由 vtable 的 into_mut 不拷贝地重建 Vec、各种存储的 is_unique 怎么回答,在 clone 与提升 的「is_unique 与 try_into_mut」里讲过,也跑过各种状态。这一页看几种「明明只有我在用,却拿到 Err」的情况,以及拿到 Err 之后怎么办。
本页实验在 M2 Max、rustc 1.99.0、--release 下运行,依赖:
bytes = "=1.12.1"
复现
每种情况先 try_into_mut;失败了,再用 BytesMut::from 走退路,看它拷不拷贝。common.rs 是一个包住 System 的计数分配器,count 返回闭包执行期间的 alloc、realloc、free 次数和新申请的字节数。
use bytes::{BufMut, Bytes, BytesMut};
#[path = "../common.rs"]
mod common;
use common::*;
fn report(name: &str, b: Bytes) {
let ptr = b.as_ptr();
let (r, d) = count(|| b.try_into_mut());
match r {
Ok(m) => println!("{name:<30} Ok same_ptr={:<5} {d}", m.as_ptr() == ptr),
Err(b) => {
// 退路:From<Bytes> for BytesMut,唯一时复用,否则拷贝
let (m, d2) = count(|| BytesMut::from(b));
println!("{name:<30} Err {d} | From: same_ptr={:<5} {d2}", m.as_ptr() == ptr);
}
}
}
fn main() {
// 1. 还有一个 clone 活着
let b = Bytes::from(vec![1u8; 1024]);
let keep = b.clone();
report("clone alive", b);
drop(keep);
// 2. 自己是从别的 Bytes 切出来的,原来那个还在
let whole = Bytes::from(vec![1u8; 1024]);
let part = whole.slice(0..16);
report("slice, parent alive", part);
drop(whole);
// 3. 从 BytesMut 切出来再 freeze,剩下的那个 BytesMut 还在(哪怕它是空的)
let mut buf = BytesMut::with_capacity(1024);
buf.put_bytes(1, 16);
let frozen = buf.split().freeze();
println!("{:<30} buf.len()={} buf.capacity()={}", " (remaining BytesMut)", buf.len(), buf.capacity());
report("split().freeze(), buf alive", frozen);
// 4. 同上,但先把剩下的 BytesMut drop 掉
let mut buf = BytesMut::with_capacity(1024);
buf.put_bytes(1, 16);
let frozen = buf.split().freeze();
drop(buf);
report("split().freeze(), buf dropped", frozen);
// 5. 存储本身不支持:只有一个句柄也 Err
report("from_static", Bytes::from_static(&[1u8; 1024]));
report("from_owner(Vec)", Bytes::from_owner(vec![1u8; 1024]));
}
跑 3 次,输出相同:
clone alive Err alloc+0 realloc+0 (0B) free+0 | From: same_ptr=false alloc+1 realloc+0 (1024B) free+0
slice, parent alive Err alloc+0 realloc+0 (0B) free+0 | From: same_ptr=false alloc+1 realloc+0 (16B) free+0
(remaining BytesMut) buf.len()=0 buf.capacity()=1008
split().freeze(), buf alive Err alloc+0 realloc+0 (0B) free+0 | From: same_ptr=false alloc+1 realloc+0 (16B) free+0
split().freeze(), buf dropped Ok same_ptr=true alloc+0 realloc+0 (0B) free+0
from_static Err alloc+0 realloc+0 (0B) free+0 | From: same_ptr=false alloc+1 realloc+0 (1024B) free+0
from_owner(Vec) Err alloc+0 realloc+0 (0B) free+0 | From: same_ptr=false alloc+1 realloc+0 (1024B) free+2
- 所有
Err都是 0 分配:try_into_mut只是判断,失败时把原来的Bytes交回来。 - 第 2 行和第 3 行最容易想不到。
part只是一个 16 字节的切片,frozen是自己split出来的独立一段,看起来都「只有我」;但「唯一」指的是整块缓冲的引用计数,父Bytes、或者split之后剩下的那个空BytesMut(长度 0、容量 1008)都还算一个句柄。 - 第 4 行:剩下的
BytesMut一 drop,同样的frozen就Ok,不分配。 - 最后两行只有一个句柄,照样
Err。 BytesMut::from的退路只拷贝这个句柄看得到的字节:切片是 16 字节,不是 1024。from_owner那一行的free+2是拷完之后释放拥有者:里面的Vec和包着它的那块堆内存。
为什么
try_into_mut 本身只有一个判断:
pub fn try_into_mut(self) -> Result<BytesMut, Bytes> {
if self.is_unique() {
Ok(self.into())
} else {
Err(self)
}
}
is_unique 交给 vtable,每种存储各答各的(完整的表在 clone 与提升):
| 情况 | 谁来回答 | 为什么是 false |
|---|---|---|
| clone、父切片还活着 | promotable_is_unique / shared_is_unique | 引用计数大于 1 |
split 剩下的 BytesMut 还活着 | shared_v_is_unique | bytes_mut::Shared 的计数大于 1,空的 BytesMut 也算 |
from_static | static_is_unique | 恒为 false:'static 数据不能拿来写 |
from_owner | owned_is_unique | 恒为 false:拥有者是任意 AsRef<[u8]>,没法变成 Vec |
文档把最后两种写明了:「This will also always fail if the buffer was constructed via either from_owner or from_static.」
退路 From<Bytes> for BytesMut 走的是同一个 vtable 函数 into_mut,区别是不先问 is_unique:唯一时和 try_into_mut 一样原地复用,否则拷贝。from_static 用 static_to_mut,就是 BytesMut::from(slice);共享存储不唯一时 to_vec 这个句柄的 ptr..ptr+len,再把计数减一。
Bytes::make_mutissue #611 要的是一个「make_mut」:能原地转就原地转。提问的人自己就倾向于失败时不要偷偷拷贝:「I see this library has an emphasis on avoiding implicit slow operations (good!) so the Option approach might be better.」PR #695(「Add zero-copy make_mut」)两样都加了:try_into_mut 失败时返回 Err,make_mut 失败时拷贝。
还没发布,issue #709 就提议把 make_mut 换成 impl From<Bytes> for BytesMut。PR #710 照做,理由之一是名字要留给别的语义:「<Arc<T>>::make_mut returns a &mut T, such an API is doable for Bytes too and thus we should reserve Bytes::make_mut for that.」另一条是能当 trait bound 用:「it would be helpful to use From<Bytes> as a trait bound in some cases with other traits such as Hyper's body trait, where Hyper gives you Bytes values.」两个 PR 都在 1.7.0 里,CHANGELOG 合成一行:「Add conversion from Bytes to BytesMut (#695, #710)」。所以 1.12.1 里「不拷贝,失败就告诉我」是 try_into_mut,「能不拷就不拷,否则拷」是 BytesMut::from。
从 hyper 拿到的 body 呢
处理函数里拿到的 body 常常是 hyper 读缓冲 split_to 再 freeze 出来的 Bytes(一块缓冲的一生),按上面第 3 行的道理,读缓冲还引用着那块内存时就会 Err。实际跑一下:一条 HTTP/1 连接上依次发 10 B、1000 B、100 000 B 的 POST,处理函数 collect().to_bytes() 之后调 try_into_mut。
bytes = "=1.12.1"
http-body-util = "=0.1.5"
hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
tokio = { version = "=1.54.1", features = ["full"] }
use std::convert::Infallible;
use bytes::Bytes;
use http_body_util::{BodyExt, Full};
use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
// 处理函数:把 body 收集成 Bytes,看能不能原地转成 BytesMut
async fn handle(req: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
let body = req.into_body().collect().await.unwrap().to_bytes();
let len = body.len();
let unique = body.is_unique();
let r = match body.try_into_mut() {
Ok(m) => format!("Ok cap={}", m.capacity()),
Err(_) => "Err".to_string(),
};
println!("body len={len:<6} is_unique={unique:<5} try_into_mut={r}");
Ok(Response::new(Full::new(Bytes::from_static(b"ok"))))
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (tcp, _) = listener.accept().await.unwrap();
http1::Builder::new().serve_connection(TokioIo::new(tcp), service_fn(handle)).await.unwrap();
});
let mut c = TcpStream::connect(addr).await.unwrap();
for n in [10usize, 1000, 100_000] {
let head = format!("POST / HTTP/1.1\r\nhost: a\r\ncontent-length: {n}\r\n\r\n");
c.write_all(head.as_bytes()).await.unwrap();
c.write_all(&vec![b'x'; n]).await.unwrap();
// 读完响应("ok",响应很短,一次 read 足够)
let mut resp = [0u8; 1024];
let _ = c.read(&mut resp).await.unwrap();
}
}
跑 5 次,输出相同:
body len=10 is_unique=true try_into_mut=Ok cap=8144
body len=1000 is_unique=true try_into_mut=Ok cap=8192
body len=100000 is_unique=true try_into_mut=Ok cap=100000
在这个实验里三次都是 Ok:处理函数拿到 body 时,读缓冲已经不再引用那块内存。cap 说明拿到的是那块缓冲从 body 开头到末尾的全部容量(第一行的 8144 是 8192 减去前面的请求头,属于 clone 与提升 里讲过的「切片之后的空闲容量变成可写容量」)。但这依赖 hyper 读缓冲当时的状态,hyper 没有对此做任何保证;to_bytes() 在 body 分多帧时还会拷贝成一块新的(http-body-util)。代码要按「可能 Err」来写。
修法
- 先放手,再转换。 把 clone、父切片、
split剩下的BytesMut先 drop,再try_into_mut。buf.split().freeze()之后如果buf不再用,就别让它活着;整块都要的话,直接buf.freeze()而不是buf.split().freeze():同样写 16 字节、容量 1024 的缓冲,freeze()之后is_unique()是true,try_into_mut成功(程序和输出见下)。 - 要可写就别用
from_static/from_owner。 它们永远Err。数据以后要改,一开始就放进Vec或BytesMut。 - 拷贝可以接受时用
BytesMut::from(bytes)。 能复用就复用,不能就只拷这个句柄看得到的字节。 - 拷贝不能接受时,处理
Err。Err(b)就是原来的Bytes,可以继续只读使用,或者记一条日志说明这里没能复用。
第一条里 freeze() 和 split().freeze() 的对比:
use bytes::{BufMut, BytesMut};
fn main() {
let mut buf = BytesMut::with_capacity(1024);
buf.put_bytes(1, 16);
let b = buf.freeze();
println!("freeze() is_unique={} try_into_mut ok={}", b.is_unique(), b.try_into_mut().is_ok());
}
freeze() is_unique=true try_into_mut ok=true
freeze 把整个 BytesMut 交出去,没有留下另一半(KIND_VEC 时走 From<Vec<u8>>,见 split 与 freeze),计数是 1。
怎么确认自己没踩
- 在调用前打印
is_unique():try_into_mut内部问的就是它。它是那一刻的快照(clone 与提升 的「没有蠢问题」),别的线程恰好在两次调用之间 drop 了一个句柄时,两者会不一致;单线程里结果相同。 - 用计数分配器量
BytesMut::from那一步:alloc+0是复用,alloc+1是拷贝。 is_unique()为false而你确信只有一个句柄时,查三处:有没有父Bytes或兄弟切片;是不是split/split_to出来的、另一半还活着;是不是from_static/from_owner。
小结
try_into_mut只在is_unique()为真时成功,失败时不分配,把原Bytes还回来。- 「唯一」是整块缓冲的引用计数:父
Bytes、兄弟切片、split之后剩下的空BytesMut都算句柄。 from_static和from_owner的Bytes只有一个句柄也永远Err。BytesMut::from(bytes)是会拷贝的退路,只拷这个句柄看得到的字节;1.7.0 里它起初叫make_mut。- 从 hyper 拿到的 body 在实验里能原地转换,但这不是保证,代码要处理
Err。