LoginSignup
28
11

More than 5 years have passed since last update.

RustでScoped Threadを用いた並行処理

Posted at

この記事は 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があり、学びが発生しましたので、
また機会があればそちらも紹介できればと思います

28
11
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
28
11