Expand description
Safe, Effortless state
Management
This crate allows you to safely and effortlessly manage global and/or thread-local state. Three primitives are provided for state management:
TypeMap
: Type-based storage for many values.InitCell
: Thread-safe init-once storage for a single value.LocalInitCell
: Thread-local init-once-per-thread cell.
Usage
Include state
in your Cargo.toml
[dependencies]
:
[dependencies]
state = "0.6.0"
Thread-local state management is not enabled by default. You can enable it
via the tls
feature:
[dependencies]
state = { version = "0.6.0", features = ["tls"] }
Use Cases
Memoizing Expensive Operations
The InitCell
type can be used to conveniently memoize expensive
read-based operations without needing to mutably borrow. Consider a struct
with a field value
and method compute()
that performs an expensive
operation on value
to produce a derived value. We can use InitCell
to
memoize compute()
:
use state::InitCell;
struct Value;
struct DerivedValue;
struct Foo {
value: Value,
cached: InitCell<DerivedValue>
}
impl Foo {
fn set_value(&mut self, v: Value) {
self.value = v;
self.cached.reset();
}
fn compute(&self) -> &DerivedValue {
self.cached.get_or_init(|| {
let _value = &self.value;
unimplemented!("expensive computation with `self.value`")
})
}
}
Read-Only Singleton
Suppose you have the following structure which is initialized in main
after receiving input from the user:
struct Configuration {
name: String,
number: isize,
verbose: bool
}
fn main() {
let config = Configuration {
/* fill in structure at run-time from user input */
};
}
You’d like to access this structure later, at any point in the program,
without any synchronization overhead. Prior to state
, assuming you needed
to setup the structure after program start, your options were:
- Use
static mut
andunsafe
to set anOption<Configuration>
toSome
. Retrieve by checking forSome
. - Use
lazy_static
with aRwLock
to set anRwLock<Option<Configuration>>
toSome
. Retrieve bylock
ing and checking forSome
, paying the cost of synchronization.
With state
, you can use LocalInitCell
as follows:
static CONFIG: LocalInitCell<Configuration> = LocalInitCell::new();
fn main() {
CONFIG.set(|| Configuration {
/* fill in structure at run-time from user input */
});
/* at any point later in the program, in any thread */
let config = CONFIG.get();
}
Note that you can also use InitCell
to the same effect.
Read/Write Singleton
Following from the previous example, let’s now say that we want to be able
to modify our singleton Configuration
structure as the program evolves. We
have two options:
- If we want to maintain the same state in any thread, we can use a
InitCell
structure and wrap ourConfiguration
structure in a synchronization primitive. - If we want to maintain different state in any thread, we can continue
to use a
LocalInitCell
structure and wrap ourLocalInitCell
type in aCell
structure for internal mutability.
In this example, we’ll choose 1. The next example illustrates an instance of 2.
The following implements 1 by using a InitCell
structure and wrapping
the Configuration
type with a RwLock
:
static CONFIG: InitCell<RwLock<Configuration>> = InitCell::new();
fn main() {
let config = Configuration {
/* fill in structure at run-time from user input */
};
// Make the config avaiable globally.
CONFIG.set(RwLock::new(config));
/* at any point later in the program, in any thread */
let mut_config = CONFIG.get().write();
}
Mutable, thread-local data
Imagine you want to count the number of invocations to a function per
thread. You’d like to store the count in a Cell<usize>
and use
count.set(count.get() + 1)
to increment the count. Prior to state
, your
only option was to use the thread_local!
macro. state
provides a more
flexible, and arguably simpler solution via LocalInitCell
. This scanario
is implemented in the folloiwng:
static COUNT: LocalInitCell<Cell<usize>> = LocalInitCell::new();
fn function_to_measure() {
let count = COUNT.get();
count.set(count.get() + 1);
}
fn main() {
// setup the initializer for thread-local state
COUNT.set(|| Cell::new(0));
// spin up many threads that call `function_to_measure`.
let mut threads = vec![];
for i in 0..10 {
threads.push(thread::spawn(|| {
// Thread IDs may be reusued, so we reset the state.
COUNT.get().set(0);
function_to_measure();
COUNT.get().get()
}));
}
// retrieve the total
let total: usize = threads.into_iter()
.map(|t| t.join().unwrap())
.sum();
assert_eq!(total, 10);
}
Correctness
state
has been extensively vetted, manually and automatically, for soundness
and correctness. All unsafe code, including in internal concurrency
primitives, TypeMap
, and InitCell
are exhaustively verified for pairwise
concurrency correctness and internal aliasing exclusion with loom
.
Multithreading invariants, aliasing invariants, and other soundness properties
are verified with miri
. Verification is run by the CI on every commit.
Performance
state
is heavily tuned to perform optimally. get{_local}
and
set{_local}
calls to a TypeMap
incur overhead due to type lookup.
InitCell
, on the other hand, is optimal for global storage retrieval; it is
slightly faster than accessing global state initialized through
lazy_static!
, more so across many threads. LocalInitCell
incurs slight
overhead due to thread lookup. However, LocalInitCell
has no
synchronization overhead, so retrieval from LocalInitCell
is faster than
through InitCell
across many threads.
Bear in mind that state
allows global initialization at any point in the
program. Other solutions, such as lazy_static!
and thread_local!
allow
initialization only a priori. In other words, state
’s abilities are a
superset of those provided by lazy_static!
and thread_local!
while being
more performant.
When To Use
You should avoid using global state
as much as possible. Instead, thread
state manually throughout your program when feasible.
Macros
- Type constructor for
TypeMap
variants.
Structs
- An init-once cell for global access to a value.
- A thread-local init-once-per-thread cell for thread-local values.
- A type map storing values based on types.