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.
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()
}
}
}
The constructor new is private, ensuring that instances of Singleton cannot be created directly. This is essential to enforce the singleton property.
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.
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.
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.
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.
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");
}
}