🦀

【Rust】static でも Drop したい

2022/08/01に公開

Rustのstatic変数はいわゆるグローバル変数として使われることが多いですが、static変数はDropできない(デストラクタが呼ばれない)という特徴があります。

Static items do not call drop at the end of the program.

https://doc.rust-lang.org/reference/items/static-items.html

メモリ管理の仕組みとして見ると、'static なライフタイムを持つstatic変数はdrop() の実行時でも生存している(有効である)必要があるため、自然な仕様に思えます。一方で、ある構造体がメモリに限らない一般的なリソースを所有するような場合、解放処理はDropとして実装されることが多いです。そのような構造体をプログラム全体で共有したいとき、static変数として表現すると、最初に提示した「Dropできない」という特徴が足枷になってしまうことがあります。この記事ではそのような場合の対処法をいくつか考えようと思います。

1. static変数を使わない

設計上static変数として扱わず、ローカル変数として明示的に引数等で渡して問題ないのであればこれが一番簡単な方法です。この場合問題なくdropが呼ばれます。

2. 明示的にデストラクタを呼ぶ

プログラム中で終了処理としてデストラクタに相当する処理を実行できるのであれば、そうするのも簡単な方法です。ただ、デストラクタに相当する処理を実行した後であってもstatic変数にはアクセス可能なので、Mutex<Option<_>> のような型で包むことで、不正なアクセスができないようにする工夫が必要かもしれません。Rustの掲げるRAII (Resource Acquisition Is Initialization) としては多少不自然なAPIとなる点は否めません。

3. thread_local! を使う

次に紹介するのはthread_local! を使う方法です。2.で扱った方法は、明示的にデストラクタを呼び出す必要があるため、リソースの解放はプログラマ(ライブラリのユーザー)の責任になります。ライブラリの開発者としてはそのようなAPIはできるだけ避けたい(自動でデストラクタが呼ばれてほしい)と考えるのが自然です。

スレッドごとに独立したstatic変数を利用する方法として、標準ライブラリには thread_local!と呼ばれるマクロが定義されています。 次のように使うことができます。

struct Var(i64);
thread_local!(static THREAD_VAR: Var = Var(0));

THREAD_VAR.with(|var| {
    println!("THREAD_VAR: {}", var.0);
});

グローバルなstatic変数と異なり、thread_local! を使って宣言された変数は、スレッドが終了する際にdropが実行されるため、明示的にデストラクタを実行する必要はなくなります。ただし、thread_local! で宣言された変数はスレッドごとに独立しているので、スレッドを超えて共有することはできません。また、thread_local!で宣言された変数はスレッド間で共有できないようなAPIになっているため、通常のstatic変数ほど自由に取り回しができない場合もあります。さらに、thread_local!で宣言された変数のdropについては、"best effort"という記述もあり、全てのプラットフォームで動作しない可能性がある点は注意が必要です。

4. thread_local!Weak を組み合わせる

ここからが本題になります。3.で紹介した方法は複数スレッドから同一のグローバルなstatic変数にアクセスしたいような場合にはそのまま使うことができません。そのような場合のテクニックとして thread_local!Weak (参照カウンタにおける弱参照)を組み合わせる方法が考えられます。これは、ある構造体をスレッド間で共有したいが、プログラム終了時にdropも呼び出したい、というような場合に有効な方法です。

まずは多少長くなりますが、サンプルコードになります。
Play ground

use once_cell::sync::Lazy;
use std::sync::{Arc, Mutex, Weak};

struct Global(&'static str);

// グローバル変数(Global)のプレースホルダー
static GLOBAL: Lazy<Mutex<Weak<Global>>> = Lazy::new(|| Mutex::new(Weak::new()));
// グローバル変数への参照カウンタ
thread_local!(static LOCAL: Arc<Global> = Global::new_arc());


impl Global {
    fn new() -> Self {
        Self("global")
    }

    fn new_arc() -> Arc<Self> {
        let mut global = GLOBAL.lock().unwrap();
        match global.upgrade() {
            Some(val) => val,
            None => {
                let val = Arc::new(Global::new());
                *global = Arc::downgrade(&val);
                val
            }
        }
    }

    fn value(&self) -> &'static str{
        self.0
    }
}

impl Drop for Global {
    fn drop(&mut self) {
        println!("drop");
    }
}

fn main() {
    LOCAL.with(|x| {
        println!("{}", x.value());
    });
}

これを実行すると、最後にdrop が表示されて確かにdropが呼ばれていることが確認できます。
このテクニックの肝はthread_local!で宣言された変数ではdropが呼ばれるため、参照カウンタの強参照を持つようにして、スレッドが終了した際には参照カウンタを減らせるようになり、一方でグローバルなstatic変数としては弱参照を持つことでスレッドが全て終了した際には、参照カウンタが0になりGlobalのdropが呼ばれるようになっていることです。

最後に

この記事ではstatic変数であってもDropできるようにするためのテクニックを紹介しました。
これは自作のライブラリでこのようなユースケースが必要になり、この記事で紹介したようなものを思いついたものの、他に妥当な方法が見当たらなかったため今回記事にしました。もし、他にいい方法がある、もしくは似たようなテクニックがどこかで使われている、といった知見をお持ちの方がいらっしゃったらコメントで教えていただけると幸いです。

Discussion