Design Patterns: Singleton

I am in the process of revisiting some fundamental knowledge while I am learning Rust and I wanted to write a series of articles about design patterns, starting with Singleton.

singleton

The Singleton pattern is a well-known design pattern in software development, aiming to ensure that a class has only one instance and provides a global point of access to it. This pattern is particularly useful when exactly one object is needed to coordinate actions across the system. Today, we’ll explore how to implement this pattern in Rust, ensuring thread safety and laziness. We’ll leverage Rust’s concurrency primitives like Arc, Mutex, and Once.

The code can be found in Github

Rust doesnt have the concept of class, it has structs. So we will create a Singleton struct, that will hold some data, I chose String for the shake of simplicity.

struct Singleton {
    data: String,
}

We want to prevent more than one instances of the Singleton to be created and provide a different mechanism than new to create the only instance. For this reason we mark the new as private (aka we do not declare it as public since in Rust by default the members of a struct are private).

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

    pub fn instance(data: String) -> Arc<Mutex<Self>> {
        static mut SINGLETON: Option<Arc<Mutex<Singleton>>> = None;
        static ONCE: Once = Once::new();

        /*
        The operation is considered unsafe because it involves modifying a static mutable variable.
        In Rust, mutable static variables are inherently unsafe due to potential data races and
        undefined behavior when accessed from multiple threads.
        The unsafe block is required to signal that you, as the programmer, are aware of these risks
        and have taken steps to ensure safety.
        In this case, the use of Once ensures that the initialization happens only once,
        which mitigates some of the risks, but Rust still requires the unsafe
        block to acknowledge the potential danger.
        */
        unsafe {
            ONCE.call_once(|| {
                let singleton = Singleton::new(data);

                SINGLETON = Some(Arc::new(Mutex::new(singleton)));
            });
            SINGLETON.clone().unwrap()
        }
    }
}

Breaking Down the Code

Private Constructor

The constructor new is private, ensuring that instances of Singleton cannot be created directly. This is essential to enforce the singleton property.

Static Variables

SINGLETON: A static mutable variable that holds the singleton instance. It’s wrapped in an Option and initialized to None.

ONCE: A Once instance ensures that the initialization code inside it runs only once, even when accessed by multiple threads simultaneously.

instance Method

The instance method provides access to the singleton instance. This method is responsible for initializing the singleton if it hasn’t been created yet and returning the existing instance otherwise.

Why Unsafe?

The use of the unsafe keyword may seem odd in first glance, but is necessary because we are modifying a static mutable variable. Rust enforces strict rules around mutable statics to prevent data races. By using Once, we mitigate some risks, but Rust still requires the unsafe block to acknowledge the potential danger.

Arc and Mutex

In our Singleton pattern implementation, we use Arc and Mutex to ensure thread safety and manage shared access to the singleton instance. Arc allows multiple threads to share ownership of the singleton instance by keeping a reference count and ensuring that the singleton is only dropped when the last reference goes out of scope. This is crucial for thread-safe reference counting in a concurrent environment. On the other hand, Mutex ensures that only one thread can access or modify the singleton’s data at any given time, preventing data races and ensuring safe, synchronized access to the shared resource. By combining Arc and Mutex, we achieve a thread-safe, lazily-initialized singleton that multiple threads can safely access and modify.

Tests

In the tests we create to instances of the Singleton, we test that they are the same and we verify that the data hasn’t changed (aka “Second” is ignored).

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_singleton_instance() {
        let instance1 = Singleton::instance("First".to_string());
        let instance2 = Singleton::instance("Second".to_string());
    
        // Both instances should be the same
        assert!(Arc::ptr_eq(&instance1, &instance2));
    
        // The data should be "First" because the singleton is initialized only once
        let data = instance1.lock().unwrap().data.clone();
        assert_eq!(data, "First");
    }
}
Published 7 Jun 2024

Tüftler (someone who enjoys working on and solving technical problems, often in a meticulous and innovative manner). Opinions are my own and not necessarily the views of my employer.
Avraam Mavridis on Twitter