设计模式之单例模式(TypeScript & Rust)

单例模式在软件开发中有很多应用场景,比如数据库连接池、全局唯一的对话框、全局日志记录等。

TypeScript 单例模式

ts 复制代码
class Singleton {
  private static instance: Singleton
  private data: string

  private constructor(data: string) {
    this.data = data
  }

  public static getInstance(data: string): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton(data)
    }
    return Singleton.instance
  }

  public getData(): string {
    return this.data
  }
}

const instance1 = Singleton.getInstance('hello')
const instance2 = Singleton.getInstance('world')

console.log(instance1 === instance2) // true
console.log(instance1.getData()) // hello
console.log(instance2.getData()) // hello

TypeScript 实现方式如上,比较简单,接下来看 Rust 怎么实现。

Rust 单例模式

学习 Rust 一天后,写出下面这个版本应该很简单:

rust 复制代码
pub struct Singleton {}

static INSTANCE: &Singleton = &Singleton {};

pub fn get_instance() -> &'static Singleton {
    INSTANCE
}

let instance1 = get_instance();
let instance2 = get_instance();
println!("{:p} {:p}", instance1, instance2);

不过上面的代码明显不能用,Singleton 根本没有任何参数。那我们来修改一下,给 Singleton 加个参数:

rust 复制代码
pub struct Singleton {
    data: String,
}

static mut INSTANCE: &mut Singleton = &mut Singleton {
    data: "".to_string(),
};

pub fn get_instance() -> &'static mut Singleton {
    unsafe { INSTANCE }
}

结果确发现报错了:

javascript 复制代码
   |
40 |     data: "".to_string(),
   |              ^^^^^^^^^^^
   |
   = note: calls in statics are limited to constant functions, tuple structs and tuple variants
   = note: consider wrapping this expression in `Lazy::new(|| ...)` from the `once_cell` crate: https://crates.io/crates/once_cell

那就按照它的提示使用 once_cell 这个库再试一下,同时加个 init 方法给外部初始化数据用:

rust 复制代码
#[derive(Default)]
pub struct Singleton {
    data: String,
}

impl Singleton {
    pub fn init(&mut self, data: String) {
        self.data = data
    }

    pub fn get_data(&self) -> &str {
        self.data.as_str()
    }
}

static mut INSTANCE: Lazy<Singleton> = Lazy::new(|| Singleton::default());

pub fn get_instance() -> &'static mut Singleton {
    unsafe { INSTANCE.deref_mut() }
}

let mut instance1 = get_instance();
let mut instance2 = get_instance();
instance1.init("hello".to_string());
instance2.init("world".to_string());
let d1 = instance1.get_data();
let d2 = instance2.get_data();
println!("{} {}", d1, d2); // world world

编译通过了,不过这个明显不符合要求,因为最后 data 的值是取最后调用 init 方法所传的参数 world

继续看 OnceCell 的文档,发现它提供了 setget 方法,那就可以用来存储和访问 SingleTon 的实例,且实例化的代码可以挪到 get_instance 中去了:

rust 复制代码
use once_cell::sync::{OnceCell};

#[derive(Default, Debug)]
pub struct Singleton {
    data: String,
}

impl Singleton {
    pub fn new(data: String) -> Self {
        Self { data }
    }

    pub fn get_data(&self) -> &str {
        self.data.as_str()
    }
}

static INSTANCE: OnceCell<Singleton> = OnceCell::new();
pub fn get_instance(data: String) -> &'static Singleton {
    unsafe {
        if (INSTANCE.get().is_none()) {
            let mut instance = Singleton::new(data);
            INSTANCE.set(instance).expect("Failed to set");
        }

        &INSTANCE.get().unwrap()
    }
}

let mut instance1 = get_instance();
let mut instance2 = get_instance();
instance1.init("hello".to_string());
instance2.init("world".to_string());
let d1 = instance1.get_data();
let d2 = instance2.get_data();
println!("{} {}", d1, d2); // hello hello

似乎又成了,不过别高兴的太早,如果换成如下测试代码,就有问题了:

rust 复制代码
    let threads: Vec<_> = (0..10)
        .map(|i| {
            spawn(move || {
                get_instance(format!("hello{}", i));
            })
        })
        .collect();

    for handle in threads {
        handle.join().unwrap();
    }

    let instance = get_instance("".to_string());
    println!("{}", instance.get_data());
css 复制代码
Failed to set: Singleton { data: "hello0" }Failed to set: Singleton { data: "hello1" }

原因在于 INSTANCE.get().is_none() 这条语句不是线程安全的,多个线程可能都能进入 if 的代码块:

rust 复制代码
if (INSTANCE.get().is_none()) {

}

那我们来加个锁吧:

rust 复制代码
static mut INITIALIZED: Mutex<bool> = Mutex::new(false);
static INSTANCE: OnceCell<Singleton> = OnceCell::new();
pub fn get_instance(data: String) -> &'static Singleton {
    unsafe {
        let mut a = INITIALIZED.lock().unwrap();
        if (!*a) {
            let instance = Singleton::new(data);
            INSTANCE.set(instance).expect("Failed to set");
            *a = true;
        }

        &INSTANCE.get().unwrap()
    }
}

并且,使用 std::sync::Once 可以让我们的代码更简单:

rust 复制代码
static INIT: Once = Once::new();
static INSTANCE: OnceCell<Singleton> = OnceCell::new();

pub fn get_instance(data: String) -> &'static Singleton {
    INIT.call_once(|| {
        let instance = Singleton::new(data);
        INSTANCE.set(instance).expect("Fail to set");
    });

    INSTANCE.get().unwrap()
}

没问题了?No!某些场景下,我需要让 Singleton 实例是线程互斥的,比如有如下需求:

rust 复制代码
pub fn log(&self, thread_name: String) {
    println!("{}: step1", thread_name);
    println!("{}: step2", thread_name);
}

当我在某个线程下调用 log 方法时,必须要连续打印完 step1step2 才能打印别的线程的日志,此时上述的代码就不能支持了。此时,需要给整个 Singleton 实例加锁:

rust 复制代码
static INSTANCE: OnceCell<Mutex<Singleton>> = OnceCell::new();

pub fn get_instance(data: String) -> MutexGuard<'static, Singleton> {
    INIT.call_once(|| {
        let instance = Mutex::new(Singleton::new(data));
        INSTANCE.set(instance).expect("Fail to set instance");
    });

    INSTANCE.get().unwrap().lock().unwrap()
}

这样一个支持多线程的单例就实现了,完整代码如下:

rust 复制代码
// singleton.rs
use once_cell::sync::OnceCell;
use std::sync::{Mutex, MutexGuard, Once};

#[derive(Debug)]
pub struct Singleton {
    data: String,
}

impl Singleton {
    fn new(data: String) -> Self {
        Self { data }
    }

    pub fn get_data(&self) -> &str {
        &self.data
    }
    pub fn log(&self, thread_name: String) {
        println!("{}: step1", thread_name);
        println!("{}: step2", thread_name);
    }
}

static INIT: Once = Once::new();
static INSTANCE: OnceCell<Mutex<Singleton>> = OnceCell::new();

pub fn get_instance(data: String) -> MutexGuard<'static, Singleton> {
    INIT.call_once(|| {
        let instance = Mutex::new(Singleton::new(data));
        INSTANCE.set(instance).expect("Fail to set instance");
    });

    INSTANCE.get().unwrap().lock().unwrap()
}

// main.rs
let threads: Vec<_> = (0..10)
    .map(|i| {
        spawn(move || {
            let instance = get_instance(format!("hello{}", i));
            instance.log(format!("thread{}", i));
        })
    })
    .collect();

for handle in threads {
    handle.join().unwrap();
}

let instance = get_instance("".to_string());
println!("{}", instance.get_data());
相关推荐
SomeB1oody1 天前
【Rust自学】4.1. 所有权:栈内存 vs. 堆内存
开发语言·后端·rust
SomeB1oody2 天前
【Rust自学】4.2. 所有权规则、内存与分配
开发语言·后端·rust
SomeB1oody2 天前
【Rust自学】4.5. 切片(Slice)
开发语言·后端·rust
编码浪子2 天前
构建一个rust生产应用读书笔记6-拒绝无效订阅者02
开发语言·后端·rust
baiyu332 天前
1小时放弃Rust(1): Hello-World
rust
baiyu332 天前
1小时放弃Rust(2): 两数之和
rust
Source.Liu2 天前
数据特性库 前言
rust·cad·num-traits
编码浪子2 天前
构建一个rust生产应用读书笔记7-确认邮件1
数据库·rust·php
SomeB1oody2 天前
【Rust自学】3.6. 控制流:循环
开发语言·后端·rust
Andrew_Ryan2 天前
深入了解 Rust 核心开发团队:这些人如何塑造了世界上最安全的编程语言
rust