🦀

Rust で mutable なグローバル変数を作る方法

2023/12/17に公開

基本的に Rust ではグローバル変数の使用はあまり推奨されていないが、たまには使いたくなることもあるのでメモしておく。

コンストラクタが const な場合

Vec のようにコンストラクタとなる関数が const fn である場合、単に Mutex で包めばよい。

use std::sync::Mutex;
use std::thread;
use std::time::Duration;

// mutable global variable
static CACHE: Mutex<Vec<String>> = Mutex::new(Vec::new());

fn main() {
    // writer thread
    let t1 = thread::spawn(|| {
        let mut cache = CACHE.lock().unwrap();
        cache.push("hello".to_owned());
    });
    // reader thread
    let t2 = thread::spawn(|| {
        thread::sleep(Duration::from_secs(1));
        let cache = CACHE.lock().unwrap();
        println!("{:?}", cache.first());
    });
    t1.join().unwrap();
    t2.join().unwrap();
}

コンストラクタが const でない場合

HashMap のようにコンストラクタが const fn でない場合、単に Mutex で包むだけでは値を構築できない。そこで、Mutex を更に LazyLock で包むことで初期化を遅延させる。

use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use std::thread;
use std::time::Duration;

// global mutable variable
static CACHE: LazyLock<Mutex<HashMap<String, String>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));

fn main() {
    // writer thread
    let t1 = thread::spawn(|| {
        let mut cache = CACHE.lock().unwrap();
        cache.insert("hello".to_owned(), "world".to_owned());
    });
    // reader thread
    let t2 = thread::spawn(|| {
        thread::sleep(Duration::from_secs(1));
        let cache = CACHE.lock().unwrap();
        println!("{:?}", cache.get("hello"));
    });
    t1.join().unwrap();
    t2.join().unwrap();
}

LazyLock は Rust 1.80 で安定化された。それ以前のバージョンの場合は以下に示すような別方法が必要になる。

Rust 1.79 以前の場合

外部crateが必要となるが、LazyLock の代わりに once_cell の Lazy が利用できる。

use std::collections::HashMap;
use std::sync::Mutex;
use std::thread;
use std::time::Duration;
use once_cell::sync::Lazy;

// global mutable variable
static CACHE: Lazy<Mutex<HashMap<String, String>>> =
    Lazy::new(|| Mutex::new(HashMap::new()));

pub fn example() {
    // writer thread
    let t1 = thread::spawn(|| {
        let mut cache = CACHE.lock().unwrap();
        cache.insert("hello".to_owned(), "world".to_owned());
    });
    // reader thread
    let t2 = thread::spawn(|| {
        thread::sleep(Duration::from_secs(1));
        let cache = CACHE.lock().unwrap();
        println!("{:?}", cache.get("hello"));
    });
    t1.join().unwrap();
    t2.join().unwrap();
}

また、標準ライブラリだけで実装したいのであれば、OnceLock を使うこともできる。この場合は初期化兼取得用の関数を用意することになる。

use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use std::thread;
use std::time::Duration;

// global mutable variable
static CACHE: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();

fn get_cache() -> &'static Mutex<HashMap<String, String>> {
    CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}

fn main() {
    // writer thread
    let t1 = thread::spawn(|| {
        let mut cache = get_cache().lock().unwrap();
        cache.insert("hello".to_owned(), "world".to_owned());
    });
    // reader thread
    let t2 = thread::spawn(|| {
        thread::sleep(Duration::from_secs(1));
        let cache = get_cache().lock().unwrap();
        println!("{:?}", cache.get("hello"));
    });
    t1.join().unwrap();
    t2.join().unwrap();
}

組み込み型の場合

bool, i64, usize などの組み込み型には AtomicBool, AtomicI64, AtomicUsize といった atomic 型が標準ライブラリに用意されている。このような型を使えば mutable なグローバル変数を作れる。

use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use std::time::Duration;

// global mutable variable
static COUNT: AtomicUsize = AtomicUsize::new(0);

fn main() {
    // writer thread
    let t1 = thread::spawn(|| {
        for _ in 0..1000 {
            COUNT.fetch_add(1, Ordering::SeqCst);
        }
    });
    // reader thread
    let t2 = thread::spawn(|| {
        thread::sleep(Duration::from_secs(1));
        let count = COUNT.load(Ordering::SeqCst);
        println!("{}", count);
    });
    t1.join().unwrap();
    t2.join().unwrap();
}

Ordering は、よくわからない場合は SeqCst を指定しておくのが無難。

Discussion