前の記事
- 【0】 準備 ← 初回
- ...
- 【26】 HashMap・順序・BTreeMap ~Rustの辞書型~ ← 前回
- 【27】 スレッド・'staticライフタイム ~並列処理に見るRustの恩恵~ ← 今回
全記事一覧
- 【0】 準備
- 【1】 構文・整数・変数
- 【2】 if・パニック・演習
- 【3】 可変・ループ・オーバーフロー
- 【4】 キャスト・構造体 (たまにUFCS)
- 【5】 バリデーション・モジュールの公開範囲 ~ → カプセル化!~
- 【6】 カプセル化の続きと所有権とセッター ~そして不変参照と可変参照!~
- 【7】 スタック・ヒープと参照のサイズ ~メモリの話~
- 【8】 デストラクタ(変数の終わり)・トレイト ~終わりと始まり~
- 【9】 Orphan rule (孤児ルール)・演算子オーバーロード・derive ~Empowerment 💪 ~
- 【10】 トレイト境界・文字列・Derefトレイト ~トレイトのアレコレ~
- 【11】 Sized トレイト・From トレイト・関連型 ~おもしろトレイトと関連型~
- 【12】 Clone・Copy・Dropトレイト ~覚えるべき主要トレイトたち~
- 【13】 トレイトまとめ・列挙型・match式 ~最強のトレイトの次は、最強の列挙型~
- 【14】 フィールド付き列挙型とOption型 ~チョクワガタ~
- 【15】 Result型 ~Rust流エラーハンドリング術~
- 【16】 Errorトレイトと外部クレート ~依存はCargo.tomlに全部お任せ!~
- 【17】 thiserror・TryFrom ~トレイトもResultも自由自在!~
- 【18】 Errorのネスト・慣例的な書き方 ~Rustらしさの目醒め~
- 【19】 配列・動的配列 ~スタックが使われる配列と、ヒープに保存できる動的配列~
- 【20】 動的配列のリサイズ・イテレータ ~またまたトレイト登場!~
- 【21】 イテレータ・ライフタイム ~ライフタイム注釈ようやく登場!~
- 【22】 コンビネータ・RPIT ~ 「
Iterator
トレイトを実装してるやつ」~ - 【23】
impl Trait
・スライス ~配列の欠片~ - 【24】 可変スライス・下書き構造体 ~構造体で状態表現~
- 【25】 インデックス・可変インデックス ~インデックスもトレイト!~
- 【26】 HashMap・順序・BTreeMap ~Rustの辞書型~
- 【27】 スレッド・'staticライフタイム ~並列処理に見るRustの恩恵~
- 【28】 リーク・スコープ付きスレッド ~ライフタイムに技あり!~
- 【29】 チャネル・参照の内部可変性 ~Rustの虎の子、mpscと
Rc<RefCell<T>>
~ - 【30】 双方向通信・リファクタリング ~返信用封筒を入れよう!~
- 【31】 上限付きチャネル・PATCH機能 ~パンクしないように制御!~
- 【32】
Send
・排他的ロック(Mutex
)・非対称排他的ロック(RwLock
) ~真打Arc<Mutex<T>>
登場~ - 【33】 チャネルなしで実装・Syncの話 ~考察回です~
- 【34】
async fn
・非同期タスク生成 ~Rustの非同期入門~ - 【35】 非同期ランタイム・Futureトレイト ~非同期のお作法~
- 【36】 ブロッキング・非同期用の実装・キャンセル ~ラストスパート!~
- 【37】 Axumでクラサバ! ~最終回~
- 【おまけ1】 Rustで勘違いしていたこと3選 🏄🌴 【100 Exercises To Learn Rust 🦀 完走記事 🏃】
- 【おまけ2】 【🙇 懺悔 🙇】Qiitanグッズ欲しさに1日に33記事投稿した話またはQiita CLIとcargo scriptを布教する的な何か
100 Exercise To Learn Rust 演習第27回になります、今回から新章7章、スレッドの話題です!
今回の関連ページ
[07_threads/01_threads] スレッド(並列処理)
問題はこちらです。
// TODO: implement a multi-threaded version of the `sum` function
// using `spawn` and `join`.
// Given a vector of integers, split the vector into two halves and
// sum each half in a separate thread.
// Caveat: We can't test *how* the function is implemented,
// we can only verify that it produces the correct result.
// You _could_ pass this test by just returning `v.iter().sum()`,
// but that would defeat the purpose of the exercise.
//
// Hint: you won't be able to get the spawned threads to _borrow_
// slices of the vector directly. You'll need to allocate new
// vectors for each half of the original vector. We'll see why
// this is necessary in the next exercise.
use std::thread;
pub fn sum(v: Vec<i32>) -> i32 {
todo!()
}
TODO部分を要約すると次のような感じです。
-
TODO:
thread::spanw
とJoinHandle::join
を使用する形でsum
関数のマルチスレッド版を作成してください -
整数のベクタが渡されるので、これらを2つに分けて合計を計算した後、その2つを足し合わせるようにします
-
警告: 調べるすべがないのでスレッド使わないで普通に足し合わせてもパスできるけど、それだと演習の意味がないからやめてね
-
ヒント: ベクトルをスライスにして参照をスレッドに渡すことはできません。この件については次の問題(本記事の2問目)の範囲です
- (とりあえず、)新しいベクトルを作成しましょう
テストを含めた全体
// TODO: implement a multi-threaded version of the `sum` function
// using `spawn` and `join`.
// Given a vector of integers, split the vector into two halves and
// sum each half in a separate thread.
// Caveat: We can't test *how* the function is implemented,
// we can only verify that it produces the correct result.
// You _could_ pass this test by just returning `v.iter().sum()`,
// but that would defeat the purpose of the exercise.
//
// Hint: you won't be able to get the spawned threads to _borrow_
// slices of the vector directly. You'll need to allocate new
// vectors for each half of the original vector. We'll see why
// this is necessary in the next exercise.
use std::thread;
pub fn sum(v: Vec<i32>) -> i32 {
todo!()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty() {
assert_eq!(sum(vec![]), 0);
}
#[test]
fn one() {
assert_eq!(sum(vec![1]), 1);
}
#[test]
fn five() {
assert_eq!(sum(vec![1, 2, 3, 4, 5]), 15);
}
#[test]
fn nine() {
assert_eq!(sum(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]), 45);
}
#[test]
fn ten() {
assert_eq!(sum(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), 55);
}
}
「2つのスレッドを建てて合計計算を並行処理して最後に足し合わせて」という問題ですね。
解説
thread::spawn
にてスレッドを生成し、 t1.join()
でスレッド内の処理が完了するのを待ち、最後にスレッドが返してきた値を足し合わせます。
pub fn sum(v: Vec<i32>) -> i32 {
let (v1, v2) = v.split_at(v.len() / 2);
let t1 = thread::spawn(|| v1.iter().sum::<i32>());
let t2 = thread::spawn(|| v2.iter().sum::<i32>());
let res1 = t1.join().unwrap();
let res2 = t2.join().unwrap();
res1 + res2
}
スライスの split_at
等を使って配列を分割すると、元の配列への参照になってしまいコンパイルエラーになります。
Compiling threads v0.1.0 (/path/to/100-exercises-to-learn-rust/exercises/07_threads/01_threads)
error[E0597]: `v` does not live long enough
--> exercises/07_threads/01_threads/src/lib.rs:104:20
|
99 | pub fn sum(v: Vec<i32>) -> i32 {
| - binding `v` declared here
...
104 | let (v1, v2) = v.split_at(v.len() / 2);
| ^ borrowed value does not live long enough
105 |
106 | let t1 = thread::spawn(|| v1.iter().sum::<i32>());
| ---------------------------------------- argument requires that `v` is borrowed for `'static`
...
113 | }
| - `v` dropped here while still borrowed
For more information about this error, try `rustc --explain E0597`.
error: could not compile `threads` (lib test) due to 1 previous error
* The terminal process "cargo 'test', '--package', 'threads', '--lib', '--', 'tests', '--show-output'" failed to launch (exit code: 101).
* Terminal will be reused by tasks, press any key to close it.
祈手「あの方は今はいない...層を跨ぐやり方は不都合が出やすい」
スレッドもそして次章8章の非同期処理にも登場する基本的な考えの一つとして、「参照類の 'static
ライフタイムではない値はそのままだとスレッドを跨げない」というものがあります。次の問題の解説で改めて紹介したいと思います。
とりあえず、新しく所有権を握れる構造体を作り、参照ではなく実体を渡すようにしてこの問題を回避します。 split_off
メソッド や drain
メソッド を使うといい感じに書けます。
pub fn sum(mut v: Vec<i32>) -> i32 {
// let v2 = v.split_off(v.len() / 2);
// Clone トレイトを要求しないバージョン↓
let v2: Vec<i32> = v.drain((v.len() / 2)..).collect();
let t1 = thread::spawn(move || v.iter().sum::<i32>());
let t2 = thread::spawn(move || v2.iter().sum::<i32>());
let res1 = t1.join().unwrap();
let res2 = t2.join().unwrap();
res1 + res2
}
地味に move
キーワードが登場していることにも注目。クロージャはデフォルトだと変数をなるべく「参照で」キャプチャしてこようとする(この参照は当然 'static
ではない)ので、「実体を」キャプチャさせるようにするためにつけるキーワードです。7章8章ではクロージャに基本的に付与することになるでしょう。
余談、ちょっとカッコつけたDRYな感じの回答です。全部イテレータでおk!(行数増えてる気がするのは気の所為)
pub fn _sum(mut v: Vec<i32>) -> i32 {
let v2 = v.drain((v.len() / 2)..).collect();
vec![v, v2]
.into_iter()
.map(|v| thread::spawn(move || v.iter().sum::<i32>()))
.collect::<Vec<_>>()
.into_iter()
.map(|handle| handle.join().unwrap())
.sum()
}
どちらかと言えばイテレータにまつわる注意点ですが、一旦途中で .collect::<Vec<_>>()
を挟んで、イテレータ内の全ての thread::spawn
が実行された上で後の .map(|handle| handle.join().unwrap())
が呼ばれるようにしなければなりません。
もし次のように書いたとしても、
use std::thread;
pub fn sum(mut v: Vec<i32>) -> i32 {
let v2 = v.drain((v.len() / 2)..).collect();
vec![v, v2]
.into_iter()
.map(|v| thread::spawn(move || v.iter().sum::<i32>()))
.map(|handle| handle.join().unwrap())
.sum()
}
これだとイテレータの特性より thread::spawn
後にすぐそのハンドルについて handle.join()
が呼ばれてしまい、直列に実行されてしまいます。気をつけましょう! こんな書き方する人自体が少数派かもですが...
[07_threads/02_static] 'static ライフタイム
問題はこちらです。
// TODO: Given a static slice of integers, split the slice into two halves and
// sum each half in a separate thread.
// Do not allocate any additional memory!
use std::thread;
pub fn sum(slice: &'static [i32]) -> i32 {
todo!()
}
さっきの問題では引数として Vec<i32>
実体が渡されましたが、今回は「staticな実体への参照(これもまたstatic)」を引数として受け取り、同様に分割して足し合わせる並列処理を書く問題です。
今回は先ほどとは異なり 新しいメモリアロケーションが起きないようにせよ という制約が加えられています。つまり、先程の回答の使い回しはしないでねということです。
テストを含めた全体
// TODO: Given a static slice of integers, split the slice into two halves and
// sum each half in a separate thread.
// Do not allocate any additional memory!
use std::thread;
pub fn sum(slice: &'static [i32]) -> i32 {
todo!()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty() {
static ARRAY: [i32; 0] = [];
assert_eq!(sum(&ARRAY), 0);
}
#[test]
fn one() {
static ARRAY: [i32; 1] = [1];
assert_eq!(sum(&ARRAY), 1);
}
#[test]
fn five() {
static ARRAY: [i32; 5] = [1, 2, 3, 4, 5];
assert_eq!(sum(&ARRAY), 15);
}
#[test]
fn nine() {
static ARRAY: [i32; 9] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
assert_eq!(sum(&ARRAY), 45);
}
#[test]
fn ten() {
static ARRAY: [i32; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
assert_eq!(sum(&ARRAY), 55);
}
}
解説
今度は逆に split_at
を使い、ヒープへのメモリアロケーションをさせない回答になります!
use std::thread;
pub fn sum(slice: &'static [i32]) -> i32 {
let (s1, s2) = slice.split_at(slice.len() / 2);
vec![s1, s2]
.into_iter()
.map(|s| thread::spawn(move || s.iter().sum::<i32>()))
.collect::<Vec<_>>()
.into_iter()
.map(|handle| handle.join().unwrap())
.sum()
}
split_at
メソッド返り値の (&[T], &[T])
のライフタイムは、引数 &self
のライフタイムと同じになるので、今回は各スレッドに 'static
ライフタイムな値が行き、よってコンパイルエラーにはならず合計を計算できます!
そういえば 'static
ライフタイムでない値だとスレッドを跨げない理由の解説がまだでした。 (次回以降扱うスコープ付きスレッド等を除き) ずばり、 スレッドそれ自体は「いつまで存在しているか」わからないため です。いつ join
が呼ばれるかはわからないので、スレッドがいつまで生きているかはわかりません! 'static
ではないと次の図のような問題が起きる可能性があります。
スレッド間でライフタイムを共有できるような術1を特に使わない場合、 'static
ライフタイムな値しかクロージャに含められないという制約を置くことでとりあえずスレッド間の値の受け渡しを問題ないものとしているわけです。ライフタイムといい感じに付き合いながらスレッドを扱う術は次回以降の話題です!
「'static
ライフタイムは実体と参照で異なる意味を持つ」のがややこしい点ですが、これについて Book の解説が今回も秀逸なので最後の方を是非読んでみてほしいです。
すなわち
'static
ライフタイムは次のように捉えることができます:
- 所有権ごと値をください!
- プログラムが動いている間ずっと存在する参照(
&'static T
)をください!
実体と参照で意味が異なるとは言いますが、「ずっと存在『できる』のが 'static
ライフタイム」と捉えると上記2つの捉え方をカバーできて直感的なんじゃないかと思います。
「ずっとは存在できない」ケースは、言い換えると「(いつ無効になるかわからない)他のリソースへの依存を内包している」ということであり、 'static
じゃない構造体(この後登場する MutexGuard<'_, T>
とか)は大体このパターンです。参照それ自体なら「参照先はプログラムが動いている間ずっと存在」していれば存在できます。そして通常の構造体は「他のリソースを参照していない」か「依存している他のリソースもずっと存在」していれば存在できます。この辺を総括した名前として見ると、「 'static
ライフタイム」という名称は結構しっくり来るのではないでしょうか?
ところで、Rustが難しいから並列処理においてこんなライフタイムの話をしなければならないのでしょうか...?筆者は逆だと考えています。「並列処理自体が本来リソースのライフタイムにまで配慮するべき」なのです。他の言語でこの辺の話を曖昧にして処理を書くと「ガッ!」「ガッ!」「ガッ!」の連続になることは想像に難くないです。 Rustはライフタイムというツールのお陰で並列処理で気をつけるべき点を簡単に説明できているというわけです!故に、個人的には、 並列処理や非同期処理の導入はRustの学習コストが他言語のそれと逆転する瞬間 なんじゃないかと思っています!逆に他言語話者はこの辺どうしてるんだ...
static
周りの話題としてタイムリーだったので取り上げたいと思います。 LazyCell
・ LazyLock
は先日出た Rust 1.80.0 にて安定化した機能で、共に「初めて参照された時にクロージャの中身を実行し、得られた値の参照を返す」という構造体です!
特に LazyLock
の方は static
を用いた変数宣言と共に用いられることで「最初から必要というわけではないけど、初めて参照された際に初期化されてくれるとありがたい」リソースや、「(コンパイル時には計算できないものの、)プログラム全体を通して有効な値とその参照を static
に持ち続けたい」というようなケースで特に効率化に寄与します!(やりたいことの説明としてはこの記事様がわかりやすいです)
一番わかりやすい例だと正規表現構造体 Regex
の生成処理を一度で済ませる、等でしょうか。
use std::sync::LazyLock;
use regex::Regex;
static MAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"^[^\s@]+@[^\s@]+\.[^\s@\.]+$"#).unwrap()
});
fn main() {
assert!(MAIL_REGEX.is_match("hoge+fuga@gmail.com"));
assert!(!MAIL_REGEX.is_match("Invalid Mail Address."));
}
LazyCell
と LazyLock
の違いはスレッドセーフかどうかで、本章以降でこの辺も詳しく解説されますが、スレッドを跨いで使用する場合は Sync
が実装された LazyLock
の方を使います。また今回例を作ってわかりましたが、 static
で使いたい場合スレッドを跨がさる2ので同様の理由で LazyLock
を使う必要があるみたいです!
スレッド関連初回ということで結構話しすぎました...では次の問題に行きましょう!
次の記事: 【28】 リーク・スコープ付きスレッド ~ライフタイムに技あり!~