プログラムがある程度大きくなってくると、任意の場所からアクセスできる データが欲しくなります。いわゆる、グローバルな変数ですね。例えば、
- 設定情報を格納した HashMap
- アプリの状態(context や state と呼ばれるもの)を表す構造体
また、Rust ではマルチスレッドのプログラムが簡単に(しかも安全に)書けますが、個々のスレッドに属するグローバル変数(一般的に、Thread Local Storage / TLS と呼びます)も使いたいことがあります。例えば、
- 擬似乱数生成器のような、データ生成器の state
この記事では、Rust で、このようなデータを安全に扱う方法を紹介します。
-
グローバルなデータ
- イミュータブル(不変)なデータ
- ミュータブル(可変)なデータと、使用上の注意
-
スレッドローカルなデータ
- ミュータブル(可変)なデータと、使用上の注意
Rust のバージョン
なお、Rust は現時点で最新の1.9.0安定版を使用しました。1箇所だけ std::panic::catch_unwind()
が使われており、1.9.0か、それ以降でないと動きませんが、本質的な部分ではないので、それを削除して古い Rust バージョンで動かすこともできます。
グローバルなデータ
原始的すぎる static
変数
Rust にも static
というグローバル変数がありますが、制限が多くて、そのまま使うのは非常に辛いです。
ドキュメント からの引用
初期化
const
、static
どちらも値に対してそれらが定数式でなければならないという要件があります。言い換えると、関数の呼び出しのような複雑なものや実行時の値を指定することはできないとうことです。
これでは、HashMap のようなデータを格納できません。static
変数は、その値がコンパイル時に評価されるので、複雑なことができないのです。そこで、static
のミュータブル版 static mut
を使って、プログラムの起動後に初期化しようとすると、さらに制限が増えます。
この静的な変数
N
はミュータブルであるため、別のスレッドから読まれている間に変更される可能性があり、メモリの不安全性の原因となります。そのためstatic mut
な変数にアクセスを行うことはunsafe
であり、unsafe
ブロック中で行う必要があります。
そうですか。というわけで、しかたなく、こんなプログラムを書き始めます:
-
static mut
な変数を生ポインタ型(例:*mut HashMap
)にして、0 as *mut HashMap
のような方法でコンパイル時に(null ポインタで)初期化する。(初期化しないとコンパイルエラーになる) - プログラムの起動時に、正規の HashMap で初期化し直す。
- アクセスする度に
unsafe { }
ブロックで囲むのは面倒なので、それを wrap した適当なメソッドを生やす。 - ところで、ドキュメントにこんなこと(↓)も書いてあるけど、今のやり方でいいんだっけ、と悩み始める。
さらに言えば、
static
な変数に格納される値の型はSync
を実装しており、かつDrop
は実装していない必要があります。
そして、しばらくすると、Qiita などの記事を通して、lazy_static という、便利なクレートがあることに気づきます:
- Rustとlazy static
- Rust "recursion limit reached while expanding the macro
lazy_static
" でエラーになった場合の対処方法
このクレートは、マクロによるコードの自動生成を通して、static な変数を扱いやすいものに変えてくれます。lazy_static の Deref
トレイトを応用した実装に感心して使い始めた後、またしばらくして、Rust の FAQ でも、間接的にですが lazy_static が紹介されていた ことに気づきます。なんだ、最初から言ってくれれば...。
という流れを、少なくても私は踏んできました。これが、この記事を書こうと思ったきっかけです。
イミュータブルなグローバルデータを lazy_static で実現する
というわけで、グローバルな変数を作る時は、迷わず lazy_static を使いましょう。以下のことが実現できます。
- グローバル変数を、実行時の値(プログラムで書かれた式)で初期化できる。つまり、HashMap なども使用できる。
- グローバル変数を、lazy に初期化できる。つまり、初めてアクセスがあった時に、(たとえ複数スレッドから同時のアクセスがあっても)ただ1度だけ初期化できる。
- 値の型が
Sync
トレイトを実装しているかチェックしてくれる。(実装してない場合は、コンパイルエラーになる)
なお、定数(const
)に限っていうと、このような仕様変更要求(RFC)「0911 const_fn」もあります。ただ、まだ方針を決めようという段階のようで、実現まではしばらく時間がかかりそうです。
では、プログラム例です。まずイミュータブル(不変)の HashMap をグローバル変数にセットしてみましょう。Cargo.toml の dependencies に lazy_static を追加します。
[dependencies]
lazy_static = "0.2.1"
バージョンは、crates.io の lazy_static のページ で確認してください。
main.rs に、グローバル変数を定義しましょう。
#[macro_use]
extern crate lazy_static;
mod app_context {
use std::collections::HashMap;
lazy_static! {
pub static ref MAP: HashMap<u32, &'static str> = {
let mut m = HashMap::new();
m.insert(0, "foo");
m
};
}
}
モジュール(mod
) app_context に、MAP
という名前のイミュータブル static 変数を用意して、要素数が1の HashMap を束縛しています。モジュールを作ることは必須ではありませんが、static 変数が増えた時に名前が衝突しないよう、名前空間を分けるために使っています。
このように、lazy_static! { }
マクロの中に、static 変数を定義すればいいので直感的ですね。この変数にモジュール外からアクセスできるように pub
属性を付けています。
使う側のコードは、こうなります。
const NF: &'static str = "not found";
fn main() {
read_app_map();
println!("Done");
}
fn read_app_map() {
let ref m = app_context::MAP;
assert_eq!("foo", *m.get(&0).unwrap_or(&NF));
assert_eq!(NF, *m.get(&1).unwrap_or(&NF));
}
簡単ですね。
なお、もしこの HashMap を更新するために、mut
属性を持たせようとしても、コンパイルエラーになります。
fn read_app_map() {
// エラー:Cannot borrow immutable static item as mutable
let ref mut m = app_context::MAP;
このようにイミュータブルであることが守られていますので、複数スレッドから同時にアクセスしても、中身が壊れることがなく安全です。
ミュータブルなグローバルデータを lazy_static と RwLock で実現する
次は、ミュータブル(可変)な HashMap をグローバル変数にセットしてみましょう。グローバル変数は、複数スレッドからアクセスされる可能性があるので、Sync
トレイト を実装している必要があります。リンク先のページに、Sync
トレイトを実装している型の一覧がありますが、その中に HashMap はありません。HashMap は複数スレッドからの同時書き込みに対応していないので当然ですね。
Sync
に対応させるためには、HashMap を Mutex や RwLock というロックで包んで守ってあげないといけません。Mutex では、read か write に関係なく、ある時点では、ただ1つのスレッドだけがロックを取得できます。RwLock はマルチリーダーロックで、read のロックは、同時に複数のスレッドから取得でき、write のロックはただ1つのスレッドが取得できます。もし、read ロックを取得しているスレッド(複数可能)が残っていたら、write ロックの取得は、それらのロックが解除するまで待たされます。同様に write ロックを取得しているスレッド(1つ)が残っていたら、read ロックの取得は待たされます。
app_context モジュールに MAP_MUT
static 変数を追加しましょう。
#[macro_use]
extern crate lazy_static;
mod app_context {
use std::collections::HashMap;
use std::sync::RwLock;
lazy_static! {
pub static ref MAP: HashMap<u32, &'static str> = {
// 先ほどと同じ内容のため。省略
};
pub static ref MAP_MUT: RwLock<HashMap<u32, &'static str>> = {
let mut m = HashMap::new();
m.insert(0, "bar");
RwLock::new(m)
};
}
}
使う側は、こうなります。関数 read_write_app_map_mut()
を追加しました。
fn main() {
read_app_map();
match read_write_app_map_mut() {
Ok(()) => (),
Err(e) => println!("Error {}", e),
}
}
fn read_write_app_map_mut() -> Result<(), String> {
// read
{
let m = try!(app_context::MAP_MUT.read().map_err(|e| e.to_string()));
assert_eq!("bar", *m.get(&0).unwrap_or(&NF));
}
// write
{
let mut m = try!(app_context::MAP_MUT.write().map_err(|e| e.to_string()));
m.insert(1, "baz");
}
// read
{
let m = try!(app_context::MAP_MUT.read().map_err(|e| e.to_string()));
assert_eq!("baz", *m.get(&1).unwrap_or(&NF));
}
Ok(())
}
このように、read する時は、app_context::MAP_MUT.read()
でアクセスし、write する時は app_context::MAP_MUT.write()
でアクセスします。もし、他のスレッドが競合するロックを持っている時は、それがアンロックされるまで待たされます。
ポイズニングに注意
ここで、関数の戻り値が Result<(), String>
になっていることに注目してください。RwLock や Mutex は、ロック時に Err<_>
が返る可能性があります。これは、write ロックを持っているスレッドが panic した後に起こります。理由は、ロックで守られているデータの整合性が保たれているか、保証できなくなるためです。このような動作を ポイズニング(poisoning)(直訳すると「毒を盛る」)と呼びます。
一度、毒を盛られてしまうと、RwLock を作り直さないと復旧できません。しかし、lazy_static ですと、1度だけ初期化することを保証していますので、こういう動作は無理です。基本的には、write ロック中に panic しないように、細心の注意を払ってプログラミングするべきでしょう。それでも、どうしても復旧が必要な場合は、lazy_static の使用を諦めて、自前で実装することになると思います。(マルチスレッド配下での再初期化は、アプリケーションによって正しいやり方が変わってくるので、本記事の範囲外とします)
デッドロックに注意
もう一点注意があります。それは、デッドロックです。もし、ロックするリソースが複数あるなら、ロックする順序に気を配るべきです。さもなければ、デッドロックが起きて、リソースにアクセスしようとした全てのスレッドが、アンロック待ちで止まってしまうでしょう。
また、たとえシングルスレッドのプログラムでも注意が必要です。先ほどの read_write_app_map_mut()
関数は、read や write の単位で、ブロック { }
で囲まれていたことに気づいたでしょうか? これをもし外すと、シングルスレッドなのにデッドロックしてしまいます。
デッドロックする例
fn read_write_app_map_mut_bad() -> Result<(), String> {
// read
let m = try!(app_context::MAP_MUT.read().map_err(|e| e.to_string()));
assert_eq!("bar", *m.get(&0).unwrap_or(&NF));
// write -- ここでデッドロックする。
let mut m = try!(app_context::MAP_MUT.write().map_err(|e| e.to_string()));
m.insert(1, "baz");
Ok(())
}
このように書くと、read の時に m
に束縛した read ロックが、write ロックを取得する時にレキシカルスコープを抜けておらず、生きている状態になっています。read ロックが存在していると、write ロックは待たされますので、その時点でデッドロックするのです。
新たなロックを取得する前に、以前のロックがスコープを抜けて削除されていることを確認してください。
正しい例
fn read_write_app_map_mut() -> Result<(), String> {
// read
{
let m = try!(app_context::MAP_MUT.read().map_err(|e| e.to_string()));
assert_eq!("bar", *m.get(&0).unwrap_or(&NF));
// ここで m のスコープを抜けるので、read ロックが開放される。
// こうしないと次の write() でデッドロックする。
}
// write
{
let mut m = try!(app_context::MAP_MUT.write().map_err(|e| e.to_string()));
m.insert(1, "baz");
}
...
}
ふう。安全なプログラムを書くというのは、いろいろと面倒ですね。
しかし、こういう配慮が必要なことを Sync
トレイトや、Result<T, E>
という「型」を通して気づかせてくれるのは、素晴らしいことだと思いませんか? これがもしなかったら、実行時に、マルチスレッドに起因する、発見しづらく、再現しづらい問題のデバッグに、多くの時間を奪われてしまうかもしれません。
スレッドローカルなデータ
次はスレッドローカルなデータです。こちらは、標準ライブラリの LocalKey
というページ を見ると使い方が書いてありますし、static 変数のような制限はありません。そのため、注意点以外はあまり話すことはありません。
もちろん、イミュータブル(不変)なスレッドローカルデータも作れますが、あまり使うことがなさそうなので省略します。
ミュータブルなスレッドローカルデータを thread_local!()
マクロと RefCell で実現する
では、ミュータブル(可変)なスレッドローカルデータを作成しましょう。このデータは、単一のスレッド内からのみアクセスできますので、先ほどのマルチスレッドの時にあった Sync
トレイトは不要です。つまり、RwLock は不要です。
しかし、Rust には、シングルスレッドのプログラムでも RwLock と似たような排他制御がありますよね。所有権と借用です。
最初に、借用は全て所有者のスコープより長く存続してはなりません。次に、次の2種類の借用のどちらか1つを持つことはありますが、両方を同時に持つことはありません。
- リソースに対する1つ以上の参照(
&T
)- ただ1つのミュータブルな参照(
&mut T
)...
書込みを行わないのであれば、参照は好きな数だけ使うことができます。
&mut
は同時に1つしか持つことができないので、データ競合は起き得ません。これがRustがデータ競合をコンパイル時に回避する方法です。もしルールを破れば、そのときはエラーが出るでしょう。
さて、これは厄介なバグを防ぐ素晴らしい仕組みですが、スレッドローカルのように、プログラムの任意の場所からアクセスできるデータについては、コンパイル時の検査(borrow check)ができません。そこで、この borrow check を実行時に行うデータ型のひとつ RefCell
を使います。
mod thread_context {
use std::cell::RefCell;
use std::collections::HashMap;
thread_local!(pub static MAP_MUT: RefCell<HashMap<u32, &'static str>> = {
let mut m = HashMap::new();
m.insert(0, "bar");
RefCell::new(m)
});
}
このように、スレッドローカルを定義するには thread_local!()
マクロを使います。lazy_static と似た雰囲気で書けますね。実行時の borrow check を実現するために、HashMap を RefCell で包んでます。
使う側はこうなります。
fn main() {
...
read_write_thread_map_mut();
...
println!("Done");
}
fn read_write_thread_map_mut() {
thread_context::MAP_MUT.with(|m| {
// read
assert_eq!("bar", *m.borrow().get(&0).unwrap_or(&NF));
// write
m.borrow_mut().insert(1, "baz");
// read
assert_eq!("baz", *m.borrow().get(&1).unwrap_or(&NF));
})
}
まずスレッドローカルにアクセスするために、with()
とクロージャを使います。RefCell<HashMap<_, _>>
がクロージャの変数 m
に束縛されるので、read 時は borrow()
、write 時は borrow_mut()
で借用を取得します。
panic に注意
コンパイル時の borrow check では、借用関係の問題があるとコンパイルエラーにしてくれるので安心です。一方、RefCell などによる実行時の borrow check では、もし借用関係の問題があると、実行時エラー、つまり、panic を起こします。
例えば、以下の関数 read_write_thread_map_mut_bad()
は panic します。
panic する例
fn main() {
...
// catch_unwind() は Rust 1.9 から使用できる。
match std::panic::catch_unwind(|| {
read_write_thread_map_mut_bad()
}) {
Ok(()) => unreachable!(),
Err(e) => println!("Panicked. Error: {:?}",
e.downcast_ref::<String>()
.unwrap_or(&"Unknown error".to_owned())),
}
println!("Done");
}
fn read_write_thread_map_mut_bad() {
thread_context::MAP_MUT.with(|m| {
let m_ref = m.borrow();
assert_eq!("bar", *m_ref.get(&0).unwrap_or(&NF));
// m_ref のスコープ内なので、m.borrow() で作った借用が有効。
// ここで panic する。
m.borrow_mut().insert(1, "baz");
})
}
実行結果
% cargo run
Running `target/debug/app-global`
thread '<main>' panicked at 'RefCell<T> already borrowed', /buildslave/rust-buildbot/slave/stable-dist-rustc-cross-host-linux/build/src/libcore/cell.rs:444
note: Run with `RUST_BACKTRACE=1` for a backtrace.
Panicked. Error: "RefCell<T> already borrowed"
Done
期待通り、"RefCell<T> already borrowed"
(RefCell<T> はすでに借用されています)で panic しました。先ほどのデッドロックのケースと同様に、借用の寿命について十分な注意を払う必要があります。
おまけ:with()
を意識しないで使う
慣れの問題かもしれませんが、なんとなく、thread_context::MAP_MUT.with(|m| { 処理 } )
のように書くのは、変な感じがするのは私だけでしょうか? let m = thread_context::MAP_MUT
みたいに書けたらいいのですが、いまのままだと、それはできません。なぜなら、MAP_MUT
がデータの所有権を持っているからです。
たとえば、こんな風に書いて、借用を with()
の外側に持ってくることはできません。
let m = thread_context::MAP_MUT.with(|m| { &m });
以下のようなエラーになります。
cannot infer an appropriate lifetime for borrow expression due to conflicting requirements [E0495]
first, the lifetime cannot outlive the expression at 109:47...
...so that reference is valid at the time of borrow
クロージャを抜ける時に変数 m
の寿命が尽きるので、それに対する参照をキープできないのです。
しかし、これと似たことを実現する方法があります。それは、リファレンスカウントを行う Rc 型でラップすることです。
mod thread_context {
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
thread_local!(static MAP_RC_MUT: Rc<RefCell<HashMap<u32, &'static str>>> = {
let mut m = HashMap::new();
m.insert(0, "boo");
Rc::new(RefCell::new(m))
});
pub fn get_map_rc_mut() -> Rc<RefCell<HashMap<u32, &'static str>>> {
MAP_RC_MUT.with(|rc| rc.clone())
}
}
このように Rc 型でラップし、さらに、get_map_rc_mut()
という関数を用意しました。そこでは、with()
でスレッドローカルなデータにアクセスして、Rc を clone()
しています。
これで、使う側のコードでは、with()
を意識しなくてよくなりました。
fn main() {
...
read_write_thread_rc_map_mut();
println!("Done");
}
fn read_write_thread_rc_map_mut() {
let m = thread_context::get_map_rc_mut();
assert_eq!("boo", *m.borrow().get(&0).unwrap_or(&NF));
m.borrow_mut().insert(1, "hoo");
assert_eq!("hoo", *m.borrow().get(&1).unwrap_or(&NF));
}
まとめ
どうでしたか? ミュータブル なグローバル変数やスレッドローカルを扱うのは、ロックや実行時の borrow check に頼るしかなく、意外に面倒ですね。もし、rustc のコンパイルエラーに慣れていて、ありがたみがわかっていると、こういうことに自分で責任を持ってチェックするのは、面倒を通り越して不安にすら感じるかもしれません。
その一方で、グローバル変数やスレッドローカルを使うことで、大きなプログラムを書く時の柔軟性を大いに改善することができます。例えば、もし、グローバルであるべきデータを「関数の引数」を使ってたらい回ししていた場合、深い関数呼び出しのネストの底で更新しようとした時に、所有権やミュータブルな借用を持ってないことに気づいて困惑するかもしれません。
つまり、引数渡しでコンパイラの静的解析を活用するか、それとも、グローバル変数でコードの自由度を上げつつ実行時のチェックで妥協するかといったことは、トレードオフの関係にあります。この2つを適切に、バランスよく選択できるようになれば、一人前の Rust 使いといえるのではないでしょうか。
とはいえ、たとえ実行時のチェックでも、write()
や、borrow_mut()
を中心にソースコードを読めば、どこに気を配るべきかがわかるのですから、よく考えられた仕組みになっていると言えるでしょう。
本記事で学んだこと:
-
グローバルなデータ
- 現状は、lazy_static クレートを使うのが便利。
- ミュータブル(可変)にする場合、
RwLock
またはMutex
で守る必要がある。アンロックは暗黙的に行われるので、デッドロックに注意すること。 - また、更新時は、ポイズニングを起こさないように注意すること。
-
スレッドローカルなデータ
- 標準ライブラリの
thread_local!
マクロを使用する。 - ミュータブルにする場合、
RefCell
を使用する。借用のスコープに注意を払い、panic を避けること。 -
Rc
と組み合わせると、with()
の外側にデータを渡せるようになる。
- 標準ライブラリの
付録:完成したプログラム
panic::catch_unwind()
のために、Rust 1.9.0 か、それ以降が必要。
[dependencies]
lazy_static = "0.2.1"
#[macro_use]
extern crate lazy_static;
mod app_context {
use std::collections::HashMap;
use std::sync::RwLock;
lazy_static! {
pub static ref MAP: HashMap<u32, &'static str> = {
let mut m = HashMap::new();
m.insert(0, "foo");
m
};
pub static ref MAP_MUT: RwLock<HashMap<u32, &'static str>> = {
let mut m = HashMap::new();
m.insert(0, "bar");
RwLock::new(m)
};
}
}
mod thread_context {
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
thread_local!(pub static MAP_MUT: RefCell<HashMap<u32, &'static str>> = {
let mut m = HashMap::new();
m.insert(0, "bar");
RefCell::new(m)
});
thread_local!(static MAP_RC_MUT: Rc<RefCell<HashMap<u32, &'static str>>> = {
let mut m = HashMap::new();
m.insert(0, "boo");
Rc::new(RefCell::new(m))
});
pub fn get_map_rc_mut() -> Rc<RefCell<HashMap<u32, &'static str>>> {
MAP_RC_MUT.with(|rc| rc.clone())
}
}
const NF: &'static str = "not found";
fn main() {
read_app_map();
match read_write_app_map_mut() {
Ok(()) => (),
Err(e) => println!("Error {}", e),
}
read_write_thread_map_mut();
// catch_unwind() は Rust 1.9 から使用できる。
match std::panic::catch_unwind(|| {
read_write_thread_map_mut_bad()
}) {
Ok(()) => unreachable!(),
Err(e) => println!("Panicked. Error: {:?}",
e.downcast_ref::<String>()
.unwrap_or(&"Unknown error".to_owned())),
}
read_write_thread_rc_map_mut();
println!("Done");
}
fn read_app_map() {
let ref m = app_context::MAP;
assert_eq!("foo", *m.get(&0).unwrap_or(&NF));
assert_eq!(NF, *m.get(&1).unwrap_or(&NF));
}
fn read_write_app_map_mut() -> Result<(), String> {
// read
{
let m = try!(app_context::MAP_MUT.read().map_err(|e| e.to_string()));
assert_eq!("bar", *m.get(&0).unwrap_or(&NF));
// ここで m のスコープを抜けるので、read ロックが開放される。
// こうしないと次の write() でデッドロックする。
}
// write
{
let mut m = try!(app_context::MAP_MUT.write().map_err(|e| e.to_string()));
m.insert(1, "baz");
}
// read
{
let m = try!(app_context::MAP_MUT.read().map_err(|e| e.to_string()));
assert_eq!("baz", *m.get(&1).unwrap_or(&NF));
}
Ok(())
}
fn read_write_thread_map_mut() {
thread_context::MAP_MUT.with(|m| {
// read
assert_eq!("bar", *m.borrow().get(&0).unwrap_or(&NF));
// write
m.borrow_mut().insert(1, "baz");
// read
assert_eq!("baz", *m.borrow().get(&1).unwrap_or(&NF));
})
}
// borrow checkでpanicするケース
fn read_write_thread_map_mut_bad() {
thread_context::MAP_MUT.with(|m| {
let m_ref = m.borrow();
assert_eq!("bar", *m_ref.get(&0).unwrap_or(&NF));
// m_ref のスコープ内なので、m.borrow() で作った借用が有効。
// ここで panic する。
m.borrow_mut().insert(1, "baz");
})
}
// withとクロージャを意識しないで使いたい
fn read_write_thread_rc_map_mut() {
let m = thread_context::get_map_rc_mut();
assert_eq!("boo", *m.borrow().get(&0).unwrap_or(&NF));
m.borrow_mut().insert(1, "hoo");
assert_eq!("hoo", *m.borrow().get(&1).unwrap_or(&NF));
}