Design Patterns in Rust: Singleton, a unique way of creating objects in a threadsafe way

Photo by Damir: https://www.pexels.com/photo/a-rock-formation-at-ales-stenar-standing-stones-in-scania-sweden-14288610/

Introduction

The singleton pattern restricts the instantiation of a class to a single instance. The singleton pattern makes it possible to ensure that:

  1. The creation of a class is controlled (in some languages like C# or Java this is done by making the constructors private)
  2. The one instance we have is easily accessible
  3. And the singleton pattern helps us to ensure we only have at most one instance of a class.

One of the main uses of the singleton pattern is logging, as all clients who wish to log need a single point of entrance.

The UML diagram of this pattern is very simple:

  • a private constructor, or a static constructor. In the constructor, a check is done to see if there is an instance of the Singleton. If there is, that instance is returned, if not a new one is made, stored and returned
  • It contains an instance of itself, in some languages this is stored in a private field.
  • The getInstance() method is used to get the single instance. If there is no instance yet, the constructor is called and a new instance is made.

Implementation in Rust

In an empty directory open your terminal or commandline, and type:

cargo new rust_singleton
cd rust_singleton

The Singleton struct

The Singleton struct in itself is pretty simple:

struct Singleton {
    data: i32,
}

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

    fn get_data(&self)->i32 {
        self.data
    }
}

It just contains a constructor and a get_data() method to access the contained data.

The lazy_static! macro

Next comes an expression which requires some explanation:

lazy_static! {
    static ref SINGLETON: Mutex<Singleton> = Mutex::new(Singleton::new());
}
  • The static keyword: this according to the docs is an item which is valid for the entire runtime of the program.
  • Since static references can only be initialized with constant functions and values, the lazy_static! macro comes into play, which allows for non-constants to initialize a static value and that is exactly what we need.
  • We would like our singleton to be threadsafe, that is, if one thread is accessing it. other threads. For that we use the Mutex struct.

This is basically the trickiest bit of this pattern, but still the code is quite elegant. The lazy_static! reduces the boiler-plate code we might have to write

Test time!

Now we can test the code in the main function:

fn main() {
    let singleton = SINGLETON.lock().unwrap();
    println!("Data: {}", singleton.get_data());
    
    MutexGuard::unlock(singleton);

    let other=SINGLETON.lock().unwrap();
    println!("Data: {}", other.get_data());
}

A line by line breakdown:

  1. We access our SINGLETON variable, lock it and unwrap it, so we can access it. The singleton variable is of type MutexGuard<Singleton>.
  2. Then we print the value of our singleton.
  3. We want to test whether we have only one instance, so we need to unlock the Mutex, with the unlock method. Forget this line and your program will stall indefinitely or time out.
  4. Next we access the singleton instance again, and lo and behold we get the same value back.

Conclusion

Even though the Singleton is possibly the simplest of all design patterns, it was not very straightforward to implement it. The difficulties were mainly due to the fact that we needed to make it threadsafe. Rust thankfully provides us with some elegant methods to do that.

One caveat with this approach is that forgetting to unlock the MutexGuard, you can stall a program or a thread which can lead to some unexpected errors.

My advice would be, and that is also based on years of experience as a developer in other languages is to use the Singleton sparingly.

Leave a Reply

Your email address will not be published. Required fields are marked *