clone 与提升
主线位置:共享主线的中心。
Bytes::clone经由 vtable 分派;对「可提升」存储,第一次 clone 把data从「缓冲地址 + 标记」换成一个Shared引用计数块,以后的 clone、drop 都按Shared处理。切片的slice/split_to/split_off内部也是先 clone,所以它们同样会触发提升。本页结尾讲反方向:try_into_mut什么时候能把Bytes原地变回BytesMut。
flowchart TB
clone["b.clone()"] --> vt{vtable}
vt -->|STATIC| st[直接复制四个字]
vt -->|SHARED| arc[ref_cnt 加一]
vt -->|PROMOTABLE| kind{data 最低位}
kind -->|"1:还没提升"| promo[shallow_clone_vec]
kind -->|"0:已是 Shared"| arc
promo --> cas{CAS 换 data}
cas -->|成功| win["返回 ref_cnt=2"]
cas -->|失败| lose[释放自己的 Shared]
lose --> arc
Bytes 的四个字段、Vtable 的五个函数在 结构与 Vtable,每种存储从哪里来(From<Vec<u8>> 什么时候建可提升存储、什么时候直接建 Shared,奇偶两张表为什么存在)在 五种存储。这一页只讲可提升存储和 Shared 之间的那一步,以及 Shared 的引用计数。代码都在 bytes.rs 的后半部分。
data 的最低位:提升了没有
可提升存储刚建出来时,data 里放的是缓冲本身的地址:偶数地址存 buf | 1(PROMOTABLE_EVEN_VTABLE),奇数地址原样存(PROMOTABLE_ODD_VTABLE)。两种情况下 data 的最低位都是 1。提升之后,data 指向一个 Shared,Shared 至少按 2 对齐,最低位是 0。于是 KIND_ARC 和 KIND_VEC 就是这一位的两个值:
const KIND_ARC: usize = 0b0;
const KIND_VEC: usize = 0b1;
const KIND_MASK: usize = 0b1;
「Shared 的地址最低位一定是 0」不是靠运气,一个编译期断言守着:
// Assert that the alignment of `Shared` is divisible by 2.
// This is a necessary invariant since we depend on allocating `Shared` a
// shared object to implicitly carry the `KIND_ARC` flag in its pointer.
// This flag is set when the LSB is 0.
const _: [(); 0 - mem::align_of::<Shared>() % 2] = []; // Assert that the alignment of `Shared` is divisible by 2.
对齐是奇数时 0 - 1 在常量求值里溢出,编译失败。
Shared 本身只有三个字:
struct Shared {
// Holds arguments to dealloc upon Drop, but otherwise doesn't use them
buf: *mut u8,
cap: usize,
ref_cnt: AtomicUsize,
}
buf 和 cap 只在最后释放时用(Drop 里 dealloc(self.buf, Layout::from_size_align(self.cap, 1))),读数据走的永远是 Bytes 自己的 ptr / len。在 64 位平台上它是 24 字节、按 8 对齐。
偶数表的 clone 是 promotable_even_clone:
unsafe fn promotable_even_clone(data: &AtomicPtr<()>, ptr: *const u8, len: usize) -> Bytes {
let shared = data.load(Ordering::Acquire);
let kind = shared as usize & KIND_MASK;
if kind == KIND_ARC {
shallow_clone_arc(shared.cast(), ptr, len)
} else {
debug_assert_eq!(kind, KIND_VEC);
let buf = ptr_map(shared.cast(), |addr| addr & !KIND_MASK);
shallow_clone_vec(data, shared, buf, ptr, len)
}
}
奇数表的版本只差一处:buf 直接就是 shared,不用去掉最低位。两张表的 drop、into_vec、into_mut 也是同样的分叉:先看最低位,KIND_ARC 走 Shared 的路径,KIND_VEC 按原始缓冲处理。
这里有一个容易忽略的细节:提升之后,原来那个 Bytes 的 vtable 不变,还是 PROMOTABLE_*,只是 data 变了;它以后的每次 clone、drop 都要先读一次 data 再分叉。新 clone 出来的 Bytes 则直接挂 SHARED_VTABLE,不再检查最低位。下面的实验会看到这一点。
load 用 Acquire,是因为 data 可能刚被另一个线程的 CAS 改成指向一个新的 Shared:要读那个 Shared 的字段,必须先和那次写入建立先后关系。
shallow_clone_vec:第一次 clone
shallow_clone_vec 是整个 bytes 里唯一一处「多个线程可能同时改 data」的地方:
#[cold]
unsafe fn shallow_clone_vec(
atom: &AtomicPtr<()>,
ptr: *const (),
buf: *mut u8,
offset: *const u8,
len: usize,
) -> Bytes {
// ...
let shared = Box::new(MaybeUninit::<Shared>::uninit());
let shared = Shared::init_to_raw(
shared,
Shared {
buf,
cap: offset.offset_from(buf) as usize + len,
// Initialize refcount to 2. One for this reference, and one
// for the new clone that will be returned from
// `shallow_clone`.
ref_cnt: AtomicUsize::new(2),
},
);
// ...
match atom.compare_exchange(ptr as _, shared as _, Ordering::AcqRel, Ordering::Acquire) {
Ok(actual) => {
debug_assert!(core::ptr::eq(actual, ptr));
// The upgrade was successful, the new handle can be
// returned.
Bytes {
ptr: offset,
len,
data: AtomicPtr::new(shared as _),
vtable: &SHARED_VTABLE,
}
}
Err(actual) => {
// The upgrade failed, a concurrent clone happened. Release
// the allocation that was made in this thread, it will not
// be needed.
let shared = Box::from_raw(shared);
mem::forget(*shared);
// Buffer already promoted to shared storage, so increment ref
// count.
shallow_clone_arc(actual as _, offset, len)
}
}
}
逐步看:
- 先分配,再抢。 每个走到这里的线程都先分配一个自己的
Shared,再用 CAS 试图把data从旧值(ptr,也就是带标记的缓冲地址)换成它。分配在 CAS 之前,所以并发时会多分配几个,下面的实验数了到底多几个。 cap是算出来的。 可提升存储不存容量,cap = (offset - buf) + len,靠的是一条不变式:ptr + len永远是原始缓冲的末尾。advance、split_to只把起点往后挪,不破坏它;会缩短末尾的操作(truncate、split_off)必须先提升,详见切片。提升的这一刻把算出来的cap记进Shared,之后就不再依赖这条不变式了。ref_cnt从 2 开始。 一份算原来的Bytes(它的data马上会指向这个Shared),一份算即将返回的 clone。- CAS 的顺序。 成功用
AcqRel:Release让别的线程在读到新data时也能看到Shared的三个字段;失败用Acquire:读到的actual是别人放进去的Shared,要和那次Release配对才能安全地读它。 - 抢输了。
Box::from_raw把自己那块内存收回来,但mem::forget(*shared)把里面的Shared值移出来扔掉而不运行它的Drop。如果直接drop(Box::from_raw(shared)),Shared::drop会dealloc(buf),把大家共用的数据缓冲释放掉。然后对赢家的Shared做一次普通的shallow_clone_arc。 #[cold]。 每个可提升存储最多成功提升一次,编译器可以把这段挪出热路径。
车(缓冲)原本只有车主一个人用,车管所的档案(data)上写着「私人所有」(最低位 1)。有人想共用这辆车时,要先填一张共享登记表(Shared),表上写好车的位置和大小(buf、cap)、已签名人数 2(ref_cnt)。几个人同时来办,每人手里都先填好了一张表;窗口(CAS)只收第一张,档案上改成「见登记表某号」。后到的人发现档案已经改了,就把自己那张空表撕掉(Box::from_raw + mem::forget),去第一张表上加一个签名(fetch_add)。
比方到这里为止:真实的窗口没有排队,抢输的人不会等,而且「撕表」只回收纸,不能连车一起报废,这正是 mem::forget 的作用。
实验:第一次和第二次 clone 各分配几次
计数用的全局分配器(实验 crate 的 src/lib.rs),依赖只有一行:
bytes = "=1.12.1"
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
// Counts every allocation while ON is set; the Shared header is [usize; 3].
pub struct Counting;
pub static ON: AtomicBool = AtomicBool::new(false);
pub static ALLOCS: AtomicUsize = AtomicUsize::new(0);
pub static DEALLOCS: AtomicUsize = AtomicUsize::new(0);
pub static SHARED_ALLOCS: AtomicUsize = AtomicUsize::new(0);
pub static SHARED_DEALLOCS: AtomicUsize = AtomicUsize::new(0);
pub static LIVE: AtomicUsize = AtomicUsize::new(0);
fn is_shared(layout: Layout) -> bool {
layout == Layout::new::<[usize; 3]>()
}
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
LIVE.fetch_add(layout.size(), Relaxed);
if ON.load(Relaxed) {
ALLOCS.fetch_add(1, Relaxed);
if is_shared(layout) {
SHARED_ALLOCS.fetch_add(1, Relaxed);
}
}
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
// ... 同上,计入 DEALLOCS / SHARED_DEALLOCS,LIVE 减去 size
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static GLOBAL: Counting = Counting;
/// Runs `f` with counting on and returns (allocs, deallocs).
pub fn count<R>(f: impl FnOnce() -> R) -> (R, usize, usize) {
let a0 = ALLOCS.load(Relaxed);
let d0 = DEALLOCS.load(Relaxed);
ON.store(true, Relaxed);
let r = f();
ON.store(false, Relaxed);
(r, ALLOCS.load(Relaxed) - a0, DEALLOCS.load(Relaxed) - d0)
}
/// Reads the four words of a `Bytes`. Depends on private layout: observation only.
pub fn words(b: &bytes::Bytes) -> [usize; 4] {
assert_eq!(size_of::<bytes::Bytes>(), size_of::<[usize; 4]>());
unsafe { std::mem::transmute_copy::<bytes::Bytes, [usize; 4]>(b) }
}
/// Index of the `data` word, found on a fresh promotable Bytes, where data == ptr | 1.
pub fn data_index() -> usize {
let b = bytes::Bytes::from(vec![7u8; 1000]);
let w = words(&b);
let p = b.as_ptr() as usize;
assert_eq!(p & 1, 0);
w.iter().position(|&x| x == p | 1).expect("data word")
}
words 把 Bytes 的四个字原样读出来,依赖私有布局,只用于观察。Bytes 不是 #[repr(C)],字段顺序不保证,所以不按位置猜,而是用一个刚建好的可提升 Bytes 找出「等于 as_ptr() | 1」的那个字,认定它是 data;vtable 字是剩下那个既不等于 ptr 也不等于 len 的。Layout::new::<[usize; 3]>() 和 Shared 的布局(三个字、按字对齐)相同,用来单独数 Shared 的分配。
src/bin/clone_count.rs:
use bytes::Bytes;
use exp_bytes_promote::{count, data_index, words};
fn main() {
let di = data_index();
let b = Bytes::from(vec![7u8; 1024]); // len == cap: promotable storage
let p = b.as_ptr() as usize;
println!("buf = {p:#x}, data = {:#x}", words(&b)[di]);
let (c1, a, d) = count(|| b.clone());
println!("clone #1: allocs={a} deallocs={d}, b.data = {:#x}, c1.data = {:#x}", words(&b)[di], words(&c1)[di]);
let (c2, a, d) = count(|| b.clone());
println!("clone #2: allocs={a} deallocs={d}, c2.data = {:#x}", words(&c2)[di]);
let (c3, a, d) = count(|| c1.clone());
println!("clone #3 (of c1): allocs={a} deallocs={d}, c3.data = {:#x}", words(&c3)[di]);
println!("same ptr: {}", c1.as_ptr() == b.as_ptr() && c3.as_ptr() == b.as_ptr());
let wb = words(&b);
let vi = (0..4).find(|&i| i != di && wb[i] != p && wb[i] != b.len()).unwrap();
println!("vtable: b == c1? {}, c1 == c2? {}", wb[vi] == words(&c1)[vi], words(&c1)[vi] == words(&c2)[vi]);
let (_, a, d) = count(|| drop(c1));
println!("drop c1: allocs={a} deallocs={d}");
let (_, a, d) = count(|| drop(c2));
println!("drop c2: allocs={a} deallocs={d}");
let (_, a, d) = count(|| drop(c3));
println!("drop c3: allocs={a} deallocs={d}, b.is_unique() = {}", b.is_unique());
let (_, a, d) = count(|| drop(b));
println!("drop b : allocs={a} deallocs={d}");
}
M2 Max、rustc 1.99.0,cargo run --release:
buf = 0x10521d8d0, data = 0x10521d8d1
clone #1: allocs=1 deallocs=0, b.data = 0x10521d0f0, c1.data = 0x10521d0f0
clone #2: allocs=0 deallocs=0, c2.data = 0x10521d0f0
clone #3 (of c1): allocs=0 deallocs=0, c3.data = 0x10521d0f0
same ptr: true
vtable: b == c1? false, c1 == c2? true
drop c1: allocs=0 deallocs=0
drop c2: allocs=0 deallocs=0
drop c3: allocs=0 deallocs=0, b.is_unique() = true
drop b : allocs=0 deallocs=2
跑了 3 次,地址不同,其余每行相同:
- 刚建好时
data是缓冲地址加 1(…8d0→…8d1),偶数表。 - 第一次 clone 分配 1 次(
Shared),b自己的data也被改成了这个Shared(…0f0,最低位 0)。 - 第二次、第三次 clone 不分配,
data都是同一个Shared;数据指针始终没动。 b和c1的 vtable 字不同(b还是PROMOTABLE_EVEN_VTABLE),c1和c2相同(SHARED_VTABLE)。- 三个 clone drop 时都不释放;只剩
b时is_unique()回到true;最后 dropb释放 2 块:数据缓冲和Shared。
并发:几个线程同时做第一次 clone
Bytes 是 Sync,几个线程可以通过 &Bytes 同时 clone 同一个值。可提升存储第一次被并发 clone 时,每个线程都会走进 shallow_clone_vec。
Bytes,一轮里通常会分配几个 Shared?平台 M2 Max(12 核)、rustc 1.99.0,--release。8 个工作线程一直在自旋等「开始」信号;主线程每轮新建一个 Bytes::from(vec![1u8; 64])(len == cap,可提升),发出信号,等 8 个线程都 clone 完。计数器只数 [usize; 3] 布局(即 Shared)的分配和释放。共跑 100 000 轮,第 0 轮预热不计(工作线程第一次运行时会释放一块同样 24 字节、与 clone 无关的内存)。
use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering::*};
use bytes::Bytes;
use exp_bytes_promote::{data_index, words, ON, SHARED_ALLOCS, SHARED_DEALLOCS};
const THREADS: usize = 8;
const ITERS: usize = 100_000;
static GEN: AtomicUsize = AtomicUsize::new(0);
static DONE: AtomicUsize = AtomicUsize::new(0);
static TARGET: AtomicPtr<Bytes> = AtomicPtr::new(std::ptr::null_mut());
static SLOTS: [AtomicUsize; THREADS] = [const { AtomicUsize::new(0) }; THREADS];
fn main() {
let di = data_index();
for t in 0..THREADS {
std::thread::spawn(move || {
let mut seen = 0;
loop {
while GEN.load(Acquire) == seen {
std::hint::spin_loop();
}
seen += 1;
let b = unsafe { &*TARGET.load(Acquire) };
let c = b.clone();
SLOTS[t].store(words(&c)[di], Relaxed);
drop(c);
DONE.fetch_add(1, AcqRel);
}
});
}
// histogram[k] = iterations where k threads allocated a Shared
let mut histogram = [0usize; THREADS + 1];
let mut survivors_not_one = 0;
let mut mismatched = 0;
let mut leaked = 0;
// round 0 is a warm-up: the workers' first run frees one unrelated 24-byte block
for i in 0..=ITERS {
let mut b = Bytes::from(vec![1u8; 64]); // fresh promotable storage
TARGET.store(&mut b, Release);
let a0 = SHARED_ALLOCS.load(Relaxed);
let d0 = SHARED_DEALLOCS.load(Relaxed);
DONE.store(0, Release);
ON.store(true, Relaxed);
GEN.fetch_add(1, Release);
while DONE.load(Acquire) != THREADS {
std::hint::spin_loop();
}
ON.store(false, Relaxed);
let allocated = SHARED_ALLOCS.load(Relaxed) - a0;
let freed = SHARED_DEALLOCS.load(Relaxed) - d0;
if i == 0 {
drop(b);
continue;
}
histogram[allocated] += 1;
if allocated - freed != 1 {
survivors_not_one += 1;
}
let shared = words(&b)[di];
if SLOTS.iter().any(|s| s.load(Relaxed) != shared) {
mismatched += 1;
}
// dropping b now frees the winner's Shared and the buffer
let before = SHARED_DEALLOCS.load(Relaxed);
ON.store(true, Relaxed);
drop(b);
ON.store(false, Relaxed);
if SHARED_DEALLOCS.load(Relaxed) - before != 1 {
leaked += 1;
}
}
println!("threads={THREADS} iterations={ITERS}");
for (k, n) in histogram.iter().enumerate().skip(1) {
println!(" {k} Shared allocated (and {} freed by losers): {n} iterations", k - 1);
}
println!("iterations where survivors != 1: {survivors_not_one}");
println!("iterations where a clone saw a different Shared: {mismatched}");
println!("iterations where final drop did not free exactly 1 Shared: {leaked}");
}
这段代码要保证的不是「只分配一次」,而是「最后只留下一个」:不管几个线程分配了 Shared,data 只会被 CAS 成功一次,其余的都在失败分支里释放掉。所有 clone 最后都指向同一个 Shared,引用计数也只记在它身上。
bytes 自己对这段代码有两类并发测试。一类是普通多线程测试:tests/test_bytes.rs 的 stress,8 个线程用 Barrier 同时 clone 一个 Bytes::copy_from_slice 出来的可提升 Bytes,跑 1000 轮(miri 下 100 轮),只检查数据没坏;CI 还在 AddressSanitizer 和 ThreadSanitizer 下跑 test_bytes(ci/tsan.sh),在 miri 下跑全部测试(ci/miri.sh)。另一类是 loom 模型检查,见本页最后一节。
Shared 的引用计数:照抄 Arc
提升之后的 clone 和 drop 就是一个手写的 Arc。shallow_clone_arc:
unsafe fn shallow_clone_arc(shared: *mut Shared, ptr: *const u8, len: usize) -> Bytes {
let old_size = (*shared).ref_cnt.fetch_add(1, Ordering::Relaxed);
if old_size > usize::MAX >> 1 {
crate::abort();
}
Bytes {
ptr,
len,
data: AtomicPtr::new(shared as _),
vtable: &SHARED_VTABLE,
}
}
release_shared(注释里引用 Boost 文档的那一大段省略了):
unsafe fn release_shared(ptr: *mut Shared) {
// `Shared` storage... follow the drop steps from Arc.
if (*ptr).ref_cnt.fetch_sub(1, Ordering::Release) != 1 {
return;
}
// ...
// Thread sanitizer does not support atomic fences. Use an atomic load
// instead.
(*ptr).ref_cnt.load(Ordering::Acquire);
// Drop the data
drop(Box::from_raw(ptr));
}
shared_drop 只是转调它;可提升存储在 KIND_ARC 状态下的 drop 也调它。drop(Box::from_raw(ptr)) 运行 Shared::drop 释放数据缓冲,再释放 Shared 自己,所以最后一个 drop 是两次 dealloc,和上面实验的 deallocs=2 对得上。
和标准库 Arc(rustc 1.99.0 自带源码 alloc/src/sync.rs)逐项对照:
| 步骤 | Arc | bytes 的 Shared |
|---|---|---|
| clone 加一 | strong.fetch_add(1, Relaxed) | ref_cnt.fetch_add(1, Ordering::Relaxed) |
| 溢出保护 | old_size > MAX_REFCOUNT(isize::MAX)时 abort() | old_size > usize::MAX >> 1(同一个数)时 crate::abort() |
| drop 减一 | fetch_sub(1, Release) != 1 就返回 | 相同 |
| 释放前的同步 | acquire!:平时是 fence(Acquire),只在 cfg(sanitize = "thread") 下换成 load(Acquire) | 总是 ref_cnt.load(Ordering::Acquire) |
| 弱引用 | 有 weak 计数 | 没有 |
为什么加一可以用 Relaxed:能调用 clone,说明调用方手里已经有一个活着的引用,计数至少是 1,Shared 不会在这期间被释放;加一本身不需要和任何别的内存操作排序。减一要 Release:本线程通过这个 Bytes 做过的所有读,都必须排在「计数减掉」之前。最后一个减到 0 的线程再做一次 Acquire,和所有之前的 Release 配对,保证释放发生在所有人读完之后。
crate::abort() 在有 std 时是 std::process::abort();no_std 下是在 panic! 期间再 panic(一个 Drop 里 panic!() 的守卫),借双重 panic 终止进程。计数超过 isize::MAX 只可能是有人 mem::forget 了海量 clone,此时继续跑下去会溢出回 0、提前释放。
SHARED_VTABLE 的 shared_clone 读 data 用的是 Relaxed,可提升表用的是 Acquire。区别在于:挂 SHARED_VTABLE 的 Bytes,data 在构造之后再也不会变,它指向的 Shared 早在这个 Bytes 被交到当前线程手里之前就初始化好了;可提升存储的 data 却可能被别的线程刚刚 CAS 掉。
release_shared 末尾那次 load 原来是一个 fence1.2.0 之前,release_shared 在最后一次减一之后写的是 atomic::fence(Ordering::Acquire),和 Arc 一样。PR #541(「Add TSAN support」)把它改成了现在的 (*ptr).ref_cnt.load(Ordering::Acquire),并加上注释:「Thread sanitizer does not support atomic fences. Use an atomic load instead.」起因是 issue #540(「Thread Sanitizer - Data race reserve_inner」),用户在 ThreadSanitizer 下跑 tokio-postgres 时报出了 bytes 里的数据竞争。PR 说明里写「Was previously attempted in #405, but that PR was closed due to TSAN detection being unstable.」bytes_mut.rs 里对应的释放函数在同一个 PR 里做了同样的替换。
两种写法的区别:fence 和之前所有的 Release 减一配对;对同一个原子变量做 Acquire 读,读到的是最后那次减一写入的值,按 release sequence 的规则同样和之前所有的 Release 配对,效果一样,而 TSAN 只认后者。标准库 Arc 的做法是只在 cfg(sanitize = "thread") 时换成 load,bytes 则无条件用 load。这个改动最早出现在 1.2.0。
is_unique 与 try_into_mut
is_unique:每种存储各答各的
Bytes::is_unique 就是 (self.vtable.is_unique)(&self.data)。可提升存储的版本 promotable_is_unique:
unsafe fn promotable_is_unique(data: &AtomicPtr<()>) -> bool {
let shared = data.load(Ordering::Acquire);
let kind = shared as usize & KIND_MASK;
if kind == KIND_ARC {
let ref_cnt = (*shared.cast::<Shared>()).ref_cnt.load(Ordering::Relaxed);
ref_cnt == 1
} else {
true
}
}
还没提升(KIND_VEC)直接返回 true:任何一次 clone 都会触发提升,没提升就说明从来没有第二个句柄。shared_is_unique 是 KIND_ARC 分支的那两行。
| 存储 | is_unique |
|---|---|
STATIC_VTABLE(from_static、空切片) | 恒为 false |
Owned<T>(from_owner) | 恒为 false |
PROMOTABLE_*,未提升 | true |
PROMOTABLE_*,已提升;SHARED_VTABLE | ref_cnt == 1 |
BytesMut 冻结来的共享存储 | 看 bytes_mut::Shared 的 ref_count(split 与 freeze) |
文档里有一句警告:「The result of this method may be invalidated immediately if another thread clones this value while this is being called.」读 ref_cnt 用 Relaxed,is_unique 返回的只是那一刻的快照。
is_unique 出过一次把类型认错的 bugBytes::is_unique 是 1.6.0 加的(PR #643)。当时 BytesMut 冻结来的 Bytes 用的 vtable 里,is_unique 一项直接填了 bytes 模块的 shared_is_unique。PR #718 指出:「that function dereferences the data argument as crate::bytes::Shared, but the actual underlying type is crate::bytes_mut::Shared.」两个 Shared 布局不同,引用计数不在同一个偏移上。修复是给 bytes_mut 加一个自己的 shared_v_is_unique。CHANGELOG 把它单独发成 1.6.1:「This release fixes a bug where Bytes::is_unique returns incorrect values when the Bytes originates from a shared BytesMut. (#718)」
这就是「data 的含义由 vtable 决定」的另一面:同样是一个 AtomicPtr<()>,只有配套的那组函数知道该把它当成什么类型来读。bytes.rs 里的 shared_is_unique 至今仍是 pub(crate),但 1.12.1 里只有 SHARED_VTABLE 自己在用它。
try_into_mut:唯一就原地复用
try_into_mut 只有几行:
pub fn try_into_mut(self) -> Result<BytesMut, Bytes> {
if self.is_unique() {
Ok(self.into())
} else {
Err(self)
}
}
self.into() 走 From<Bytes> for BytesMut,也就是 vtable 的 into_mut。is_unique 为真时,两条路径都不拷贝:
- 可提升存储、未提升:
promotable_to_mut用off = ptr - buf、cap = off + len重建Vec::from_raw_parts(buf, cap, cap),再advance_unchecked(off)把起点挪回ptr。不分配也不释放。 - 已提升或
SHARED_VTABLE:shared_to_mut_impl再用Acquire读一次ref_cnt,确认是 1 后从Shared里取出buf和cap,用ManuallyDrop防止Shared::drop释放缓冲,只释放Shared这个 24 字节的块,然后同样重建Vec并advance_unchecked(off):
if (*shared).ref_cnt.load(Ordering::Acquire) == 1 {
// Deallocate the `Shared` instance without running its destructor.
let shared = *Box::from_raw(shared);
let shared = ManuallyDrop::new(shared);
let buf = shared.buf;
let cap = shared.cap;
// Rebuild Vec
let off = ptr.offset_from(buf) as usize;
let v = Vec::from_raw_parts(buf, len + off, cap);
let mut b = BytesMut::from_vec(v);
b.advance_unchecked(off);
b
} else {
// Copy the data from Shared in a new Vec, then release it
let v = slice::from_raw_parts(ptr, len).to_vec();
release_shared(shared);
BytesMut::from_vec(v)
}
else 分支(拷贝)是直接调用 BytesMut::from(bytes) 时用的;经由 try_into_mut 进来时,不唯一的情况已经在前面返回 Err 了。这里的 Acquire 和其他句柄 drop 时的 Release 配对:别的线程曾经通过它们的 Bytes 读过这块内存,那些读必须排在本线程开始写之前。
Err 的情况只有两类:还有别的句柄指向同一个 Shared;或者存储根本不支持(from_static、from_owner,is_unique 恒为 false)。返回的 Err(self) 就是原来那个 Bytes,没有被消耗,也没有分配。
实验:各种状态下的 try_into_mut
src/bin/into_mut.rs,计数分配器同上:
use bytes::{Buf, Bytes};
use exp_bytes_promote::count;
fn report(name: &str, b: Bytes) {
let ptr = b.as_ptr();
let unique = b.is_unique();
let (r, a, d) = count(|| b.try_into_mut());
match r {
Ok(m) => println!(
"{name:<28} is_unique={unique:<5} Ok same_ptr={:<5} len={} cap={} allocs={a} deallocs={d}",
m.as_ptr() == ptr, m.len(), m.capacity()
),
Err(b) => println!("{name:<28} is_unique={unique:<5} Err same_ptr={:<5} allocs={a} deallocs={d}", b.as_ptr() == ptr),
}
}
fn main() {
report("promotable, never cloned", Bytes::from(vec![1u8; 1024]));
let mut b = Bytes::from(vec![1u8; 1024]);
b.advance(1000);
report("promotable, advance(1000)", b);
let b = Bytes::from(vec![1u8; 1024]);
drop(b.clone());
report("promoted, clone dropped", b);
let b = Bytes::from(vec![1u8; 1024]);
let keep = b.clone();
report("promoted, clone alive", b);
drop(keep);
let b = Bytes::from(vec![1u8; 1024]);
let s = b.slice(10..20);
drop(b);
report("slice(10..20), only handle", s);
let mut v = Vec::with_capacity(2048);
v.resize(1024, 1u8);
report("Vec len != cap (Shared)", Bytes::from(v));
report("from_static", Bytes::from_static(&[1u8; 1024]));
report("from_owner(Vec)", Bytes::from_owner(vec![1u8; 1024]));
}
promotable, never cloned is_unique=true Ok same_ptr=true len=1024 cap=1024 allocs=0 deallocs=0
promotable, advance(1000) is_unique=true Ok same_ptr=true len=24 cap=24 allocs=0 deallocs=0
promoted, clone dropped is_unique=true Ok same_ptr=true len=1024 cap=1024 allocs=0 deallocs=1
promoted, clone alive is_unique=false Err same_ptr=true allocs=0 deallocs=0
slice(10..20), only handle is_unique=true Ok same_ptr=true len=10 cap=1014 allocs=0 deallocs=1
Vec len != cap (Shared) is_unique=true Ok same_ptr=true len=1024 cap=2048 allocs=0 deallocs=1
from_static is_unique=false Err same_ptr=true allocs=0 deallocs=0
from_owner(Vec) is_unique=false Err same_ptr=true allocs=0 deallocs=0
跑了 3 次,输出相同。几点:
- 所有
Ok都是same_ptr=true、allocs=0:没有拷贝。走Shared路径的多一次释放,是那个 24 字节的Shared。 advance(1000)之后,BytesMut的capacity()是 24:前面 1000 字节没有丢,记在BytesMut的偏移里(编码方式见 data 字段的编码),只是不算进capacity()。slice(10..20)在原句柄 drop 之后是唯一句柄,try_into_mut成功,len=10、cap=1014:切片之后、原缓冲末尾之前的 1004 字节变成了可写的空闲容量。文档说「Ifselfis unique for the entire original buffer」,这里的「unique」指的是引用计数,不要求这个句柄看得到整块缓冲。Vec的len != cap时From<Vec<u8>>直接建Shared,try_into_mut拿回完整的 2048 容量。from_static、from_owner即使只有一个句柄也是Err。
问: is_unique 的文档说结果可能「立刻失效」。try_into_mut 先判断 is_unique 再转换,中间会不会被别的线程 clone 一份,导致两个句柄共用一个被当成可写的缓冲?
答: 不会。try_into_mut 按值拿走 self,别的线程手里不可能有指向这个 Bytes 本身的 &Bytes,能 clone 的只有其他句柄。如果读到 ref_cnt == 1,说明其他句柄一个都没有了,也就没有人能再加一;如果读到大于 1,就返回 Err,最坏情况是别的句柄恰好在这之后 drop,得到一次本可以成功的 Err。文档的警告针对的是只拿着 &Bytes 调 is_unique 的代码:那时别的线程可以通过同一个 Bytes 的共享引用随时 clone。
loom 检查了什么
src/loom.rs 只有一件事:在 cfg(all(test, loom)) 下把 AtomicPtr、AtomicUsize、Ordering 换成 loom 的版本,否则用 core::sync::atomic(开了 extra-platforms feature 时用 extra_platforms crate)。Bytes::new 和 Bytes::from_static 在 cfg(all(loom, test)) 下各有一份非 const 的版本,其余代码不变。CI 的 loom 任务运行 RUSTFLAGS="--cfg loom -Dwarnings" cargo test --lib。
模型检查的测试只有两个,都叫 fuzz 模块:
bytes.rs的bytes_cloning_vec:Bytes::from(b"abcdefgh".to_vec())(to_vec的容量等于长度,所以是可提升存储),放进 loom 的Arc,两个线程各 clone 一次,断言 clone 的as_ptr()等于原地址。loom 会穷举两个线程在promotable_*_clone的load、shallow_clone_vec的 CAS、以及之后release_shared里各次原子操作之间的交错,也就是本页的提升竞争。bytes_mut.rs的bytes_mut_cloning_frozen:BytesMut::from(...).split().freeze()得到的Bytes,同样两个线程各 clone 一次。这是BytesMut那套共享存储的引用计数。
两个测试的断言都只是「数据指针没变」。is_unique、try_into_mut、into_vec 这些路径没有 loom 测试,它们的并发正确性靠的是上面的推理和 miri、TSAN 下的普通测试。
动脑筋答案
threads=8 iterations=100000
1 Shared allocated (and 0 freed by losers): 1 iterations
2 Shared allocated (and 1 freed by losers): 8 iterations
3 Shared allocated (and 2 freed by losers): 17 iterations
4 Shared allocated (and 3 freed by losers): 7 iterations
5 Shared allocated (and 4 freed by losers): 70 iterations
6 Shared allocated (and 5 freed by losers): 332 iterations
7 Shared allocated (and 6 freed by losers): 2272 iterations
8 Shared allocated (and 7 freed by losers): 97293 iterations
iterations where survivors != 1: 0
iterations where a clone saw a different Shared: 0
iterations where final drop did not free exactly 1 Shared: 0
答案是 8:大约 97% 的轮次里,8 个线程每个都分配了一个 Shared。另外两次运行分别是 98047 和 97542 轮落在 8,分布的尾部每次不同,但最后三行三次都是 0。
- 为什么几乎总是 8:每个线程先
load到带标记的data,再去分配Shared,最后才 CAS。一次malloc比线程之间的启动时差长,所以第一个线程 CAS 成功时,其余线程多半已经读过旧的data、正在分配或已经分配完了。只有启动得足够晚、load时已经看到KIND_ARC的线程,才会直接走shallow_clone_arc、不分配。 - 「freed by losers」一列是按 k−1 打印的;它和实测对得上,由下一行证明:每一轮实测的分配数减释放数都是 1(
survivors != 1: 0)。 a clone saw a different Shared: 0:8 个 clone 的data全都等于原Bytes最后的data,赢家只有一个。final drop did not free exactly 1 Shared: 0:每轮 drop 原Bytes时恰好释放一个Shared,没有泄漏。
所以「抢输的一方释放自己的 Shared」不是少见的边角情况:只要可提升存储的第一次 clone 发生在多个线程上,多分配、再释放几次是常态,代价是几次 malloc / free,换来的是单线程场景下 Vec 转 Bytes 完全不分配引用计数。想避免它,可以在交给多个线程之前先在一个线程里 clone 一次(或直接 slice(..))。
小结
- 可提升存储的
data最低位是 1 表示还没提升(缓冲地址本身),0 表示已指向Shared;Shared的偶数对齐由编译期断言保证。 - 第一次 clone 进
shallow_clone_vec:分配ref_cnt = 2的Shared,cap由ptr - buf + len算出,CAS(成功AcqRel、失败Acquire)换进data。抢输的线程Box::from_raw+mem::forget只释放自己的Shared块,再给赢家加一。 - 提升后原句柄的 vtable 仍是
PROMOTABLE_*,每次 clone/drop 先查最低位;新 clone 挂SHARED_VTABLE。实测第一次 clone 分配 1 次,之后 0 次,最后一个 drop 释放 2 块。 - 8 线程同时第一次 clone,约 97% 的轮次 8 个线程都分配了
Shared,但每轮只留下一个。 Shared的计数照抄Arc:Relaxed加一、超过isize::MAX就 abort、Release减一、最后用Acquireload(1.2.0 起代替 fence,为了 TSAN)。try_into_mut在is_unique时原地复用缓冲,不拷贝(走Shared时只释放 24 字节的Shared);有其他句柄,或存储来自from_static/from_owner时返回Err(self)。- loom 只模型检查了两个线程并发 clone(可提升存储和冻结的
BytesMut各一个),断言数据指针不变。