Rc と Arc の違いは参照カウントがアトミックかどうかの一点
TL;DR
Rc<T>はArc<T>のシングルスレッド版。共有所有権の挙動は同じで、違いは参照カウントがアトミックかどうかだけ。Rcは非アトミックなのでオーバーヘッドが非常に小さいが、Sendを実装していないためスレッド間で送れない。これはコンパイル時に検査される。- どちらも不変共有しか与えない。可変性が要るなら
Rc<RefCell<T>>/Arc<Mutex<T>>。 - どちらも循環すると leak する。断ち切るには
Weak。
このドキュメントの射程
Rc と Arc の違い、および両者に共通する制約(不変性・循環参照)を整理する。どちらを選ぶかの判断は最後にまとめる。
原因
Rc は 'Reference Counted' の略で、ヒープに置かれた T の共有所有権を提供する。Rc に対して clone を呼ぶと同じアロケーションを指す新しいポインタができ、そのアロケーションを指す最後の Rc が破棄されたとき中の値もドロップされる。ここまでは Arc と完全に同じ。
違いは 1 点。Rc は非アトミックな参照カウントを使う。そのためオーバーヘッドは非常に小さいが、スレッド間で送れず Send を実装していない。結果としてコンパイラがコンパイル時に「Rc をスレッド間で送っていないか」を検査する。
Send を実装できない理由は The Book にある。Rc<T> を clone して別スレッドに所有権を移すと、両方のスレッドが同時に参照カウントを更新する可能性があるため。Arc がアトミック命令を使っているのはこの競合を防ぐためである。
解決
use std::rc::Rc;
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a); // ポインタ複製 + カウント++(非アトミック)
assert_eq!(Rc::strong_count(&a), 2);
共通する制約 1: 不変共有しか得られない
Rust の共有参照はデフォルトで変更を許さず、Rc も例外ではない。可変性が必要なら Cell か RefCell を Rc の中に入れる。
| シングルスレッド | マルチスレッド |
|---|---|
Rc<RefCell<T>> | Arc<Mutex<T>> / Arc<RwLock<T>> |
RefCell は借用カウントを非アトミックに追跡するためスレッドセーフではない。したがって Arc と組み合わせるのは std::sync 系になる。
共通する制約 2: 循環すると leak する
Rc どうし(Arc どうし)が循環すると参照カウントが 0 に到達せず、永久に解放されない。断ち切るには Weak を使う。Weak は中の値を生かし続けないが、アロケーション自体は生かし続ける。upgrade() は値がすでにドロップされていれば None を返す。
典型パターンは木構造で、親から子へは強い Rc、子から親へは Weak。
使い分け
| スレッド間 | カウント | |
|---|---|---|
Rc<T> | 不可(!Send) | 非アトミック・安い |
Arc<T> | 可 | アトミック |
マルチスレッドでアトミックな参照カウントが必要なら sync::Arc を使う、というのが公式の指針。
実務上は「まず Rc で書き、スレッドをまたぐ必要が出たらコンパイラが怒るので Arc に替える」で足りる。型を差し替えるだけで済むので、最初から保険で Arc にする必要はない。GUI、パーサ、シングルスレッドのグラフ構造は Rc で十分。
まとめ
Rc と Arc は挙動が同じで、参照カウントがアトミックか否かだけが違う。Rc が !Send なのはコンパイル時の安全網として機能するので、必要になってから Arc に替えればよい。