後学のためにRustでSingletonを実現する方法をまとめました。改めて調べ直すと、いろいろな方法があって迷ってしまうので比較してみました。Rustでアプリ内で共通の設定パラメータを用意したり、ロガーなどを実装するのに役立つでしょう。
結論 – once_cellクレートを使うのが簡単
この投稿を書いている時点で、once_cellクレートはRust標準に取り込まれることが検討されています。以下でいろいろな方法を紹介しますが、once_cellクレートを使う方法が、簡潔で見通しがよくなります。
use once_cell::sync::Lazy;
use std::sync::Mutex;
// 設定
#[derive(Debug)]
pub struct Config {
debug_level: usize,
data: Vec<usize>,
}
// インスタンスの初期化
pub static INSTANCE: Lazy<Mutex<Config>> = Lazy::new(|| {
Mutex::new(Config {
data: vec![1],
debug_level: 0,
})
});
fn main() {
// 設定の変更
INSTANCE.lock().unwrap().data.push(2);
INSTANCE.lock().unwrap().debug_level = 1;
// 設定の参照
println!("config={:?}", INSTANCE.lock().unwrap());
// 再び、変更と参照
INSTANCE.lock().unwrap().data.push(3);
println!("config={:?}", INSTANCE.lock().unwrap());
}
(参考)以下がonce_cellのリポジトリです。
ちなみになぜ、Singoletonを調べたのかと言えば、もっと手軽に乱数を使えるコンパクトなクレートが欲しかったので、自分でクレート作ったからです。良かったら使ってみてください!
なぜRustでSingletonを使うのが難しいのか
とは言え、初見では上記の方法でも十分複雑です。なぜ、Rustでは簡単にSingletonが使えないのでしょうか。
Rustで可変のグローバル変数は非推奨である
まず、Rustでグローバル変数を使うには、staticを使います。ただし、可変なグローバル変数を使うためには、unsafe { … } で括る必要があるのです。
// グローバル変数を定義
static mut TAX: f32 = 0.1;
fn main() {
// 安全でないことを宣言
unsafe {
// 可変なグローバル変数を使う
println!("Price: {}", TAX * 300.0);
// staticな変数を変更する
TAX = 0.08;
println!("Price: {}", TAX * 300.0);
}
}
しかし、unsafeは、安全でないために、この仕組みが用意されているわけで、使わない方が良いでしょう。また、unsafe内はスレッドセーフでもないため、初期化処理に時間がかかる場合に、内容を壊してしまう可能もあります。
関数の引数にコンテキストを与えるのが基本
それでは、ある程度の規模のアプリを作る場合、設定ファイルやその他、いろいろなパーツ全体で使うパラメーターなどどのように受け渡したら良いのでしょうか。簡単な解決作では、基本的には、コンテキストとして毎回、設定パラメータを引き継いで与えるのが良いでしょう。
// 全体で使うデータ
pub struct AppContext {
pub title: String,
pub log_level: usize,
// ...
}
fn main() {
let context = AppContext::new();
foo(&mut context);
}
// コンテキストを使う関数1など
pub fn foo(context: &mut AppContext) {
// ...
bar(context, data);
// ...
}
// コンテキストを使う関数2など
pub fn bar(context: &mut AppContext, data: usize) {
// ...
}
しかし、毎回パラメータを受け渡すのが面倒ですよね。
オブジェクトの動的な差し替えで実現する方法1
それで、thread_local!マクロとArc/RwLockを使う方法が用意されています。グローバルなオブジェクトの状態を変更するのではなく新しい状態にオブジェクトを差し替えるという方法です。
use std::sync::{Arc, RwLock};
// グローバルな設定内容を定義
#[derive(Default)]
struct Config {
pub debug_mode: bool,
}
impl Config {
// 設定の参照
pub fn current() -> Arc<Config> {
CURRENT_CONFIG.with(|c| c.read().unwrap().clone())
}
// 設定を上書き
pub fn make_current(self) {
CURRENT_CONFIG.with(|c| *c.write().unwrap() = Arc::new(self))
}
}
thread_local! {
static CURRENT_CONFIG: RwLock<Arc<Config>> = RwLock::new(Default::default());
}
fn main() {
// 設定を書き換え
Config { debug_mode: true }.make_current();
// 設定を参照
if Config::current().debug_mode {
// do something
}
}
上記のコードは以下のサイトより抜粋したものです。
オブジェクトの動的な差し替えで実現する方法2
なお、似たような手法ですが下記のような実装例もあります。こちらは、インスタンスの初期化は一度きりで、その後、構造体のフィールドをMutexにして書き換えるものです。
use std::sync::{Arc, Mutex};
#[derive(Default)]
struct Singleton {
count: Mutex<u8>
}
impl Singleton {
pub fn get_instance() -> Arc<Singleton> {
SINGLETON_POOL.with(|singleton_pool| singleton_pool.clone())
}
}
thread_local! {
static SINGLETON_POOL: Arc<Singleton> = Arc::new(Default::default());
}
fn instance_and_use_singleton() {
let singleton = Singleton::get_instance();
let mut count = singleton.count.try_lock().unwrap();
println!("singleton init value: {}", count);
*count += 1;
println!("singleton end value: {}", count);
}
fn main() {
instance_and_use_singleton();
instance_and_use_singleton();
}
上記のコードは下記のサイトからの抜粋です。
なお、MutxとRwLockの違いに関しては以下が分かりやすかったです。
-
- Mutex … リソースの利用者は、 読み書き関わらず1人だけ
- RwLock … 書き込みは1人 読み込みだけ複数人に許す
lazy-staticクレートを使う方法
lazy-staticクレートは下記のように利用します。lazy-staticを使うのが今のところ人気がありますが、once_cellはマクロを使わないということで、once_cellに流れが移ってきているようです。
use lazy_static::lazy_static; // 1.4.0
use std::sync::Mutex;
lazy_static! {
static ref ARRAY: Mutex<Vec<u8>> = Mutex::new(vec![]);
}
fn do_a_call() {
ARRAY.lock().unwrap().push(1);
}
fn main() {
do_a_call();
do_a_call();
do_a_call();
println!("called {}", ARRAY.lock().unwrap().len());
}
その他の情報
(参考) How do I create a global, mutable singleton?
(参考) MaybeUninit / Mutex / Once を使う方法
(参考) Rustで可変 Singletonしたいんだけど、どうしたらいい?
(参考) lazy_static はもう古い!? once_cell を使おう
(参考) Rustのstatic変数とthread local