📝

Rust の Cell, RefCell を整理する

2022/07/31に公開約3,300字

はじめに

Rust の Cell, RefCell まわりを理解してなかったのでその辺りと関連してスレッド間の排他制御まわりを整理しました。

Cell

"原則として不変の型として扱いたいが、特例として一部を限定的に変更できるようにしたい" というケースが存在します。代表的な例としては参照カウンターです。参照カウンターは管理しているインスタンスが不変だったとしてもカウンターは参照の増減に応じて変化させなければならないので変更可能にしなければなりません。
C++ の場合はそのような特例メンバーに対して "mutable" を付与します。

class Immutable {
private:
    mutable int _a;
    int _b;

public:
    void increment() const {
        _a++;    // OK
        _b++;    // NG
    }
};

Rust の場合は Cell を用いると不変状態でも更新可能になります。

pub struct Immutable {
    a: Cell<i32>,
    b: i32,
}

let x = Immutable { Cell::new(0), 0 }
x.a.set(x.a.get() + 1);     // OK
x.b += 1;                   // NG

Cell は get/set で値の取得、変更をします。
Cell は実体を管理するため Copy Trait が実装されている必要があります。

RefCell

RefCell はコンパイル時での借用チェックでは不正 (借用できない) と判断せざるをえないがプログラマーが問題ないと判断できる場合、コンパイル時に借用チェックをせずに実行時に借用チェックをするようにするものです。

次のような通常の借用を用いて記述したコード

let mut a = 10;
let a1 = &mut a;
let a2 = &a;
*a1 = 10;

をコンパイルしようとするとエラーになります。

error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable

 let a1 = &mut a;
          ------ mutable borrow occurs here
 let a2 = &a;
          ^^ immutable borrow occurs here
 *a1 = 10;
 -------- mutable borrow later used here

RefCell を用いて同等のコードを記述してみます。
RefCell は Cell と同様に不変値とすることができます。また、可変参照の取得には borrow_mut を、不変参照は borrow を用います。

let a = RefCell::new(10);
let mut a1 = a.borrow_mut();
let a2 = a.borrow();
*a1 = 10;

これはコンパイルが通ります。しかしこれを実行すると "let a2 = a.borrow();" で panic がおきます。

コンパイルエラーと同様、可変参照 a1 が有効な状態で不可変参照 a2 を取得しようとしてエラーになりました。

Rc と RefCell

Rc は参照カウンターを用いて複数の所有権を持たせるものですが、 Rc は不変値しか扱えない (可変参照取得に関する制約のためと思われます) のでこれだと使用できるケースがかなり限定的になってしまいます。
そこで RefCell と組み合わせる事で変更可能な Rc とすることができるようになります (これが RefCell の代表的な用途) 。

use std::cell::RefCell;
use std::rc::Rc;

pub struct Test {
    x: i32,
}

impl Test {
    pub fn println(&self) {
        println!("{}", self.x);
    }
}

fn main() {
    let a = Rc::new(RefCell::new(Test { x: 11 }));
    a.borrow().println();
    let a1 = a.clone();
    let a2 = a.clone();

    a1.borrow_mut().x = 12;
    a2.borrow().println();

    let a3 = a1.borrow_mut();
    let a4 = a2.borrow();
}

最後の a4 は借用ルール違反なので panic になります。

基本的に RefCell からの借用は保持しないようにした方がよいでしょう。

スレッドをまたぐ

RefCell はスレッドをまたげないのでスレッドをまたぐ場合は Mutex を使用します。 Mutex はその名の通り Mutex での排他制御 (シングルアクセスのみ) を行い競合を防ぎます。
Arc は Rc の参照カウンターが Atomic 版です。

fn main() {
    let a = Arc::new(Mutex::new(Test { x: 11 }));
    let a1 = a.clone();
    let a2 = a.clone();

    let t = thread::spawn(move || {
        a2.lock().unwrap().x = 10;
    });
    t.join();
    a1.lock().unwrap().println();
}

あるいは RwLock を使用します。 RwLock は書き込み (write lock) はシングルアクセス、読み込み (read lock) は複数同時アクセスを許容するものです。

fn main() {
    let a = Arc::new(RwLock::new(Test { x: 11 }));
    let a1 = a.clone();
    let a2 = a.clone();

    let t = thread::spawn(move || {
        a2.write().unwrap().x = 10;
    });
    t.join();
    a1.read().unwrap().println();
}

Rust の Mutex, RwLock は再帰を認めていないようなので、

let a = RwLock::new(1);
let a1 = a.write().unwrap();
let a2 = a.write().unwrap();

はデッドロックします。これは見方によると借用ルール違反っぽく見えますが、 lock() や read(), write() の段階では借用しているわけではないので、あくまで不正な排他制御の結果によるものです。

おわりに

Rust 面倒くさいなあとちょっと思いましたがそれだけ安全性に配慮がされているという事ですね。

Discussion

ログインするとコメントできます