kdnakt blog

hello there.

Rust dojo第70回を開催した

第70回です。前回はこちら。

kdnakt.hatenablog.com

[第70回の様子]

2023/03/15に第70回を開催した。

内容としてはRust By Example 日本語版20. 標準ライブラリのその他の「20.1. スレッド」、「20.1.1. Testcase: map-reduce」に取り組んだ。最後の演習問題を終えられなかったのが残念...。
参加者は自分を入れて5人。久しぶりに自分がドライバを担当した。

[学んだこと]

  • 20.1. スレッド
  • Rustはspawn()関数を使ってOSのネイティブスレッドを起動できる
    • 従ってスレッドのスケジューリング(実行順序の決定)はOSが行う
  • spawn()は引数にmoveクロージャをとる
    • 内部で利用する変数は参照ではなく、値を取る
use std::thread;
const NTHREADS: u32 = 10;
fn main() {
    // spawnの戻り値JoinHandleを保持するベクタ
    let mut children = vec![];

    for i in 0..NTHREADS {
        // 新しいスレッドを起動
        children.push(thread::spawn(move || {
            println!("this is thread number {}", i);
        }));
    }

    for child in children {
        // 子スレッドが終了するのを待つ
        let _ = child.join();
    }
}

// 実行結果の一例:OSがスケジューリングするので起動順に実行されるわけではない
this is thread number 0
this is thread number 4
this is thread number 6
this is thread number 5
this is thread number 3
this is thread number 2
this is thread number 8
this is thread number 1
this is thread number 9
this is thread number 7
  • 最近、Rustの並行処理についての本(Rust Atomics and Locks)を読んでいる
  • そこでは、JoinHandleを利用せずに、スコープを利用して子スレッドの終了を待つ方法が紹介されていた
use std::thread;
const NTHREADS: u32 = 10;
fn main() {
    thread::scope(|s| {
        for i in 0..NTHREADS {
            // スコープから新しいスレッドを起動
            s.spawn(move || {
                println!("this is thread number {}", i);
            });
        }    
    });
}
  • 20.1.1. Testcase: map-reduce
  • Rustはデータ処理の並列化を、並列化につきものの頭痛なしで簡単に実現できる
  • Rustには所有権があるので、データ競合を自動的に防いでくれる
  • あるデータについて、1つだけ書き込み可能な参照を持つか、あるいは複数の読み込み可能な参照を持つか、というエイリアス・ルールのおかげで、複数スレッド間の操作がなくなる
    • 同期が必要な場合はMutexやChannelを使う
  • 以下では、'staticなライフタイムの参照を用いて、スレッド間のデータをやり取りする
    • 非staticなデータの場合はArcのようなスマートポインタを利用できる
use std::thread;
fn main() {

    // このデータを改行を含む空白文字で分割してスレッドで合計する
    let data = "86967897737416471853297327050364959
11861322575564723963297542624962850
70856234701860851907960690014725639
38397966707106094172783238747669219
52380795257888236525459303330302837
58495327135744041048897885734297812
69920216438980873548808413720956532
16278424637452589860345374828574668";

    // 子スレッドの結果を保持するベクタ
    let mut children = vec![];

    // マップフェーズ
    let chunked_data = data.split_whitespace();
    for (i, data_segment) in chunked_data.enumerate() {
        println!("data segment {} is \"{}\"", i, data_segment);

        // スレッドを起動する
        children.push(thread::spawn(move || -> u32 {
            let result = data_segment
                        .chars()
                        // 1文字ずつ10進数の数値に変換
                        .map(|c| c.to_digit(10).expect("should be a digit"))
                        .sum();
            println!("processed segment {}, result={}", i, result);
            result
        }));
    }


    // リデュースフェーズ
    let final_result = children.into_iter().map(|c| c.join().unwrap()).sum::<u32>();

    println!("Final sum result: {}", final_result);
}
  • spawn()にmoveキーワードをつけずにビルドすると、以下のようにコンパイルエラーとなる
    • 変数iを所有するクロージャが、main関数の範囲を超えて生き残る点に問題があるらしい
error[E0373]: closure may outlive the current function, but it borrows `i`, which is owned by the current function
  --> src/main.rs:55:37
   |
55 |         children.push(thread::spawn(|| -> u32 {
   |                                     ^^^^^^^^^ may outlive borrowed value `i`
...
66 |             println!("processed segment {}, result={}", i, result);
   |                                                         - `i` is borrowed here
   |
note: function requires argument type to outlive `'static`
  --> src/main.rs:55:23
   |
55 |           children.push(thread::spawn(|| -> u32 {
   |  _______________________^
56 | |             // Calculate the intermediate sum of this segment:
57 | |             let result = data_segment
58 | |                         // iterate over the characters of our segment..
...  |
71 | |
72 | |         }));
   | |__________^
help: to force the closure to take ownership of `i` (and any other referenced variables), use the `move` keyword
   |
55 |         children.push(thread::spawn(move || -> u32 {
   |                                     ++++
  • ちなみに、リデュース部分のターボフィッシュ構文は以下のように書き換えることもできる
// ターボフィッシュ
let final_result = children.into_iter()
    .map(|c| c.join().unwrap())
    .sum::<u32>();

// 変数の型宣言
let final_result: u32 = children.into_iter()
    .map(|c| c.join().unwrap())
    .sum();
  • 最後に、課題が提示されていた
    • ユーザーの入力を受け取ってデータを処理する場合、大量のホワイトスペースが含まれていたとしたら、それだけのスレッドを起動するのは無駄である
    • 従って、スレッドの上限値を設定してほしい...という課題だ。
    • 残念ながら勉強会の時間が足りず、この課題は次週に持ち越しとなった。

[まとめ]

モブプログラミングスタイルでRust dojoを開催した。
最後の課題、全然わからん...。

今週のプルリクエストはこちら。

github.com