Rust 中的全局状态

Rust 中的全局状态

·

1 min read

最近写 Rust 程序需要用到全局状态,我希望有一个状态在公共的作用域里,其他模块能通过引入等方式去访问他,在 JS 中做起来很简单:

const globalState = {
    name: 'abc'
}

function foo() {
  console.log(globalState)
}

function bar() {
  globalState.name = '123'
}

由于 JS 的限制较少,变量声明的位置没有限制,在代码顶部声明后,后续所有作用域都能使用。

在 Rust 中就不能直接写成 JS 这样,由于 Rust 的所有权和生命周期限制,这种写法是不可能出现的。

但 Rust 是多范式的编程语言,要实现这种“全局状态”的功能,也会有好几种替代的方案:

static

既然要管控生命周期,那就把生命周期拉长。

将变量修饰成 static 后,这个变量的生命周期就变成了程序运行期间,这就意味着任何地方都能访问他,因为这是最长的生命周期了:

static STATE: u32 = 1;

static 存在一些限制,其一是当他配合 mut 修饰的时候,需要使用 unsafe 标记才能修改变量的值,并且还有隐含的状态共享、并发修改问题。

其二是他的值只能是在编译期能确定的(常量,常量表达式,常量函数,还有实例化的语句),比如说不能将函数的值赋给 static 变量。


题外话,如果要求 safe,可以配合 LazyLock 使用:

pub fn get_val() -> i32 {
    1
}

static A: LazyLock<i32> = LazyLock::new(get_val);

LazyLock 本身是可以在编译期确定的,get_val 的结果不需要那么早确定,在运行时,第一次使用时才会对 get_val 求值,所以这种写法是合理,且带有 lazy 特性的。

thread_local!

当我想到 static 解法的时候,我第一反应是 static + RefCell 直接绕过检查去修改指针的值,但这也是不行的,因为 RefCellCell 都没有实现 Sync trait,所以在多线程场景下不安全,作为 static 变量必须得考虑多线程的情况。

但如果确定这个变量只在单线程内有效,可以使用 thread_local 宏:

thread_local! {
        pub static FOO: u32 = 1;
}

pub fn main() {
        FOO.with(|v| println!("{v}"));
}

这样限制了使用场景只在单线程,是可以配合 RefCell 使用的,因为不需要考虑并发修改的情况。

单例模式

这就像回归了 Java 等 OOP 语言的编码思路,在 Java 中就会做一些单例的状态管理类,在任何地方都能初始化,能同时访问、管理内部状态。

但我查了一圈,发现 Rust 中没有“标准”的单例模式写法,又或者说写法五花八门,最接近 OOP 写法的应该是这样的:

struct Singleton {
    data: String;
}

impl Singleton {
    fn get_instance() -> Arc<Mutex<Singleton>> {
        static mut INSTANCE: Option<Arc<Mutex<Singleton>>> = None;
        unsafe {
                INSTANCE.get_or_insert(|| {
                        Arc::new(Mutex::new(Singleton {
                                data: String::from("abc");
                        }))
                }).clone()
        }
    }
}

后话

在看完 actix_web 和 tauri 的文档后,我发现他们都有一个相似的解决“全局状态”的办法:

App::new()
    .app_data(state.clone())
    .run()

在创建服务的时候,他们会提供一个“设置状态”的 API,在 tauri 中是 manage,通过这个 API 设置的状态会被保存到 app 中,在后续定义 service/command 时可以直接通过 app 实例访问。