Crate state [−] [src]
state -- safe and effortless state management
This crate allows you to safely and effortlessly manage global and/or
thread-local state through set
and get
methods. State is set and
retrieved in a type-directed fashion: state
allows one instance of a
given type to be stored in global storage as well as n instances of a
given type to be stored in n thread-local-storage slots. This makes
state
ideal for singleton instances, global configuration, and
once-initialized state.
Usage
Include state
in your Cargo.toml
[dependencies]
:
[dependencies]
state = "0.1"
Thread-local state management is not enabled by default. You can enable it via the "tls" feature:
[dependencies]
state = { version = "0.1", features = ["tls"] }
Global State
Global state is set via the set function and retrieved via
the get function. The type of the value being set must be
thread-safe and transferable across thread boundaries. In other words, it
must satisfy Sync + Send + 'static
.
Example
Set and later retrieve a value of type T:
state::set(T::new()); state::get::<T>();
Thread-Local State
Thread-local state is set via the set_local function
and retrieved via the get_local function. The type of
the value being set must be transferable across thread boundaries but need
not be thread-safe. In other words, it must satisfy Send + 'static
but not
necessarily Sync
. Values retrieved from thread-local state are exactly
that: local to the current thread. As such, you cannot use thread-local
state to synchronize across multiple threads.
Thread-local state is initialized on an as-needed basis. The function used
to initialize the thread-local state is passed in as an argument to
set_local
. When the state is retrieved from a thread for the first time,
the function is executed to generate the initial value. The function is
executed at most once per thread. The same function is used for
initialization across all threads.
Note: Rust reuses thread IDs across multiple threads. This means that is possible to set thread-local state in thread A, have that thread die, start a new thread B, and access the state set in A in B.
Example
Set and later retrieve a value of type T:
state::set_local(|| T::new()); state::get_local::<T>();
Use Cases
state
is an optimal solution in several scenarios.
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.
Prior to state
, assuming you needed to setup the structure after program
start, your options were:
- Use a
static mut
andunsafe
to set anOption<Configuration>
toSome
. Retrieve by checking forSome
. - Use
lazy_static
with aRwLock
to set anOption<Configuration>
toSome
. Retrieve bylock
ing and checking forSome
.
With state
, you simply call state::set
and state::get
, as follows:
fn main() { let config = Configuration { /* fill in structure at run-time from user input */ }; // Make the config avaiable globally. state::set(config); /* at any point later in the program */ let config = state::get::<Configuration>(); }
Mutable, thread-local data
It is entirely safe to have an unsynchronized global object, as long as that
object is accessible to a single thread at a time: the standard library's
thread_local!
macro allows this behavior to be encapsulated. state
provides another, arguably simpler solution.
Say you want to count the number of invocations to a function per thread.
You store the invocations in a struct InvokeCount(Cell<usize>)
and use
invoke_count.0.set(invoke_count.0.get() + 1)
to increment the count. The
following implements this using state
:
struct InvokeCount(Cell<usize>); fn function_to_measure() { let count = state::get_local::<InvokeCount>(); count.0.set(count.0.get() + 1); } fn main() { // setup the initializer for thread-local state state::set_local(|| InvokeCount(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(|| { function_to_measure(); state::get_local::<InvokeCount>().0.get() })); } // retrieve the thread-local counts let counts: Vec<usize> = threads.into_iter() .map(|t| t.join().unwrap()) .collect(); }
Performance
state
is heavily tuned to perform near-optimally when there are many
threads. On average, state
performs slightly worse than lazy_static
when
only a single thread is used to access a global variable, and slightly
better than lazy_static
when many threads are used to access a global
variable. Keep in mind that state
allows global initialization at any
point in the program, while lazy_static
initialization must be declared
a priori. In other words, state
's abilities are a superset of those in
lazy_static
.
When To Use
You should avoid using state
as much as possible. Instead, prefer to pass
state manually through your program.
Functions
get |
Retrieves the global state for type |
set |
Sets the global state for type |
try_get |
Attempts to retrieve the global state for type |