Rustのstaticいろいろ

概要: Rustのstaticの亜種をいろいろ挙げる。

ここでは以下のように分類します。

  • 変数
    • ローカル束縛 (引数、 let, match, for, if let, while let)
    • 静的変数/定数
      • コンパイル時定数 (const)
        • アイテム位置の定数
        • トレイトの定数
        • 実装の定数
      • 静的変数 (static)
        • 非スコープ借用される静的変数
          • 組み込み static
          • lazy_static!
          • futures-0.2 task_local!
        • スコープ借用される静的変数
          • thread_local!
          • scoped_thread_local!
          • futures-0.1 task_local!

目次

  • conststatic の違い
  • スコープ借用と非スコープ借用
  • 組み込み static
  • lazy_static!
  • thread_local!
  • scoped_thread_local!
  • futures-0.1 task_local!
  • futures-0.2 task_local!

conststatic の違い

  • constコンパイル時に決まるそのものを定義します。この値はコンパイル時定数が必要な他の場所で使えます。
  • static は(コンパイル時に決まる値が入っている)場所を定義します。値がコンパイル時に決まるのは初期化の都合上にすぎず、それ以上の意味はありません。

たとえば、次のようなコードを考えます。

use std::sync::atomic::{self, AtomicUsize};

const X: AtomicUsize = AtomicUsize::new(0);
static Y: AtomicUsize = AtomicUsize::new(0);

fn main() {
    // どちらもX, Yを5回インクリメントして表示している
    for _ in 0..5 {
        // 0が5回表示される
        println!("X = {}", X.fetch_add(1, atomic::Ordering::SeqCst));
    }
    for _ in 0..5 {
        // ちゃんとインクリメントされる
        println!("Y = {}", Y.fetch_add(1, atomic::Ordering::SeqCst));
    }
}

これを実行すると以下のように出力されます。

X = 0
X = 0
X = 0
X = 0
X = 0
Y = 0
Y = 1
Y = 2
Y = 3
Y = 4

fetch_add&self をとるので、実際には &X&Y が計算されています。この & の動作は以下のように規定されています。

  • X が左辺値なら、そのアドレスをとる。
  • X が右辺値なら、匿名のローカル変数に X を代入して、そのアドレスをとる。
  • ただし、 X が右辺値の場合であっても、以下の条件が満たされる場合は、一時変数ではなく匿名の静的領域に X を代入して、そのアドレスをとる。 (RFC1414 rvalue static promotion)
    • &mut ではなく & 参照をとろうとしている。
    • Xコンパイル時に決定される。
    • X は内部可変性をもたない。
    • X の計算中に内部可変性をもつ関数が使われていない。

ここで X は内部可変性をもつ定数右辺値ですから、2番目の規則が適用されて、一時変数の値が使われます。その結果毎回新しい AtomicUsize が生成されるため、毎回0が出力されます。

このように、状態を管理する場所を作りたい場合は、 static を使います。そのため static が必要なときは 内部可変性をともなうことがほとんどです。

一方、コンパイル時に決まるそのものを定義したい場合は const が適しています。たとえば以下のような場合です。

const X: usize = 0;
// constは他のconst/staticの初期化に利用できる
static Y: usize = X;

fn main() {
    // constは長さのようなコンパイル時定数に使える
    let a = [0; X];
}

以前は、static ではなく const にしてしまうと借用が常に一時的になってしまって煩わしいという問題がありましたが、rvalue static promotionによりこの問題は改善しています。そのため、「値を定義するときは const」「場所を定義するときは static」という原則に近い運用であまり困らないと思います。

スコープ借用と非スコープ借用

Rustの組み込み static 以外にも、 static に準ずる機能を提供するマクロはいくつかあります。これらは使われ方で「スコープ借用」型と「非スコープ借用」型に分類できます。(※本稿独自の分類)

スコープ借用では、借用ごとにクロージャのコールバックが必要になります。コールバックの終了時に借用の終了が強制されます。

X.with(|x| {
  ...
})

いっぽう、非スコープ借用では &XX.get(..) のような方法で借用をとることができます。

組み込み static

言語に組み込みの static です。

use std::sync::atomic::{self, AtomicUsize};

static COUNTER: AtomicUsize = AtomicUsize::new(0);

fn main() {
    // インクリメント
    COUNTER.fetch_add(1, atomic::Ordering::SeqCst);
}

ポイント

  • スコープ: プロセス全体で共有される。
  • 要請: T: Sync + 'static
  • 初期化: コンパイル時定数式でしか初期化できない。 (より高度な初期化には lazy_static! を使う)
  • 借用: &X で借用できる。
  • drop: dropされない。

組み込み static の亜種

組み込みの static は原則として単独で使いますが、以下のような亜種があります。

  • static mut: &mut X での借用が可能になるが、unsafe。排他性を自分で確認する必要がある。 static から借用したものを無理矢理 &mut X として使うとUBになるので static mut にする必要がある。 たいていの場合は static + 内部可変性で事足りる。
  • #[thread_local] static: スレッドローカルになる。不安定機能なので #![feature(thread_local)] が必要。以下の問題点がある。
    • thread_local! と違って動かないプラットフォームがある。
    • dropされない。
  • extern { static }: FFIで使われる。

lazy_static!

初回使用時に初期化される静的変数のためのマクロです。

Mutex::new() 等がコンパイル時定数でないために必要になることが多いです。これは Mutex::new() が内部で Box::new() を必要としているなどの事情があります。軽量同期ライブラリの parking_lot を使って解決する手もあります。

#[macro_use]
extern crate lazy_static;

use std::sync::Mutex;

lazy_static! {
    // 文法上の特徴: "static" ではなく "static ref" を使う
    static ref COUNTER: Mutex<i64> = Mutex::new(0);
}

fn main() {
    // インクリメント
    *COUNTER.lock().unwrap() += 1;
}

ポイント

  • スコープ: プロセス全体で共有される。
  • 要請: T: Sync + 'static
  • 初期化: 初回使用時に初期化するので、コンパイル時定数の必要はない。明示的な初期化も可能。
  • 借用: &X で借用できる。(Derefが実装されている)
  • drop: dropされない。

thread_local!

thread_local! は標準ライブラリマクロで、スレッドローカル変数を作ります。

use std::cell::RefCell;

thread_local! {
    static COUNTER: RefCell<i32> = RefCell::new(0);
}

fn main() {
    COUNTER.with(|counter| {
        // インクリメント
        *counter.borrow_mut() += 1;
    })
}

ポイント

  • スコープ: スレッドごと
  • 要請: T: 'static
  • 初期化: スレッドごとの初回使用時に初期化するので、コンパイル時定数の必要はない。
  • 借用: with でスコープを作って借用する必要がある。
  • drop: 通常はスレッド終了時にdropされるが、保証されているわけではない。

なお、 thread_localクレイトはスレッドごとの静的変数ではなく、スレッドごと×オブジェクトごとの静的領域を提供します。

scoped_thread_local!

scoped_thread_local! は scoped-tlsクレイトが提供するマクロで、参照をそのまま保管できる特殊なストレージを提供します。何らかのコンテキストを暗黙的に引き回す必要がある場合に便利です。 (基本的には明示的に引き回したほうがいいと思いますが)

#[macro_use]
extern crate scoped_tls;

scoped_thread_local!(
    static CONTEXT: i32
);

fn main() {
    let context = 42;
    CONTEXT.set(&context, || {
        CONTEXT.with(|context| {
            println!("context = {}", context);
        })
    })
}

ポイント

  • スコープ: スレッドごと
  • 要請: T: 'static (設計上は T: ?Sized になりそうだけどそれは対応していない様子) ただし、実際には T ではなく &T を保管する。
  • 初期化: set が呼び出されている間だけ値が入っていて、それ以外の期間に with を呼ぶとパニックになる。
  • 借用: with でスコープを作って借用する必要がある。
  • drop: 参照渡しなのでdropは関係ない。

futures-0.1 task_local!

futuresの task_local! はタスク(軽量スレッド) ごとに異なる記憶領域を擬似的に提供しますが、このインターフェースはfutures-0.1とfutures-0.2で異なります。futures-0.1のそれはスコープ借用で、 thread_local! と似た使い心地です。

#[macro_use]
extern crate futures;
extern crate tokio;

use tokio::prelude::*;

use std::cell::RefCell;

task_local!(
    static COUNTER: RefCell<i32> = RefCell::new(0)
);

fn main() {
    tokio::run(future::lazy(|| {
        COUNTER.with(|counter| {
            // インクリメント
            *counter.borrow_mut() += 1;
        });
        Ok(())
    }));
}
  • スコープ: タスクごと
  • 要請: T: Send + 'static (宣言時は Send は要求されない)
  • 初期化: タスクごとの初回使用時に初期化するので、コンパイル時定数の必要はない。
  • 借用: with でスコープを作って借用する必要がある。
  • drop: タスク終了時にdropされる。

futures-0.2 task_local!

futuresの task_local! はタスク(軽量スレッド) ごとに異なる記憶領域を擬似的に提供しますが、このインターフェースはfutures-0.1とfutures-0.2で異なります。futures-0.2のそれは非スコープ借用で、面白い構成になっています。contextを引き回しているおかげで、&mut な借用ができるというのも面白い点といえます。

#[macro_use]
extern crate futures;

use futures::prelude::*;

use futures::executor::ThreadPool;

task_local!(
    static COUNTER: i32 = 0
);

fn main() {
    ThreadPool::new()
        .unwrap()
        .run(futures::future::lazy(|context| {
            // インクリメント
            *COUNTER.get_mut(context) += 1;
            Ok(()) as Result<(), futures::never::Never>
        }))
        .unwrap();
}
  • スコープ: タスクごと
  • 要請: T: Send + 'static (宣言時は Send は要求されない)
  • 初期化: タスクごとの初回使用時に初期化するので、コンパイル時定数の必要はない。
  • 借用: contextのライフタイムだけ借用できるが、同時に1つのtask-local dataしか借用できない。
  • drop: タスク終了時にdropされる。

なお事実上のfutures-0.3にあたるRFC2408の現時点でのドラフトでは、contextからLocalMapを削除することが提案されています。したがってfutures-0.3のtask-local storageはfutures-0.1方式に戻るのではないかと思います。

まとめ

  • const は値、 static は場所を定義する。
  • static っぽいことをするマクロは色々あり、使い分けると幸せになれる。