はじめに
この記事はRustにおけるシングルトンの実装方法を見て書いたものです。初めは記事にコメントしようかと思ったのですが、コメントでは長くなりすぎると思い記事化することにしました。
TL; DR
once_cell 使え。
(将来stdでonce_cellが安定化したら)LazyLock使え。
unsafe
は安全な時にしか使ってはいけない
矛盾するようですが、 unsafe
キーワードを使うのは安全性を保証できるときに限るべきです。
Rustの有効な(コンパイル可能な)コードには、以下の3種類があります。
- 静的解析で安全性が保証できるコード
- 静的解析で安全性が保証できないが、(事前条件を満たせば)ロジック的には安全であるコード
- 静的解析で安全性が保証できないし、いかなる意味でも安全ではないコード
1のコードは unsafe
ブロックを使わずに実装できます。2のコードと3のコードは unsafe
ブロックの中にしか書けません。しかし、 unsafe
ブロックの中であっても、3のコードは書くべきではありません。 unsafe
というのはあくまで安全だけど静的解析で安全性が保証できない処理を行うために用意された穴であって、 unsafe
ブロックの中なら何をやっても良いと捉えるのではなく、 unsafe
ブロックの中に書いた処理の安全性は、コンパイラが保証してくれない分、プログラマが強く保証しなければならない、と捉えるべきです。
RustのStandard library developers Guideにはsafety comments policyという項目があります。unsafe
関数のドキュメントコメントには # Safety
節を書くことと、 unsafe
ブロックの前には SAFETY:
を付けたコメントを書いて、安全に使うための事前条件や、なぜそれが安全なのかを明示するというポリシーです。もちろんこれは標準ライブラリの開発者ガイドラインにおけるポリシーであって全てに適用される訳ではありませんが、これを書くことができないなら、それは真に安全な処理ではないということは肝に銘じておくべきです。
という前置きをした上で、元記事の
Rustでは、
static
キーワードとunsafe
ブロックを使用してシングルトンを実装することができます。
という文を読んでみましょう。当然、その安全性はどうやって保証するのか? という疑問が浮かびます。
実際、元記事で提示されたコードには、安全ではない可能性が含まれています。
static mut
変数のマルチスレッド安全性
static
変数はグローバルに一意な変数です。それはつまり複数のスレッドから参照されうる、ということです。
そのため、 static mut
変数に対するアクセスは unsafe
になります。複数のスレッドからの書き込みや読み込みが競合しうるからです。
なお、 mut
が付かない static
変数に対する読み込みアクセスだけなら unsafe
にはなりません。書き込みが行われなければいくら同時に読み込みを行っても安全だからです。Cell
のような内部可変パターンもあるじゃないか、と思う方もいると思いますが、 static
変数は Sync
トレイトを実装した型でしか作れず、 Cell
は Sync
トレイトを実装していないので static
変数にすることができません。
元記事のコードでは、
pub fn singleton_integer() -> &'static SingletonInteger {
unsafe {
if SINGLETON_INTEGER.is_none() {
SINGLETON_INTEGER = Some(SingletonInteger { value: 123 });
}
SINGLETON_INTEGER.as_ref().unwrap()
}
}
という処理を行っています。 SINGLETON_INTEGER
は Option<SingletonInteger>
型の変数なので、 「None
または Some
のフラグ」と「SingletonInteger一個分のメモリ領域」をメンバーに持つ構造です。この構造を
struct OptionalSingletonInteger {
is_some: bool,
value: MaybeUninit<SingletonInteger>,
}
という形で表現してみましょう。(これはあくまで Option<SingletonInteger>
の内部構造を模式的に表すためのもので、実際にコンパイラがこのような構造で扱うことを保証するものではありません)
SINGLETON_INTEGER
の型を上記の OptionalSingletonInteger
に置き換えてみると、
pub fn singleton_integer() -> &'static SingletonInteger {
unsafe {
if !SINGLETON_INTEGER.is_some {
SINGLETON_INTEGER.is_some = true;
SINGLETON_INTEGER.value.write(SingletonInteger { value: 123 });
}
if SINGLETON_INTEGER.is_some {
SINGLETON_INTEGER.assume_init_ref()
} else {
panic!();
}
}
}
という処理になります。
SINGLETON_INTEGER
が未初期化の時、スレッドAとスレッドBで同時に singleton_integer().value()
が呼ばれた場合を考えましょう。以下のようになる可能性があります。
- スレッドAが
!SINGLETON_INTEGER.is_some
をチェック、true
だったので{}
内部に入る - スレッドAが
SINGLETON_INTEGER.is_some = true
を実行 - スレッドBが
!SINGLETON_INTEGER.is_some
をチェック、false
だったので処理をスキップ - スレッドBが
SINGLETON_INTEGER.is_some
をチェック、true
だったので{}
内部に入る - スレッドBが
SINGLETON_INTEGER.assume_init_ref()
を実行、&'static SingletonInteger
の参照を返す - スレッドBが
value()
を実行 - スレッドAが
SINGLETON_INTEGER.value.write(SingletonInteger { value: 123 });
を実行
この例だとスレッドBが value()
を実行した時点で内部は初期化されていないので、未定義動作になります。
これはあくまでも一例で、必ずこうなる、という訳ではありません。実際には問題は複雑で、コンパイラによる命令のリオーダー、CPUのパイプラインによる命令のリオーダー、マルチコアCPUにおけるメモリコヒーレンスの問題など様々な落とし穴が存在します。
一方で、二つのスレッドから同時に書き込みを伴うアクセスが行われなければ問題は発生しないため、プログラムの構造によってはまず発生しない可能性もあり、同じようなコードが見過ごされていることも多いかもしれません。
安全なシングルトンを作る
という訳で、元記事のコードを安全なものに変えてみようと思います。
それにあたって、 once_cell クレートを導入します。
once_cell::sync::Lazy
は、初回アクセス時に一度だけ初期化を行う値を表現することができ、また、内部可変パターンの一種なため mut
を使わずに利用することができるため、 unsafe
ブロックを使わずに書くことができます。
use derive_getters::Getters;
use once_cell::sync::Lazy;
#[derive(Debug, Getters)]
pub struct SingletonInteger {
value: i64,
}
pub fn singleton_integer() -> &'static SingletonInteger {
&*SINGLETON_INTEGER
}
static SINGLETON_INTEGER: Lazy<SingletonInteger> = Lazy::new(|| SingletonInteger { value: 123 });
また、 once_cellクレートと同様の機能は標準ライブラリにも提案されており、 once_cell::sync::Lazy
は
std::sync::LazyLock
として実装予定です。
おわりに
この記事を途中まで書いていたら、
が投稿されていました。こちらの記事の方が Mutex
を使った書き込みもできるパターンに触れていたり、once_cellクレート以前に良く使われていたlazy_staticクレートに触れていたりと網羅的な内容で、もう投稿しなくてもいいかなとも思ったのですが、せっかく書いたし勿体ないので投稿します。