LoginSignup
10
7

More than 1 year has passed since last update.

Rustにおけるシングルトンの安全な実装方法

Posted at

はじめに

この記事はRustにおけるシングルトンの実装方法を見て書いたものです。初めは記事にコメントしようかと思ったのですが、コメントでは長くなりすぎると思い記事化することにしました。

TL; DR

once_cell 使え。
(将来stdでonce_cellが安定化したら)LazyLock使え。

unsafe は安全な時にしか使ってはいけない

矛盾するようですが、 unsafe キーワードを使うのは安全性を保証できるときに限るべきです。
Rustの有効な(コンパイル可能な)コードには、以下の3種類があります。

  1. 静的解析で安全性が保証できるコード
  2. 静的解析で安全性が保証できないが、(事前条件を満たせば)ロジック的には安全であるコード
  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 トレイトを実装した型でしか作れず、 CellSync トレイトを実装していないので 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_INTEGEROption<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() が呼ばれた場合を考えましょう。以下のようになる可能性があります。

  1. スレッドAが !SINGLETON_INTEGER.is_some をチェック、 true だったので {} 内部に入る
  2. スレッドAが SINGLETON_INTEGER.is_some = true を実行
  3. スレッドBが !SINGLETON_INTEGER.is_some をチェック、 false だったので処理をスキップ
  4. スレッドBが SINGLETON_INTEGER.is_some をチェック、 true だったので {} 内部に入る
  5. スレッドBが SINGLETON_INTEGER.assume_init_ref() を実行、 &'static SingletonInteger の参照を返す
  6. スレッドBが value() を実行
  7. スレッド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 として実装予定です。

おわりに

この記事を途中まで書いていたら、

RustでSingletonを実現する方法

が投稿されていました。こちらの記事の方が Mutex を使った書き込みもできるパターンに触れていたり、once_cellクレート以前に良く使われていたlazy_staticクレートに触れていたりと網羅的な内容で、もう投稿しなくてもいいかなとも思ったのですが、せっかく書いたし勿体ないので投稿します。

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7