More than 1 year has passed since last update.
Rust 100 Ex 🏃【28/37】 リーク・スコープ付きスレッド ~ライフタイムに技あり!~
前の記事
- 【0】 準備 ← 初回
- ...
- 【27】 スレッド・'staticライフタイム ~並列処理に見るRustの恩恵~ ← 前回
- 【28】 リーク・スコープ付きスレッド ~ライフタイムに技あり!~ ← 今回
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参照を得られるので、ここまでの演習通り足し合わせてください
解説
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> がやってきています。その上で
- 新しいヒープを確保するな!
- メモリリークを使うな!
という条件が足され、本エクササイズテーマのスコープ付きスレッドを使わせたいという感じの問題になっています!
解説
スレッド付きスコープは、「スレッドがいつまで生きているかわからないから」 '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>>~
-
例外的に使用することがあるとすると例えば FFI が絡む時が挙げられます。Rustの所有権の枠組みだと不都合がある時にしばしば
std::mem::forgetを呼んだりします。 ↩
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme
