Rust
22
どのような問題がありますか?

この記事は最終更新日から1年以上が経過しています。

投稿日

Organization

Rustで相互参照: ノーガード戦法編

RustはトレースGCを持たずRAIIと所有権に基づくメモリ管理を行います。これには様々な利点がある一方、相互参照をもつデータの扱いが他のプログラミング言語より難しいという困難があります。本記事では、あまり一般的ではないが特定の限られた用途では有用と思われる方法を紹介します。

標準的な方法

まずは相互参照が起きないように設計を再考するのがいいでしょう。特に「子データから親データを参照する」といったユースケースでは、必ずしも子データ自体が親データへの参照を持たなくてもいいことがあります。以下ではこれに当てはまらない例、典型的にはグラフの表現を念頭に置いて記述します。

Rustで相互参照を扱う最も標準的な方法は、typed_arenaなどのアリーナアロケーターRefCellなどの内部可変性コンテナを組み合わせる方法です。これについては私のブログ記事などを参考にしてください。

またRustの Arc を読む (1)ではArcとWeakを使った方法も説明しました。

ノーガード戦法

Rustにおける相互参照が難しいのは、結局のところメモリやリソースの解放の順番の問題に由来しています。たとえばアリーナアロケータは個別に確保し、一括で解放することによってこの問題を解決しています。

では、そもそもメモリやリソースを解放しなくていい状況であればどうでしょうか?「1プロセスでは1つの問題しか解かなくてよい」という割り切りが可能なのであれば、自身でリソースの解放処理を行わなくても、単にプロセスを終了まで持っていったほうが簡単です。その場合は単にメモリリークを許容することで簡単に相互参照を実現できます。

// ノーガード戦法による相互参照
// 意図的にメモリリークを行っているので、通常の用途では使ってはいけない

use std::cell::RefCell;

struct Node {
    id: i32,
    edges: RefCell<Vec<&'static Node>>,
}

impl Node {
    fn new(id: i32) -> &'static Self {
        // Box::leak を使うことで意図的にメモリリークを起こしている
        Box::leak(Box::new(Node {
            id,
            edges: RefCell::new(vec![]),
        }))
    }
}

fn main() {
    let node1 = Node::new(1);
    let node2 = Node::new(2);
    let node3 = Node::new(3);
    node1.edges.borrow_mut().push(node2);
    node2.edges.borrow_mut().push(node1);
    node2.edges.borrow_mut().push(node3);
    node3.edges.borrow_mut().push(node2);

    eprintln!("node1 has {} edges", node1.edges.borrow().len());
    eprintln!("node2 has {} edges", node2.edges.borrow().len());
    eprintln!("node3 has {} edges", node3.edges.borrow().len());
}

FAQ

  • メモリリークして大丈夫か?
    • 一般的には大丈夫じゃないです。が特定の場面では有用かもしれない、というのが本記事の意図です。
    • 「メモリリークを厳密に防ぐのは難しい」ということがLeakpocalypseと呼ばれる議論によってわかって以来、Rustではメモリリークは許容することになっています。メモリリークが起きても、それ以上の深刻な事象 (境界外参照やデータ競合などの未定義動作) は発生しないように設計されています。この意味で、メモリリークは「安全」という扱いになっています。
    • とはいえ、メモリリークは良いことではないので、意図しないメモリリークはできるだけ防ぐように設計されています。たとえば上記のプログラムでは Box::leak というメソッドを呼ぶことで明示的にメモリリークを起こしていますが、このようにプログラマーがメモリリークを意図しているとはっきりわかる方法をとらずにメモリリークを起こすのは簡単ではありません。
  • RefCell は回避できないのか?
    • 理想の選択肢はないです。
    • Cellを使えばランタイム検査を回避できますが、借用が使えなくなるのでトリッキーな操作が要求されるようになります。
    • RefCellで起こりがちな、意図しない二重借用によるパニックはqcellを使えば防げますが、ランタイム検査は依然として存在します。また、オーナーハンドルを持ち回す分コードが若干冗長になるかもしれません。
    • スレッド安全性が必要なら上記の qcell か、 Mutex/RwLock を使うことができます。
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
ユーザー登録ログイン
qnighy
wantedly
「シゴトでココロオドル」ためのビジネスSNS「Wantedly」の開発・運営をしています。

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
remote.it を使って○○に接続してみた!
~
深層学習の論文について解説してみた
~
22
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー