この記事は Rustその2 Advent Calendar 2016 20日目の記事としてかかれました。
環境と対象読者
この記事に含まれるコードは
rustc 1.13.0 (2c6933acc 2016-11-07)
の
Rust Playgroundまたはここで動くことを確認しました。
最適な対象読者としてはrustで並行処理するアプリケーションを
これから書きたいと思っている人です。
std::threadの罠
rustの標準ライブラリのstd::thread
を用いて並行処理を書きたい時に
例えばこういうような処理が例示されるかと思います。
use std::thread;
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
let y = 1i32;
let handle = thread::spawn(move ||{println!("{}",add_one(y))});
handle.join();
}
この処理は問題ないと思います。(少し誤魔化している部分はありますが)
関数をspawnしてやれば1足してプリントしてくれます。
しかし、現実の問題はより複雑で、上記のように常に1足して終わるような問題は少なく、
そのうちこんな風なより一般化された処理を書きたくなります。
use std::thread;
struct Adder {
x: i32
}
impl Adder {
pub fn new(x:i32) -> Adder {
Adder {x:x}
}
pub fn add_println(self,y:i32) {
let h = thread::spawn(move ||{println!("{}",y+self.x)});
h.join();
}
}
fn main() {
let y = 1i32;
let adder = Adder::new(2);
adder.add_println(y);
}
これで一般化されました。
threadを立ち上げて最初に与えたi32
型の整数を加算してくれます。
しかし、上記には少し問題があり、それは一行足すだけで顕在化します。
use std::thread;
struct Adder {
x: i32
}
impl Adder {
pub fn new(x:i32) -> Adder {
Adder {x:x}
}
pub fn add_println(self,y:i32) {
let h = thread::spawn(move ||{println!("{}",y+self.x)});
h.join();
}
}
fn main() {
let y = 1i32;
let adder = Adder::new(2);
adder.add_println(y);
adder.add_println(y);
}
こうする事でこのプログラムはこんな風に怒られます。
error[E0382]: use of moved value: `adder`
--> <anon>:31:5
|
30 | adder.add_println(y);
| ----- value moved here
31 | adder.add_println(y);
| ^^^^^ value used here after move
|
= note: move occurs because `adder` has type `Adder`, which does not implement the `Copy` trait
error: aborting due to previous error
なるほど(なるほど)。
つまりstruct自体をmoveセマンティクスで移動しちゃってるので
コンパイル時にlifetimeチェッカーで怒られてしまってるわけですね。
ここで浅知恵を働かせ、次のように書き換えてみます。
use std::thread;
struct Adder {
x: i32
}
impl Adder {
pub fn new(x:i32) -> Adder {
Adder {x:x}
}
pub fn add_println(&self,y:i32) {
let h = thread::spawn(move ||{println!("{}",y+self.x)});
h.join();
}
}
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
let y = 1i32;
let adder = Adder::new(2);
adder.add_println(y);
}
変更点はadd_println
メソッドの第一引数に渡していたself
を&self
としただけです。
つまり、add_println
メソッドにはstructの参照を持たせ、
それ自体はmoveされても問題ないようにしようと考えました。
勘のいい方は既にお気付きの通り、この目論見は崩れ去ります。
error[E0477]: the type `[closure@<anon>:13:31: 13:63 y:i32, self:&Adder]` does not fulfill the required lifetime
--> <anon>:13:17
|
13 | let h = thread::spawn(move ||{println!("{}",y+self.x)});
| ^^^^^^^^^^^^^
|
= note: type must outlive the static lifetime
error: aborting due to previous error
なぜか。それは spawnされたthreadは'static
なlifetimeを持つ からです。
一方でAdder
のインスタンスはここではmain
関数内に留まっています。
threadのlifetimeである'static
より狭い範囲のためやはり
lifetimeチェッカーによって弾かれてしまいます。
この要件があるためthread::spawn
する際はたいていの場合moveセマンティクスが必要になります。
上で少し誤魔化していると書いたのはこの部分です。
ここに来てプログラマはrustのスレッドはネイティブなスレッドであり、
rustはランタイムのない言語なんだったと言う事を改めて気付かされます。
(同時にC++でスレッドプログラミングをしたことのある方は
この制限はある種の懐かしさを感じるのではないでしょうか。)
crossbeam
前置きが長くなりましたがここで漸く本題です。
上のコードがエラーになるのは理屈としてはわかるんですが、
とは言えプログラマの心情的には納得できない部分もあります。
なぜならadd_println
メソッドの中でthreadはjoin()しているわけなので、
threadのlifetimeはそこまでとわかっているからです。
このようなケースで有効になるのがscoped threadです。
これはまあ名前の通りscopeを限定したthreadです
rustにおけるscoped threadは以前はstd内に入っていたようですが、
今はcrossbeamという外部crateを使うのが良いようです
自分のプロジェクトで使う場合はCargo.tomlに加えましょう。
ちなみにこちらのRust playgroundには追加済みのようです
extern crate crossbeam;
struct Adder {
x: i32,
}
impl Adder {
pub fn new(x: i32) -> Adder {
Adder { x: x }
}
pub fn add_println(&self, y: i32) {
crossbeam::scope(|scope| {
let h = scope.spawn(|| println!("{}", y + self.x));
h.join();
});
}
}
fn main() {
let y = 1i32;
let adder = Adder::new(2);
adder.add_println(y);
adder.add_println(y);
}
これでようやく意図した通りの並行処理が実現できました。文明ですね。
まとめ
rustのthreadはnativeなthreadなので
いろいろつらい部分もあるかもしれませんが、
crossbeamとか使ったら少し楽になるという話でした。
余談
現在rust用のTUIライブラリを書いています。
(まだまだbuggyなのでこれに依存したアプリケーションは書かないでください…)
今回のcrossbeamもこれの開発中で必要になりました。
もちろん、既にTUIの為のライブラリは存在していますし、
なぜ今更?と思われるかもしれませんが、
一番大きいのは既存のライブラリが複雑なUnicodeの仕様に
勝ててない感じというのがあります。
ここはeast asian widthな母語を持つ一人として
立ち上がろうと決心しました(勝てるとは言ってない)。
もちろん今回自分で書く上で既存のライブラリ群はとても参考にさせていただいています。
書いている上で様々なTipsがあり、学びが発生しましたので、
また機会があればそちらも紹介できればと思います