Rust in Everything
基于 bytes 1.12.1

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_uniquebytes_mut::Shared 的计数大于 1,空的 BytesMut 也算
from_staticstatic_is_unique恒为 false:'static 数据不能拿来写
from_ownerowned_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,再把计数减一。

📝NOTE
冷知识:拷贝的那条退路,起初叫 Bytes::make_mut

issue #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。

💬 关联讨论

发起讨论
加载中...