第70回です。前回はこちら。
[第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); }
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を開催した。
最後の課題、全然わからん...。
今週のプルリクエストはこちら。