2
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

Rust 100 Ex 🏃【28/37】 リーク・スコープ付きスレッド ~ライフタイムに技あり!~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第28回になります!

今回の関連ページ

[07_threads/03_leak] 意図的なメモリリーク

問題はこちらです。同じネタが続いてます。

lib.rs
// 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 参照を得られるので、ここまでの演習通り足し合わせてください
テストを含めた全体
lib.rs
// 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] を引き出せるので、これを使って後は前回と同様の処理です!

lib.rs
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 トレイト処理を動かさなければならない場合

第8回の注釈で free を呼ぶべきかみたいな話を地味にしていたのですが、筆者としてはすぐ終わるようなタイプの処理なら全然書いてもいいんじゃないかと思っています。

まぁ今回の演習は「リークさせる手もあるよ!」ということの紹介程度の内容でしょう。やって良いとは言え、明示的にリークさせることは少なく1、大体は最初のほうで取り組んだ通りクローンしてしまったり、以降で紹介されるようなパターンや手法を使うことの方が多いと思います。

[07_threads/04_scoped_threads] スコープ付きスレッド

問題はこちらです。

lib.rs
// 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> がやってきています。その上で

  • 新しいヒープを確保するな!
  • メモリリークを使うな!

という条件が足され、本エクササイズテーマのスコープ付きスレッドを使わせたいという感じの問題になっています!

テストを含めた全体
lib.rs
// 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 ライフタイムにする必要があったなら、「スレッドの生存範囲をそもそも狭めてやればよい」という解決策です!

lib.rs
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上のデータが喪失する可能性を鑑みて、一応記事にもソースコードを掲載することとしました。

URL: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=d3665b648c9d09ad643221e1b4a6acf0

Rust
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` が呼ばれるか呼ばれないか挙動が変化
}
  1. 例外的に使用することがあるとすると例えば FFI が絡む時が挙げられます。Rustの所有権の枠組みだと不都合がある時にしばしば std::mem::forget を呼んだりします。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0