在编程中,我们经常会遇到需要创建全局状态的情况。这种做法通常被认为是不好的编程实践,因为它可能会导致代码难以维护和理解。然而,在某些情况下,全局状态是必要的,例如OpenGL子系统。在这种情况下,我们需要找到一种安全有效的方法来实现它。
Rust 语言为我们提供了几种方法来创建全局可变单例模式。在本文中,我们将探讨这些方法,并讨论它们各自的优缺点。
使用 std::sync::Mutex 创建全局单例
最基本的方法是使用 std::sync::Mutex
来保护我们的全局状态。这种方法如下所示:
use std::sync::Mutex;
static GLOBAL_DATA: Mutex<Vec<i32>> = Mutex::new(vec![]);
fn main() {
GLOBAL_DATA.lock().unwrap().push(42);
println!("{:?}", GLOBAL_DATA.lock().unwrap());
}
这种方法的优点是简单直接,但也存在一些缺点:
- 每次访问全局状态时都需要获取锁,这可能会影响性能。
- 如果忘记释放锁,可能会导致死锁问题。
- 初始化全局状态的方式不够灵活。
使用 std::sync::OnceLock 创建全局单例
Rust 1.17 版本引入了 std::sync::OnceLock
,它可以帮助我们更好地管理全局单例。使用 OnceLock
的例子如下:
use std::sync::OnceLock;
static GLOBAL_DATA: OnceLock<Mutex<Vec<i32>>> = OnceLock::new();
fn array() -> &'static Mutex<Vec<i32>> {
GLOBAL_DATA.get_or_init(|| Mutex::new(vec![]))
}
fn main() {
array().lock().unwrap().push(42);
println!("{:?}", array().lock().unwrap());
}
这种方法的优点是:
- 初始化全局状态的方式更加灵活。
- 只有在首次访问时才会进行初始化,减少了不必要的开销。
但缺点是:
- 代码相对更加复杂。
- 如果初始化过程非常昂贵,可能会影响性能。
使用 lazy_static 创建全局单例
lazy_static
是一个第三方库,它提供了一种更简单的方式来创建全局单例。使用 lazy_static
的例子如下:
#[macro_use]
extern crate lazy_static;
use std::sync::Mutex;
lazy_static! {
static ref GLOBAL_DATA: Mutex<Vec<i32>> = Mutex::new(vec![]);
}
fn main() {
GLOBAL_DATA.lock().unwrap().push(42);
println!("{:?}", GLOBAL_DATA.lock().unwrap());
}
这种方法的优点是:
- 代码更加简洁和易读。
- 初始化过程隐藏在宏中,降低了使用者的复杂度。
缺点是:
- 需要依赖第三方库,增加了项目的复杂度。
- 如果初始化过程非常昂贵,可能会影响性能。
使用 once_cell 创建全局单例
类似于 lazy_static
,once_cell
也是一个第三方库,提供了一种创建全局单例的简单方法。使用 once_cell
的例子如下:
use once_cell::sync::Lazy;
use std::sync::Mutex;
static GLOBAL_DATA: Lazy<Mutex<Vec<i32>>> = Lazy::new(|| Mutex::new(vec![]));
fn main() {
GLOBAL_DATA.lock().unwrap().push(42);
println!("{:?}", GLOBAL_DATA.lock().unwrap());
}
这种方法的优点和缺点与使用 lazy_static
类似。
特殊情况: 使用 std::sync::atomic::AtomicUsize 创建全局单例
如果你只需要跟踪一个整数值,可以直接使用 std::sync::atomic::AtomicUsize
。这种方法更加简单和高效,示例如下:
use std::sync::atomic::{AtomicUsize, Ordering};
static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
fn do_a_call() {
CALL_COUNT.fetch_add(1, Ordering::SeqCst);
}
fn main() {
do_a_call();
do_a_call();
do_a_call();
println!("called {}", CALL_COUNT.load(Ordering::SeqCst));
}
这种方法的优点是:
- 代码更加简单和高效。
- 适用于只需要跟踪整数值的情况。
缺点是:
- 只适用于整数值,不能用于更复杂的数据结构。
手动实现全局单例
除了使用现有的工具,我们也可以手动实现全局单例。这种方法可以更好地控制初始化过程,示例如下:
use std::sync::{Mutex, Once};
use std::time::Duration;
use std::{mem::MaybeUninit, thread};
struct SingletonReader {
inner: Mutex<Vec<u8>>,
}
fn singleton() -> &'static SingletonReader {
static mut SINGLETON: MaybeUninit<SingletonReader> = MaybeUninit::uninit();
static ONCE: Once = Once::new();
ONCE.call_once(|| {
let singleton = SingletonReader {
inner: Mutex::new(vec![]),
};
unsafe { SINGLETON.write(singleton); }
});
unsafe { SINGLETON.assume_init_ref() }
}
fn main() {
let threads: Vec<_> = (0..10)
.map(|i| {
thread::spawn(move || {
thread::sleep(Duration::from_millis(i * 10));
let s = singleton();
let mut data = s.inner.lock().unwrap();
*data = i as u8;
})
})
.collect();
for _ in 0..20 {
thread::sleep(Duration::from_millis(5));
let s = singleton();
let data = s.inner.lock().unwrap();
println!("It is: {}", *data);
}
for thread in threads {
thread.join().unwrap();
}
}
这种方法的优点是:
- 可以更好地控制初始化过程。
- 不需要依赖任何第三方库。
缺点是:
- 代码相对更加复杂。
- 需要更多的手动管理,增加了出错的风险。
关于”全局”的含义
需要注意的是,你仍然可以使用普通的 Rust 作用域和模块级别的隐私来控制对全局状态的访问。这意味着你可以在模块或函数内部声明它,这样它就不会被外部访问。这有助于控制访问:
lazy_static! {
static ref NAME: String = String::from("hello, world!");
}
fn only_here() {
println!("{}", &*NAME);
}
fn not_here() {
// println!("{}", &*NAME); // Error: `NAME` not found in this scope
}
尽管变量是全局的,但它仍然是在整个程序中唯一存在的。
总结
在 Rust 中,创建全局可变单例模式有多种方法。每种方法都有自己的优缺点,开发者需要根据具体需求进行权衡和选择。总的来说,避免使用全局状态是最好的做法,但在某些情况下,它可能是必要的。无论选择哪种方法,开发者都需要谨慎地考虑它的影响,并确保代码的安全性和可维护性。