代码
rust
static DATA_MANAGER: Lazy<Mutex<DataManager>> = Lazy::new(|| Mutex::new(DataManager::new()));
这行代码实现了一个线程安全的全局单例模式,拆解如下:
-
static DATA_MANAGER--- 全局静态变量在整个程序运行期间只有一份
DATA_MANAGER,所有代码共享同一个实例。 -
Lazy<T>--- 惰性初始化(懒加载)once_cell::sync::Lazy保证初始化闭包|| Mutex::new(DataManager::new())只会在第一次被访问时执行一次,之后每次访问都返回已经初始化好的值。
- 不是程序启动时就创建,而是"用到时才创建"
- 只创建一次(线程安全),后续访问直接复用
-
Mutex<DataManager>--- 互斥锁Mutex(互斥锁)保证多线程环境下对DataManager的访问是互斥的------同一时刻只有一个线程能持有锁并操作数据,避免数据竞争(data race)。 -
DataManager::new()--- 实际数据真正的数据管理器实例,持有
movie_map、user_map、genre_reverse_index三张核心哈希表。
详细例子
假设有一个电影推荐系统 Web 服务,同时有 3 个用户请求进来,分别由 3 个线程处理:
时间线 →
线程A(查电影1) ──── 等锁 ──┬── 拿到锁 🔒 → 查movie_map → 释放锁 🔓
线程B(查用户5) ──── 等锁 ──┤ ├── 拿到锁 🔒 → 查user_map → 释放锁 🔓
线程C(查类型) ──── 等锁 ──┘ └── 拿到锁 🔒 → 查genre_index → 释放锁 🔓
首次访问(以 init_data_manager 为例)
rust
pub fn init_data_manager(base_path: &str) {
let mut dm = DATA_MANAGER.lock().unwrap(); // ① 第一次访问,触发 Lazy 初始化
// ② Lazy 执行闭包:Mutex::new(DataManager::new()),创建空的 movie_map、user_map、genre_reverse_index
// ③ lock() 获取锁,拿到可变引用 &mut DataManager
dm.load_data(&movie_path, &link_path, &rating_path, ...); // ④ 加载所有CSV数据
} // ⑤ dm 离开作用域,自动释放锁
// 之后任何线程再访问 DATA_MANAGER.lock(),Lazy 不会再初始化,直接返回已有的 Mutex
运行时查询(多线程并发场景)
rust
// 线程1:查询某部电影
fn handle_get_movie(movie_id: i32) {
let dm = DATA_MANAGER.lock().unwrap(); // 获取锁
let movie = dm.get_movie_by_id(movie_id); // 查询
println!("{:?}", movie);
} // 锁自动释放
// 线程2:按类型查询电影(同时进行,但需要排队等锁)
fn handle_get_by_genre(genre: &str) {
let dm = DATA_MANAGER.lock().unwrap(); // 等线程1释放锁后才能获取
let movies = dm.get_movies_by_genre(genre, 10, "rating");
println!("{:?}", movies);
}
这个模式解决了什么问题?
| 问题 | 解决方案 |
|---|---|
| 数据只加载一次(避免重复读取CSV文件) | Lazy 保证只初始化一次 |
| 多线程同时访问同一份数据 | Mutex 保证互斥访问 |
| 全局可访问(不需要到处传参) | static 全局变量 + get_data_manager() |
| 不使用时零开销 | Lazy 在首次访问前不占用资源 |
简单类比 :就像整个程序有一本共享的百科全书(电影、用户、评分数据),所有线程都能查阅,但同一时刻只允许一个人翻书,而且这本书只在第一次有人要看时才从仓库搬出来。