インシデントの発生
Rustは通常、コンパイル時に間違いを検出しますが、RefCellは少し特殊です。これは、ボローチェッカーのロジックをコンパイラから実行中のプログラムへと移動させます。アプリケーションが突然パニックを起こしてクラッシュするまでは、すべてが順調に見えるかもしれません。ログにはその原因が記録されています:
thread 'main' panicked at 'already borrowed: BorrowMutError'
これが起こるのは、RefCell<T>が「内部可変性(interior mutability)」を提供しているためです。これにより、コンテナへの不変参照しか持っていない場合でも、データを変更できるようになります。しかし、Rustのルールは依然として適用されます。複数の読み手(reader)を持つか、あるいは唯一の書き手(writer)を持つかのどちらかであり、両方を同時に持つことはできません。プログラムの実行中にコードがこのルールを破ろうとすると、RefCellはデータの破損を防ぐために即座にパニックを発生させます。
根本原因:実行時の借用カウンター
内部的に、RefCellはアクティブな借用を追跡するための小さなカウンターを保持しています。.borrow()を呼び出すと、読み手のカウントが増加します。.borrow_mut()を呼び出すと、書き手がアクティブであることを示すフラグが立てられます。BorrowMutErrorは、主に以下の2つの状況で発生します:
- シナリオA: 他の誰かがすでにデータを読み取っている最中に(アクティブな
Refガードが存在する状態)、borrow_mut()を呼び出そうとした場合。 - シナリオB: データがすでに変更されている最中に(アクティブな
RefMutガードが存在する状態)、borrow()またはborrow_mut()を呼び出そうとした場合。
多くの開発者がこのエラーに直面するのは、借用ガードが予想以上に長くスコープ内に留まり、同じデータにアクセスしようとする関数呼び出しをまたいでしまうことが原因です。
解決策1:明示的なスコープの使用
借用ガードは、スコープを抜けたときにのみドロップされます。実行時間の長い関数がある場合、借用の使用が終わった後でも、その借用が「生存」し続けることがあります。ロジックを単純なブロック {} で囲むことで、借用を強制的に終了させることができます。
問題のあるコード:
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
let list = data.borrow(); // この読み取りガードはメインブロックの最後まで生存し続けます
// ... 50行の他のロジック ...
data.borrow_mut().push(4); // パニック! 読み手である 'list' がまだアクティブです。
println!("{:?}", list);
解決策:
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
{
let list = data.borrow();
println!("Current list: {:?}", list);
} // ガードはここで即座にドロップされます。
data.borrow_mut().push(4); // 成功! アクティブな借用は残っていません。
解決策2:手動での借用解除
コードを新しいブロックで囲むのが難しい場合もあります。そのような場合は、std::mem::drop()を使用して、使い終わった瞬間にガードを解放します。これは、ロジックが入り組んでいたり、条件分岐に依存していたりする場合に便利です。
let data = RefCell::new(10);
let val = data.borrow();
if *val > 5 {
// 変更が必要ですが、'val' がまだ読み取りロックを保持しています
drop(val);
*data.borrow_mut() += 1;
}
解決策3:パニックを起こさない try_borrow の使用
本番環境では、クラッシュは多くの場合、最悪のシナリオです。失敗時にパニックを起こす borrow_mut() の代わりに、try_borrow_mut() を使用してください。このメソッドは Result を返すため、アプリケーションを停止させることなく、競合を適切に処理できます。
match data.try_borrow_mut() {
Ok(mut guard) => {
*guard += 1;
}
Err(_) => {
// クラッシュさせる代わりに、エラーをログに記録するか再試行します
eprintln!("Resource is currently busy. Skipping update.");
}
}
解決策4:再入(リエントランシ)を避けるためのリファクタリング
再帰関数でこのエラーが発生する場合、設計の結合度が強すぎる可能性があります。&RefCell をヘルパー関数に渡し、その関数内でも借用を行うのは、トラブルの元です。代わりに、データを一度借用し、その直接の参照(&T または &mut T)をヘルパー関数に渡すようにします。
避けるべき例:
fn update(cell: &RefCell<Data>) {
let mut data = cell.borrow_mut();
helper(cell); // もし helper() が cell.borrow() を呼び出すとクラッシュします
}
推奨される例:
fn update(cell: &RefCell<Data>) {
let mut data = cell.borrow_mut();
helper(&mut data); // コンテナではなく、内部のデータを渡します
}
fn helper(data: &mut Data) {
// この関数は RefCell の存在を知る必要さえありません
}
検証
実行時の動作を確認して、修正を検証します:
cargo runを実行します。BorrowMutErrorが発生せずにアプリケーションがタスクを完了すれば、当面の競合は解決されています。- 断続的に発生するバグについては、ロジックを1,000回ループさせるユニットテストを記述してください。これにより、ロジックの流れにおけるレースコンディションのような状況を特定しやすくなります。
try_borrowの手順で追加したカスタムエラーメッセージがログに出ていないか監視します。
予防策
- 標準の借用を優先する: コンパイラをどうしても満足させられない場合にのみ
RefCellを使用してください。 - 借用を最小限に留める:
RefまたはRefMutを保持する行数を、必要最小限に抑えてください。 - Mutex への切り替え: マルチスレッド環境に移行する場合、
RefCellは機能しません。代わりにRwLockやMutexを使用してください。これらは同様の内部可変性を提供しますが、パニックを起こすのではなく、スレッドを安全にブロックします。

