リーク

所有権に基づいたリソース管理は、構成を単純にすることを意図しています。 オブジェクトを生成すると、リソースを獲得します。そして、オブジェクトが 破棄されるとリソースを解放します。オブジェクトは自動的に破棄されるので、 リソースは必ず解放されますし、できるだけ早く解放されるのです! 明らかにこれは完全で、我々の全ての問題は解決します。

実際にはこれは最悪で、我々には新しくそして風変わりな問題が与えられるのです。

多くの人々は、 Rust はリソースのリークを取り除いたと信じることが好きです。 実際、これは基本的には正しいです。安全な Rust のプログラムが、制御不能なやり方で、 リソースをリークしたら驚くでしょう。

しかしながら理論的な観点から見れば、どのように見ても、これは全く真実ではありません。 最も厳密な意味では、 "リーク" は非常に抽象的で、防げるものではありません。 プログラムの始めにコレクションを初期化し、デストラクタと共に沢山のオブジェクトで いっぱいにし、そしてこのコレクションを絶対に参照しない無限イベントループに 突入することは極めて些細なことです。コレクションはプログラムが終わるまで、 貴重な資源を保持し続けたまま、使われず無駄に浪費し続けます (そして OS によって 結局全ての資源は返還されますが) 。

より厳密なリークの形式を考えたほうが良いかもしれません。到達できない値を ドロップし損ねることです。 Rust はこれも防ぐことが出来ません。実際 Rust には、 これを行なう関数がありますmem::forget です。この関数は渡された値を消費し、 そしてその値のデストラクタを実行しません

過去に mem::forget は、リントにおいてアンセーフとしてマークされていました。 デストラクタを呼ばないことは、通常行儀の良い方法ではないからです (いくつかの 特別なアンセーフのコードにおいては便利ですが) 。 しかしこれは、一般的に次の意見に対して擁護できない考えだと決定されました。 すなわち、 安全なコードでデストラクタを呼び損ねる方法が沢山存在するのです。 最も有名な例は、内部可変性を使用した、参照カウント方式のポインタの循環を生成 することです。

安全なコードが、デストラクタのリークが起こらないと見なすことは理に適っています。 いかなるプログラムにおいても、デストラクタをリークするようなものは大体間違っていますから。 しかし、アンセーフなコードは、デストラクタがきちんと安全に実行されると信用できません。 ほとんどの型にとっては、これは問題ではありません。もしデストラクタをリークしたら、 当然その型へはアクセス不可能となります。ですからこれは問題ではありません。 そうですよね? 例えば、 Box<u8> をリークしても、いくらかのメモリを無駄にはしますが、 メモリ安全性はほとんど侵害することがないでしょう。

しかし、デストラクタのリークに対して注意深くならなければいけない場合は、プロキシ型です。 これらは、なんとかして異なったオブジェクトにアクセスするものの、そのオブジェクトを 実際には所有しない型です。プロキシオブジェクトは極めて稀です。気を付ける必要のある プロキシオブジェクトに至っては殊更稀です。しかし、ここでは標準ライブラリにある 3 つの 興味深い例について着目していきます。

  • vec::Drain
  • Rc
  • thread::scoped::JoinGuard

Drain

drain は、コンテナを消費せずにコンテナからデータをムーブする、 コレクションの API です。これによって、 Vec の全ての内容の所有権を獲得した後に、 Vec の アロケーションを再利用することが出来ます。 drain は Vec の内容を値で返すイテレータ (Drain) を 生成します。

では、 Drain がイテレーションの真っ最中であるとしましょう。ムーブされた値もあれば、 まだの値もあります。つまりこれは、 Vec の一部のデータが今、論理的には未初期化のデータで 埋まっていることを意味します! 値を削除する度に Vec の要素をずらすことも出来たでしょう。 けれどもこれは結果的に、パフォーマンスをひどく落とすことになるでしょう。

その代わりに Drain がドロップする時に、 Vecの背後にあるストレージを 修正するようにしたいと思います。 Drain を使い終わったら、削除されなかった要素を ずらし (Drain は副範囲をサポートしています) 、そして Vec の len を修正します。 巻き戻し安全でもあります! 安心!

それでは以下の例を考えてみましょう。

let mut vec = vec![Box::new(0); 4];

{
    // ドレインを開始します。 vec にはもうアクセスできません
    let mut drainer = vec.drain(..);

    // 2 つの値を引き出し、即座にドロップします
    drainer.next();
    drainer.next();

    // drainer を取り除きますが、デストラクタは呼び出しません
    mem::forget(drainer);
}

// しまった、 vec[0] はドロップされていたんだった、解放されたメモリを読み出そうとしているぞ!
println!("{}", vec[0]);

これは本当に明らかに良くないです。残念ながら、ある種の板挟みになっています。 すなわち、毎回のステップで一貫性のある状態を維持することは、膨大なコストが 発生するのです (そして API のあらゆる利点を消してしまうでしょう) 。 一貫性のある状態を維持できないことで、安全なコードで未定義動作を起こしてしまいます (これにより API が 不健全となってしまいます) 。

ではどうすればいいのでしょうか? うーん、自明に一貫性のある状態を選択することが出来ます。 すなわち、イテレーションの初めでは Vec の len を 0 に設定し、そしてもし必要ならば、 デストラクタ内で len を修正します。このようにすることで、もしすべてが普通に実行されるなら、 最小限のオーバーヘッドで望まれている振る舞いを得ることが出来ます。 しかし、もし大胆にも mem::forget がイテレーションの真ん中に存在したら、 この関数によって、更に多くのものがリークされます (そして多分 Vec を、 一貫性はあるとしてもユーザが予期しない状態にするでしょう) 。 mem::forget は安全だとして 受け入れたので、このリークは絶対安全です。リークが更に多くのリークを引き起こしてしまうことを、 リークの増幅と呼びます。

Rc

Rc は興味深いケースです。なぜなら、ひと目見ただけでは、 Rc がプロキシな値とは全く見えないからです。 結局、 Rc は自身が指しているデータを操作し、その値に対する Rc が全てドロップされることで、 その値もドロップされます。 Rc をリークすることは特に危険のようには見えません。 参照カウントが永遠にインクリメントされたまま、データが解放されたりドロップされるのを 妨害します。けれどもこれは単に Box に似ています。そうですよね?

いいえ。

では、以下の単純化された Rc の実装を確認しましょう。

struct Rc<T> {
    ptr: *mut RcBox<T>,
}

struct RcBox<T> {
    data: T,
    ref_count: usize,
}

impl<T> Rc<T> {
    fn new(data: T) -> Self {
        unsafe {
            // もし heap::allocate がこのように動作したらよいと思いませんか?
            let ptr = heap::allocate::<RcBox<T>>();
            ptr::write(ptr, RcBox {
                data: data,
                ref_count: 1,
            });
            Rc { ptr: ptr }
        }
    }

    fn clone(&self) -> Self {
        unsafe {
            (*self.ptr).ref_count += 1;
        }
        Rc { ptr: self.ptr }
    }
}

impl<T> Drop for Rc<T> {
    fn drop(&mut self) {
        unsafe {
            (*self.ptr).ref_count -= 1;
            if (*self.ptr).ref_count == 0 {
                // データをドロップしそして解放します
                ptr::read(self.ptr);
                heap::deallocate(self.ptr);
            }
        }
    }
}

このコードは暗黙で微妙な前提を含んでいます。すなわち、 ref_countusize に 収まるということです。なぜなら、 usize::MAX 個以上の Rc はメモリに存在し得ないからです。 しかしながら、これ自体が ref_count が正確に、メモリ上にある Rc の数を反映しているという 前提の上にあります。ご存知のように、 mem::forget のせいでこれは正しくありません。 mem::forget を 使用することで、 ref_count をオーバーフローさせることが可能です。そして、 Rc が存在していても ref_count を 0 にすることができます。こうしてめでたく、内部データを解放後に使用することができます。 だめだだめだ、最悪だ。

これは単に ref_count を確認し、何かを行なうことで解決可能です。 標準ライブラリにおいては、単にアボートします。なぜならプログラムが ひどく悪化したからです。そしておーまいがっしゅ、これは本当に馬鹿げたコーナーケースなのです。

thread::scoped::JoinGuard

thread::scoped API は、共有されているデータが 1 つでもスコープを抜ける前に、 親がスレッドを join することを保証します。こうすることで、親スレッドのスタック上の データを同期なしに参照するようなスレッドを生成することが可能になります。

pub fn scoped<'a, F>(f: F) -> JoinGuard<'a>
    where F: FnOnce() + Send + 'a

ここで f は、他のスレッドで実行する何らかのクロージャです。 F: Send + 'a ということは、 f は 'a の長さ 生きるデータを内包します。そしてそのデータを所有するか、データは Sync ということになります (&data が Send であることを示唆します) 。

JoinGuard がライフタイムを所有しているため、 JoinGuard は、内包しているデータが 親スレッドで借用されたままの状態にします。これはつまり、 JoinGuard が、もう片方のスレッドが扱っている データよりも長生きできないことを意味します。 JoinGuard が本当にドロップされるとき、 JoinGuard は親スレッドをブロックし、内包されているデータのどれか 1 つでも親のスコープを 抜ける前に、子スレッドを確実に終了させます。

使用法は以下のような感じです。

let mut data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
{
    let guards = vec![];
    for x in &mut data {
        // 可変参照をクロージャ内にムーブします。そして、
        // クロージャを別のスレッド上で実行します。クロージャには
        // 可変参照 `x` のライフタイムによる、ライフタイムの制限があります。
        // 値が返される guard には、代わってクロージャのライフタイムが
        // 代入されます。ですから `x` と同じように、 guard も `data` を可変で借用します。
        // これは、 guard がスコープを抜けるまで `data` にアクセスできないことを意味します。
        let guard = thread::scoped(move || {
            *x *= 2;
        });
        // 後の使用に備えて、スレッドのガードを保存します
        guards.push(guard);
    }
    // 全てのガードはここでドロップされましたので、全てのスレッドを強制的に join します
    // (このスレッドは他のスレッドが終了するまでブロックされます) 。
    // スレッドが join されたら、借用は有効ではなくなり、データは
    // 再びこのスレッドからアクセス可能となります。
}
// data は絶対ここで変化します。

原則として、これは完全に動作します! Rust の所有権システムが見事にこれを確実に行ないます! ...デストラクタが安全に呼ばれるということに頼っている以外は。

let mut data = Box::new(0);
{
    let guard = thread::scoped(|| {
        // これは良くてもデータ競合を引き起こし、最悪、解放後の使用にもなります。
        *data += 1;
    });
    // guard が忘れられたので、このスレッドをブロックせず、
    // 借用が有効ではなくなります。
    mem::forget(guard);
}
// ですからスコープ内のスレッドが Box にアクセスしようとしているかもしれないし、
// しようとしていないかもしれない最中に Box がここでドロップされてしまいます。

ちくしょう。デストラクタが実行されるのは API にとってとても大切だったのですが、 全く別の設計上の理由で破棄しなくてはいけなかったのです。