Rust のスマートポインタまとめ
Rust 勉強シリーズ。
いまここ ↓
はじめに
先日 Arc<T>
や Mutex<T>
に触れたので、そろそろ「いつか理解する」と放置していた ↓ の構造体について整理する。
筆者の方の補足記事 ↓
筆者の理解度は先月の時点で「Box<T>
はみたことある、ほかは知らん」くらい。
スマートポインタとは
スマートポインタとは、通常の参照のように使えるだけでなく、追加のメタデータと能力を持つデータ構造のこと。
また、多くの場合スマートポインタは対象とするデータを所有している。
通常、スマートポインタは構造体で実装され、Deref
トレイトと Drop
トレイトを実装する。
次の記事では構造体 String
や構造体 Vec
もスマートポインタとして紹介されている。
Deref トレイト
Deref
トレイトは参照外し演算子 ( *
) の挙動を定義できる。
次の構造体 Foo
を所有する変数 foo
は、参照ではないため *foo
は当然コンパイルできない。
#[derive(Debug)]
struct Foo {
value: i32
}
fn main() {
let foo = Foo { value: 42 };
println!("{:?}", foo); // Foo { value: 42 }
println!("{:?}", *foo); // type `Foo` cannot be dereferenced
}
構造体 Foo
が Deref
トレイトを実装している場合、参照外し演算子 ( *
) により次の実装例では所有している 42
が得られる。
use std::ops::Deref;
#[derive(Debug)]
struct Foo {
value: i32
}
impl Deref for Foo {
type Target = i32;
fn deref(&self) -> &Self::Target {
&self.value
}
}
fn main() {
let foo = Foo { value: 42 };
println!("{:?}", foo); // Foo { value: 42 }
println!("{:?}", *foo); // 42
}
上記コードの *foo
は *(foo.deref())
に相当する。
また Rust には参照外し型強制という変換が組み込まれており、Deref
トレイトを実装した構造体の参照を参照外し後の型の参照に変換する。
この変換は参照を関数やメソッドの引数に渡すときに型が一致しないと自動的に行われる。
次のコードでは &i32
を期待するところに &foo
を渡せている。
use std::ops::Deref;
#[derive(Debug)]
struct Foo {
value: i32
}
impl Deref for Foo {
type Target = i32;
fn deref(&self) -> &Self::Target {
&self.value
}
}
fn is_even(n: &i32) -> bool {
n % 2 == 0
}
fn main() {
let foo = Foo { value: 42 };
let b = is_even(&foo);
println!("{}", b); // true
}
構造体 String
を引数 &str
に渡せるのも Deref
トレイトによるため。
fn hello(name: &str) {
println!("hello {}", name);
}
fn main() {
let name: String = String::from("John");
hello(&name); // hello John
}
構造体 String
が Deref
トレイトを Target = str
で実装している。
impl Deref for String {
type Target = str;
fn deref(&self) -> &str {
...
}
}
Drop トレイト
Drop
トレイトは値がスコープを抜けるときの挙動を定義できる。
次の構造体 Foo
を所有する変数 foo
は、Drop
トレイトの実装によりスコープから抜けるときメッセージを出力する。
#[derive(Debug)]
struct Foo {
value: i32,
}
impl Drop for Foo {
fn drop(&mut self) {
println!("drop: {:?}", self);
}
}
fn main() {
println!("scope start"); // scope start
{
let foo = Foo { value: 42 };
} // drop: Foo { value: 42 }
println!("scope end"); // drop: end
}
Drop
トレイトにより、リソースの解放やロックの解除を漏らさず実行できるようになる。
シングルスレッド用スマートポインタ
シングルスレッドで用いるスマートポインタを整理する。
ここで挙げる構造体はマルチスレッドでは使えない。
( 理由はマルチスレッドの項で解説 )
マルチスレッドでは使えないかわりに、スレッドセーフにするための実行コストを持たない。
Box<T>
Rust において値はスタック [1] に割り当てられるが、構造体 Box
を使うとヒープ [2] に割り当てられる。
次のコードの変数 boxed_n
はスタックに配置され、ヒープに展開した値を所有している。
fn main() {
let n = 42;
let boxed_n = Box::new(42);
}
ところで構造体 String
と構造体 Box
はよく似ている。
スタックにはサイズが固定で既知のものしか配置できないが、文字列は実行しないとサイズがわからない。
その問題を解決するために、構造体 String
はヒープに展開された文字列を所有するスマートポインタになっている。
fn main() {
let sting1 = String::from("Lorem ipsum dolor sit amet");
let sting2 = String::from("Hello World");
}
スマートポインタ自体のサイズはコンパイル時にわかる [3] ため、スタックに配置できる。
同様に構造体 Box
を使うと実行するまでサイズのわからないものを固定サイズのものとして扱える。
これは再帰的なデータ構造を実装したり、トレイトオブジェクトのために必要になる。
( 詳細は本記事の範囲外とする )
Rc<T>
構造体 Rc
は複製してもコピーが行われず、かわりに所有する値への別の参照が作成される。
RC は Reference Counting を意味し、構造体 Rc
は自身が所有する値への参照がいくつあるかを管理している。
カウントは複製 ( clone
) すると増加し、破棄 ( drop
) すると減少する。
同じ値を複数の値が所有するグラフのようなデータ構造で用いられる。
use std::rc::Rc;
fn main() {
let rc1: Rc<String> = Rc::new(String::from("foo"));
println!("refs: {}", Rc::strong_count(&rc1)); // refs: 1
{
let rc2: Rc<String> = rc1.clone();
println!("refs: {}", Rc::strong_count(&rc1)); // refs: 2
println!("refs: {}", Rc::strong_count(&rc2)); // refs: 2
}
println!("refs: {}", Rc::strong_count(&rc1)); // refs: 1
println!("{:?}", rc1.len()); // 3
}
Deref
トレイトにより Rc<String>
を String
のように扱えるため、上記 rc1.len()
のように所有する値のメソッドを明示的な変換なしに実行できる。
drop
メソッドは参照の数をひとつ減らし、どこからも参照されなくなったら所有している値を破棄する。
unsafe impl<#[may_dangle] T: ?Sized, A: Allocator> Drop for Rc<T, A> {
fn drop(&mut self) {
unsafe {
self.inner().dec_strong(); // decrement ( 筆者注釈 )
if self.inner().strong() == 0 {
// destroy the contained object
...
}
}
}
}
したがって次のコードの構造体 Foo
の破棄は、Rc<Foo>
への参照がすべてなくなるタイミング一度だけ行われる。
use std::rc::Rc;
#[derive(Debug)]
struct Foo {
value: i32,
}
impl Drop for Foo {
fn drop(&mut self) {
println!("drop: {:?}", self);
}
}
fn main() {
{
let rc1: Rc<Foo> = Rc::new(Foo { value: 42 });
{
let rc2: Rc<Foo> = rc1.clone();
println!("message 1"); // message 1
// drop rc2
}
println!("message 2"); // message 2
// drop rc1
// drop: Foo { value: 42 }
}
println!("message 3"); // message 3
}
Cell<T>
構造体 Cell
は mut
なしに内部で所有している値を変更できる。
このような特性を内部可変性 ( Interior Mutability ) と呼ぶ。[4] [5]
use std::cell::Cell;
fn main() {
let c: Cell<i32> = Cell::new(42);
println!("{:?}", c); // Cell { value: 42 }
c.set(c.get() * 2);
println!("{:?}", c); // Cell { value: 84 }
c.replace(42);
println!("{:?}", c); // Cell { value: 42 }
}
unsafe
と聞くと心配になるが、構造体 Cell
は次のような仕組みで通常の借用ルールに則り安全を保証してくれている。
まず Cell<T>
から &T
を得る方法がないため、replace
メソッドが unsafe
の中で &mut T
を扱っても &T
と &mut T
が同時に存在することはない。
また &mut T
を得られる get_mut
メソッドを呼ぶには Cell<T>
自体を可変参照にする必要があり、同一の Cell<T>
から複数の &mut T
を得ることはできない。
構造体 Cell
の get
メソッドは T: Copy
を返却する。
したがって構造体 String
のようなコピーできない値を扱うことは実質できない。
impl<T: Copy> Cell<T> {
pub fn get(&self) -> T {
...
}
}
RefCell<T>
構造体 Cell
がコピーできない値を扱えないため、必要に応じて構造体 RefCell
を使う。
構造体 RefCell
は構造体 Cell
の上位互換だが、借用ルールの検査はコンパイル時から実行時になる。
RefCell<T>
から borrow
メソッドで Ref<T>
が得られる。
use std::cell::{RefCell, Ref};
fn main() {
let rc: RefCell<String> = RefCell::new(String::from("foo"));
let r: Ref<String> = rc.borrow();
println!("{}", r); // foo
println!("{}", r.len()); // 3
}
また borrow_mut
メソッドでは RefMut<T>
が得られ、これは更新が行える。
次のコードの不変の変数 rc
から得られた RefMut<String>
は、&mut self
を必要とする String::push
メソッドを実行できる。
use std::cell::RefCell;
fn main() {
let rc: RefCell<String> = RefCell::new(String::from("foo"));
rc.borrow_mut().push('!');
println!("{:?}", rc); // foo!
}
明示的な変換なしに &mut String
が得られるのは、構造体 RefMut
が DerefMut
トレイトを実装しているため。
構造体 Box
などと異なり、構造体 RefCell
は借用ルールを破るとコンパイルエラーではなく実行時エラーになる。
たとえば不変参照と可変参照を同時に存在させようとすると panic が発生する。
use std::cell::{Ref, RefCell, RefMut};
fn main() {
let rc: RefCell<String> = RefCell::new(String::from("foo"));
let mut rm: RefMut<String> = rc.borrow_mut();
let r: Ref<String> = rc.borrow();
// ↑ already mutably borrowed: BorrowError
}
use std::cell::{Ref, RefCell, RefMut};
fn main() {
let rc: RefCell<String> = RefCell::new(String::from("foo"));
let r: Ref<String> = rc.borrow();
let mut rm: RefMut<String> = rc.borrow_mut();
// ↑ already borrowed: BorrowMutError
}
構造体 RefCell
の活用例のひとつとして、The Rust Programming Language の通知モックの例が参考になる。 [6]
マルチスレッド用スマートポインタ
マルチスレッドで用いるスマートポインタを整理する。
まずスレッドを作成するコードを確認し、次になぜここで挙げる構造体はマルチスレッドで使用できるのか整理する。
スレッドを作る
スレッドは thread::spawn
関数で作成する。
完了を待つには構造体 JoinHandle
の join
メソッドを使う。
use std::thread;
fn main() {
let t1 = thread::spawn(|| {
println!("thread 1");
});
let t2 = thread::spawn(|| {
println!("thread 2");
});
t1.join().unwrap();
t2.join().unwrap();
// thread 1
// thread 2
}
変数 value
を参照で捕捉するクロージャは、thread::spawn
メソッドに渡せない。
use std::thread;
fn main() {
let value = 42;
let handle1 = thread::spawn(|| {
println!("thread 1: {}", value);
});
handle1.join().unwrap();
}
// error[E0373]: closure may outlive the current function,
// but it borrows `value`, which is owned by the current function
クロージャは可能な限り変数 value
を &T
として捕捉しようとするが、thread::spawn
メソッドの型定義は F: Send + 'static
になっておりクロージャが参照を含まないことを要求している。
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
...
}
コンパイルするには変数 value
の所有権を明示的にクロージャに move
する必要がある。
use std::thread;
fn main() {
let value = 42;
let handle1 = thread::spawn(move || {
println!("thread 1: {}", value);
});
handle1.join().unwrap();
// thread 1: 42
}
クロージャが行う変数の捕捉や Fn
と FnMut
と FnOnce
の違いについては、整理済みのため本記事では割愛する。
Sync トレイトと Send トレイト
Sync
トレイトは、複数のスレッドから参照されても安全であることを示すマーカートレイト。
Send
トレイトは、所有権をスレッド間で転送できることを示すマーカートレイト。
ほとんどの型は Sync
トレイトと Send
トレイトを実装している ( 以降 Sync + Send
とする ) が、構造体 Rc
など一部の型は例外である。
thread::spawn
メソッドは F
も T
も Send
トレイトが要求されているため、次のように Rc<i32>
などを扱うことはできない。
use std::rc::Rc;
use std::thread;
fn main() {
let value = Rc::new(42);
let handle1 = thread::spawn(move || {
println!("thread 1: {}", value);
});
handle1.join().unwrap();
// cannot be sent between threads safely
}
構造体 Rc
は参照カウンタの部分に内部可変性を用いており、カウンタが複数のスレッドから変更されてしまうとレースコンディション [7] が発生してしまうため。
レースコンディションを避けるには、ロックやセマフォなどの仕組みを用いて複数のスレッドが同時に共有データにアクセスしないようにする必要がある。
Rust ではそれらの仕組みを内包するスレッドセーフなスマートポインタも提供されており、これらだけに Sync + Send
トレイトが実装されている。
Arc<T>
構造体 Arc
はスレッドセーフな構造体 Rc
であり、Atomically Reference Counted を意味する。[8]
先に整理した Rc<T>
を用いた参照カウントのコードは、Arc<T>
でも同じように実行できる。
Rc<T> ( 再掲 )
use std::rc::Rc;
fn main() {
let rc1: Rc<String> = Rc::new(String::from("foo"));
println!("refs: {}", Rc::strong_count(&rc1)); // refs: 1
{
let rc2: Rc<String> = rc1.clone();
println!("refs: {}", Rc::strong_count(&rc1)); // refs: 2
println!("refs: {}", Rc::strong_count(&rc2)); // refs: 2
}
println!("refs: {}", Rc::strong_count(&rc1)); // refs: 1
println!("{:?}", rc1.len()); // 3
}
use std::sync::Arc;
fn main() {
let arc1: Arc<String> = Arc::new(String::from("foo"));
println!("refs: {}", Arc::strong_count(&arc1)); // refs: 1
{
let arc2: Arc<String> = arc1.clone();
println!("refs: {}", Arc::strong_count(&arc1)); // refs: 2
println!("refs: {}", Arc::strong_count(&arc2)); // refs: 2
}
println!("refs: {}", Arc::strong_count(&arc1)); // refs: 1
println!("{:?}", arc1.len()); // 3
}
利用側にとっての Rc<T>
と Arc<T>
の違いは、構造体 Arc
は Sync + Send
であること。
unsafe impl<T: ?Sized + Sync + Send, A: Allocator + Send> Send for Arc<T, A> {}
unsafe impl<T: ?Sized + Sync + Send, A: Allocator + Sync> Sync for Arc<T, A> {}
したがって、次のように同一の構造体 Arc
が所有する値 ( 42
) に対する複数の参照を複数のスレッドに渡せる。
当然参照カウントもおかしくならない。
use std::sync::Arc;
use std::thread;
fn main() {
let value = Arc::new(42);
let v1 = value.clone();
let handle1 = thread::spawn(move || {
println!("thread 1 value: {}", v1);
let v3 = v1.clone();
println!("thread 1 count: {}", Arc::strong_count(&v3));
});
let v2 = value.clone();
let handle2 = thread::spawn(move || {
println!("thread 2 value: {}", v2);
let v4 = v2.clone();
println!("thread 2 count: {}", Arc::strong_count(&v4));
});
handle1.join().unwrap();
handle2.join().unwrap();
// thread 1 value: 42
// thread 1 count: 3
// thread 2 value: 42
// thread 2 count: 4
}
構造体 Arc
は参照カウントのために内部可変性を用いているが、意味的には読み取り用の構造体である。
更新を行いたい場合は別の構造体を用いる。
AtomicT
スレッドセーフなプリミティブ値として構造体 AtomicI32
などが標準ライブラリで提供されている。
実際には次のいずれかの具体的な構造体だが、本記事では便宜上これらを総称して AtomicT
と呼ぶ。
AtomicBool
AtomicI8
AtomicI16
AtomicI32
AtomicI64
AtomicIsize
AtomicPtr
AtomicU8
AtomicU16
AtomicU32
AtomicU64
AtomicUsize
AtomicT
は名前の通り原子的な操作 [9] ができる値であり、fetch_add
メソッドなどを用いてスレッドセーフな更新処理が行える。
use std::sync::atomic::{AtomicI32, Ordering};
fn main() {
let a: AtomicI32 = AtomicI32::new(42);
a.fetch_add(1, Ordering::Relaxed);
println!("{:?}", a); // 43
}
AtomicT
も内部可変性を用いており、また Sync + Send
である。
メモリオーダリング ( Ordering::Relaxed
) については、本記事では範囲外とする。
AtomicT
そのものを複数のスレッドから所有することはできないため、それが可能な構造体 Arc
に包んで扱う。
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
fn main() {
let value = Arc::new(AtomicUsize::new(0));
let v1 = value.clone();
let handle1 = thread::spawn(move || {
v1.fetch_add(1, Ordering::Relaxed);
});
let v2 = value.clone();
let handle2 = thread::spawn(move || {
v2.fetch_add(1, Ordering::Relaxed);
});
handle1.join().unwrap();
handle2.join().unwrap();
println!("{:?}", *value); // 2
}
AtomicT
は、複数のスレッドからカウンタを更新したい場合などに適切である。[10] [11]
Mutex<T>
AtomicT
より複雑なデータ構造を複数のスレッドで更新したい場合は、構造体 Mutex
を使用する。
ミューテックスは相互排他 ( Mutual Exclusion ) を意味し、ロックを用いて同時アクセスできなくすることでスレッドセーフを実現している。
use std::sync::{Mutex, MutexGuard};
fn main() {
let m = Mutex::new(String::from("foo"));
m.lock().unwrap().push('!');
m.lock().unwrap().push('!');
m.lock().unwrap().push('!');
let mg: MutexGuard<String> = m.lock().unwrap();
println!("{}", mg); // foo!!!
}
lock
メソッドで LockResult<MutexGuard<T>>
が得られ、ロックできた場合は unwrap
メソッドで MutexGuard<T>
を取り出す。
構造体 MutexGuard
は、内部可変性と DerefMut
トレイトにより &mut T
のように扱え、Drop
トレイトによりロックの解除漏れが発生しないようになっている。
impl<T: ?Sized> Drop for MutexGuard<'_, T> {
fn drop(&mut self) {
unsafe {
...
self.lock.inner.unlock();
}
}
}
ロックが解除されるのは MutexGuard<T>
が破棄 ( drop
) されるタイミングなので、次のように変数 mg1
が生きている間はそれより先のコードには到達しない。
use std::sync::Mutex;
fn main() {
let m: Mutex<i32> = Mutex::new(42);
let mg1 = m.lock();
println!("{:?}", mg1); // Ok(42)
let mg2 = m.lock();
println!("{:?}", mg2); // 応答なし
println!("done"); // 到達しない
}
構造体 Mutex
は、AtomicT
ではない値を複数のスレッドから更新したい場合に使用を検討する。
AtomicT
と同様に構造体 Arc
に包んで共有する。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let value = Arc::new(Mutex::new(String::from("foo")));
let v1 = value.clone();
let handle1 = thread::spawn(move || {
v1.lock().unwrap().push('!');
});
let v2 = value.clone();
let handle2 = thread::spawn(move || {
v2.lock().unwrap().push('!');
});
handle1.join().unwrap();
handle2.join().unwrap();
println!("{}", value.lock().unwrap()); // foo!!
}
構造体 Mutex
は 1 つの更新か 1 つの参照のどちらかだけを認める厳密なロックを行いたい場合に適切である。
( writer / reader )
RwLock<T>
構造体 Mutex
と異なり構造体 RwLock
は 1 つの更新か複数の参照が許される。
( writer / readers )
さきほどの構造体 Mutex
の例とほぼ同じ次のコードは、複数の read が許されるためロック待ちが発生しない。
use std::sync::RwLock;
fn main() {
let rw: RwLock<i32> = RwLock::new(42);
let r1 = rw.read();
println!("{:?}", r1); // Ok(42)
let r2 = rw.read();
println!("{:?}", r2); // Ok(42)
println!("done"); // done
}
reader が存在する場合、writer は生まれない。
use std::sync::RwLock;
fn main() {
let rw: RwLock<i32> = RwLock::new(42);
let r1 = rw.read();
println!("{:?}", r1); // Ok(42)
let w1 = rw.write();
println!("{:?}", w1); // 応答なし
println!("done"); // 到達しない
}
writer が存在する場合、reader 生成は失敗する。
use std::sync::RwLock;
fn main() {
let rw: RwLock<i32> = RwLock::new(42);
let w1 = rw.write();
println!("{:?}", w1); // Ok(42)
let r1 = rw.read();
println!("{:?}", r1); // rwlock read lock would result in deadlock
println!("done"); // 到達しない
}
内部可変性や Deref
トレイトや Drop
トレイトについては、構造体 Mutex
とほぼ同じ。
頻繁に参照されるが更新頻度は低い値 ( e.g. プロダクトの実行時設定 ) を扱う場合は、Mutex<T>
より RwLock<T>
が適切な場合がある。
まとめ
概要 | |
---|---|
Box<T> |
サイズのわからない値をスタックで所有できる |
Rc<T> |
同じ値を複数の値から所有できる |
Cell<T> |
不変参照でも更新できる ( Copy できる値のみ ) |
RefCell<T> |
不変参照でも更新できる |
Arc<T> |
スレッドセーフな Rc<T>
|
AtomicT |
スレッドセーフに更新できるプリミティブ値 |
Mutex<T> |
スレッドセーフに更新できる任意の値 ( write / reader ) |
RwLock<T> |
スレッドセーフに更新できる任意の値 ( write / readers ) |
Memory Container Cheat-sheet
ここまで理解すればチートシートを理解できる。
以下、ざっくりペン入れ。
シングルスレッドでいい場合。
余計なコストがかからない。
複数の値で所有したいなら、構造体 Rc
が確定。
サイズ不定のものをスタックで持ちたいなら、ヒープに展開する構造体 Box
で確定。
mut
なしに変更したいなら、構造体 Cell
か構造体 RefCell
が確定。
使い分けは Copy
トレイトを実装してるか次第。
マルチスレッドの場合。
構造体と所有する T
に Sync + Send
が求められる。
複数の値で所有したいなら、構造体 Arc
が確定。
構造体 Rc
と同様。
書き込みが少なく読み込みの複数同時アクセスを許容できるなら、構造体 RwLock
で確定。
読み込みの同時アクセスを認めないなら、AtomicT
か構造体 Mutex
で確定。
使い分けはプリミティブであり標準ライブラリに AtomicT
が用意されているかどうか。
ざっくりだが十分だろう。
おわりに
ぶっちゃけ、まじめに調べている間に理解してしまいチートシートはいらなくなった。
ずっといつか理解したいと思っていたので、ちゃんと学べてよかった。
-
プロセス起動時に割り当てられる領域で、ヒープと比較して高速にアクセスできるが小さい。 ↩︎
-
必要に応じてプロセスが実行時に割り当てる領域で、スタックと比較してアクセスが遅いが大きい。 ↩︎
-
スマートポインタには追加のメタデータなどがあるが、それらのサイズは固定なので構造体自体のサイズも固定。 ↩︎
-
構造体
Cell
の実装内部ではunsafe
が使われている。 ↩︎ -
構造体
Rc
の参照カウントが増減するのも内部可変性によるもの。 ↩︎ -
通知を行う構造体は
mut
を持たないが、同一のトレイトを実装するモックは IO ではなく通知内容をベクターにためたいというもの。一部のトレイト実装だけmut
を付けることはできないが、内部可変性を用いるとmut
をつけないままベクターを更新できる。 ↩︎ -
共有データへの更新が複数のスレッドから発生し、変更結果が意図しない状態になること。たとえばスレッド A と B で「A が 0 をよみとり」「B が 0 をよみとり」「A が +1」「B が +1」すると最終結果が 1 になってしまう。 ↩︎
-
Atomically ( 原子的 ): 分割できないというニュアンスで理解するとわかりいい。参照処理と更新処理が不可分である。更新処理は行われているか完全に終わっているかしかないため、割り込みが起きずレースコンディションが発生しない。 ↩︎
-
参照と更新が不可分である。 ↩︎
-
詳細は次の課題とするが、おそらく
AtomicT
はセマフォで実現されている。アトミック操作はプラットフォームの低レベルな機能に依存しているため、Atomic<T>
のようにいずれの型にも対応させることができない。 ↩︎
Discussion