前の記事
- 【0】 準備 ← 初回
- ...
- 【27】 スレッド・'staticライフタイム ~並列処理に見るRustの恩恵~ ← 前回
- 【28】 リーク・スコープ付きスレッド ~ライフタイムに技あり!~ ← 今回
全記事一覧
- 【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 演習第28回になります!
今回の関連ページ
[07_threads/03_leak] 意図的なメモリリーク
問題はこちらです。同じネタが続いてます。
// TODO: Given a vector of integers, leak its heap allocation.
// Then split the resulting static slice into two halves and
// sum each half in a separate thread.
// Hint: check out `Vec::leak`.
use std::thread;
pub fn sum(v: Vec<i32>) -> i32 {
todo!()
}
TODOを要約すると以下のような感じです。
- 整数のベクトルが与えられるので、そのヒープアロケーションをメモリリークさせちゃってください!
- そうすれば
&'static
参照を得られるので、ここまでの演習通り足し合わせてください
テストを含めた全体
// TODO: Given a vector of integers, leak its heap allocation.
// Then split the resulting static slice into two halves and
// sum each half in a separate thread.
// Hint: check out `Vec::leak`.
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);
}
}
解説
leak
メソッドを使うことで、 &'static mut [T]
を引き出せるので、これを使って後は前回と同様の処理です!
use std::thread;
pub fn sum(v: Vec<i32>) -> i32 {
let slice: &'static mut [i32] = v.leak();
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()
}
仕組みとしては、ベクトルが参照しているヒープ先を所有権の仕組みから外し、ベクトルのライフタイムが尽きた後でも片付けられないようにしています。つまり、プログラム終了までメモリが有効に残り続ける「メモリリーク」を意図的に起こさせています!
「メモリリーク?!なんだか危なそう...」と思った方、メモリリークは"基本的には"全く問題なく安全です。なぜなら、Bookにも書いてある通り、「リークさせたメモリもプロセス終了後にOSが勝手に解放してくれるから」です。
メモリリークが問題になるとして、考えられるのは次の2点ぐらいです。
-
常駐アプリケーション(Webサーバーとか)を動かす場合で、リクエスト毎などに確保されたリソースをリークしてしまう場合
- メモリリークが問題になる典型例です
- 手動実装の
Drop
トレイト処理を動かさなければならない場合-
drop
メソッドが呼ばれなくなります。(おそらく、呼ぶとしてどの順番で何を呼べば良いの?という問題があるため) - Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=d3665b648c9d09ad643221e1b4a6acf0
-
第8回の注釈で free
を呼ぶべきかみたいな話を地味にしていたのですが、筆者としてはすぐ終わるようなタイプの処理なら全然書いてもいいんじゃないかと思っています。
まぁ今回の演習は「リークさせる手もあるよ!」ということの紹介程度の内容でしょう。やって良いとは言え、明示的にリークさせることは少なく1、大体は最初のほうで取り組んだ通りクローンしてしまったり、以降で紹介されるようなパターンや手法を使うことの方が多いと思います。
[07_threads/04_scoped_threads] スコープ付きスレッド
問題はこちらです。
// TODO: Given a vector of integers, split it in two halves
// and compute the sum of each half in a separate thread.
// Don't perform any heap allocation. Don't leak any memory.
pub fn sum(v: Vec<i32>) -> i32 {
todo!()
}
&'static [i32]
がやってきてくれるわけではなく最初の問題と同様に Vec<i32>
がやってきています。その上で
- 新しいヒープを確保するな!
- メモリリークを使うな!
という条件が足され、本エクササイズテーマのスコープ付きスレッドを使わせたいという感じの問題になっています!
テストを含めた全体
// TODO: Given a vector of integers, split it in two halves
// and compute the sum of each half in a separate thread.
// Don't perform any heap allocation. Don't leak any memory.
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);
}
}
解説
スレッド付きスコープは、「スレッドがいつまで生きているかわからないから」 'static
ライフタイムにする必要があったなら、「スレッドの生存範囲をそもそも狭めてやればよい」という解決策です!
use std::thread;
pub fn sum(v: Vec<i32>) -> i32 {
let mid = v.len() / 2;
thread::scope(|scope| {
// (&v[..mid]).into_iter()...とする必要はないらしい
let h1 = scope.spawn(|| v[..mid].into_iter().sum::<i32>());
let h2 = scope.spawn(|| v[mid..].into_iter().sum::<i32>());
h1.join().unwrap() + h2.join().unwrap()
})
}
scope.spawn
で生成されたスレッドは v
のライフタイムより短いライフタイムであることが確約されているため、 v
の参照を渡すことができるようになっています。スコープ付きならば、今まではできなかった「別スレッドへ 'static
じゃない参照を渡す」ということが可能になるわけです!
ところで、スレッド付きスレッドは scope
というローカル変数を用いて生成されている部分が興味深いですね。もしかしたら thread::scope(|scope| {...})
の中で thread::spawn
を呼び出す感じの仕組みにしようと思えばできたのかもしれませんが、あえて scope
という変数を導入することで、この変数のライフタイムより長いライフタイムを持つ変数ならスレッド内で扱える というわかりやすい判断基準が導入されています!要は読みやすいのです。Rustのライフタイムに慣れている人にとって親切な設計となっており、ライフタイムを上手く活用しているのが面白いなと感じました。
では次の問題に行きましょう!
次の記事: 【29】 チャネル・参照の内部可変性 ~Rustの虎の子、mpscと Rc<RefCell<T>>
~
登場したPlayground
(実際に無効化したことはないですが、)Rust Playground上のデータが喪失する可能性を鑑みて、一応記事にもソースコードを掲載することとしました。
struct Hoge(i32);
impl Drop for Hoge {
fn drop(&mut self) {
println!("Drop!: {}", self.0);
}
}
fn main() {
let v = vec![Hoge(1), Hoge(2), Hoge(3)];
v.leak(); // この行の有無で `drop` が呼ばれるか呼ばれないか挙動が変化
}
-
例外的に使用することがあるとすると例えば FFI が絡む時が挙げられます。Rustの所有権の枠組みだと不都合がある時にしばしば
std::mem::forget
を呼んだりします。 ↩