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