Design Pattern - Singleton (TypeScript & Rust)

ayou - Mar 8 - - Dev Community

The singleton pattern has many applications in software development, such as a database connection pool, a globally unique dialog, a global logging tool, and more.

TypeScript

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
Enter fullscreen mode Exit fullscreen mode

The implementation in TypeScript is as mentioned above, relatively simple. Now let's see how to implement it in Rust.

Rust

After learning Rust for a day, it should be very easy to write the following code:

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);
Enter fullscreen mode Exit fullscreen mode

However, the code above is obvious invalid, as the Singleton struct does not have any parameter. Let's modify it by adding a parameter to Singleton:

pub struct Singleton {
    data: String,
}

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

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

However, it failed to compile:

   |
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
Enter fullscreen mode Exit fullscreen mode

So let's follow its prompt and try using the once_cell library, and add an init method for external data initialization.


#[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
Enter fullscreen mode Exit fullscreen mode

The compilation passed, but it clearly does not meet the requirements because the value of data is set to the parameter passed to by the last calling of init method, which is 'world'. Continuing to look at the documentation for once_cell, I found that it provides set and get methods, which can be used to store and access the instance of Singleton, and the instantiation code can be moved to the get_instance method:

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
Enter fullscreen mode Exit fullscreen mode

It seems to work again, but don't get too excited. If we switch to the following test code, there will be a problem:

    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());
Enter fullscreen mode Exit fullscreen mode
Failed to set: Singleton { data: "hello0" }Failed to set: Singleton { data: "hello1" }
Enter fullscreen mode Exit fullscreen mode

The reason is that the statement INSTANCE.get().is_none() is not thread-safe. Multiple threads may enter the code block inside the if statement:

if (INSTANCE.get().is_none()) {

}
Enter fullscreen mode Exit fullscreen mode

Let's add a lock then:

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

Now it can work properly again. Moreover, we can use std::sync::Once to make our code simpler:

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()
}

Enter fullscreen mode Exit fullscreen mode

It's OK now? No! In certain scenarios, I need the Singleton instance to be thread-exclusive. For example, consider the following requirements:

pub fn log(&self, thread_name: String) {
    println!("{}: step1", thread_name);
    println!("{}: step2", thread_name);
}
Enter fullscreen mode Exit fullscreen mode

When I invoke the log method in a particular thread, it is necessary to print step1 and step2 consecutively before printing logs from other threads. In this case, the aforementioned code cannot support this requirement. Therefore, it is necessary to lock the entire Singleton instance:

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()
}

Enter fullscreen mode Exit fullscreen mode

With this implementation, a multi-threaded singleton is achieved. The complete code is as follows:

// 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());
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player