Rust in Everything
基于 bytes 1.12.1

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]。 每个可提升存储最多成功提升一次,编译器可以把这段挪出热路径。
💡TIP
打个比方:几个人同时去给一辆私家车办共享登记

车(缓冲)原本只有车主一个人用,车管所的档案(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;最后 drop b 释放 2 块:数据缓冲和 Shared。

并发:几个线程同时做第一次 clone

Bytes 是 Sync,几个线程可以通过 &Bytes 同时 clone 同一个值。可提升存储第一次被并发 clone 时,每个线程都会走进 shallow_clone_vec。

❗IMPORTANT
动脑筋:8 个线程同时 clone 同一个刚建好的 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}");
}
先猜「大多数轮」那一行的 k,再看文末的答案。

这段代码要保证的不是「只分配一次」,而是「最后只留下一个」:不管几个线程分配了 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)逐项对照:

步骤Arcbytes 的 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 掉。

📝NOTE
冷知识:release_shared 末尾那次 load 原来是一个 fence

1.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_VTABLEref_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 返回的只是那一刻的快照。

📝NOTE
冷知识:is_unique 出过一次把类型认错的 bug

Bytes::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 字节变成了可写的空闲容量。文档说「If self is unique for the entire original buffer」,这里的「unique」指的是引用计数,不要求这个句柄看得到整块缓冲。
  • Vec 的 len != cap 时 From<Vec<u8>> 直接建 Shared,try_into_mut 拿回完整的 2048 容量。
  • from_static、from_owner 即使只有一个句柄也是 Err。
📝NOTE
没有蠢问题

问: 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 减一、最后用 Acquire load(1.2.0 起代替 fence,为了 TSAN)。
  • try_into_mut 在 is_unique 时原地复用缓冲,不拷贝(走 Shared 时只释放 24 字节的 Shared);有其他句柄,或存储来自 from_static / from_owner 时返回 Err(self)。
  • loom 只模型检查了两个线程并发 clone(可提升存储和冻结的 BytesMut 各一个),断言数据指针不变。

💬 关联讨论

发起讨论
加载中...