Expand description
§Keep Calm (and call Clone)
Simple shared types for multi-threaded Rust programs: keepcalm gives you permission to simplify your synchronization code in concurrent Rust applications.
Name inspired by @luser’s Keep Calm and Call Clone.
§Overview
This library simplifies a number of shared-object patterns that are used in multi-threaded programs such as web-servers.
Advantages of keepcalm:
- You don’t need to decide on your synchronization primitives up-front. Everything is a
SharedorSharedMut, no matter whether it’s a mutex, read/write lock, read/copy/update primitive, or a read-only sharedstd::sync::Arc. - Everything is
project!able, which means you can adjust the granularity of your locks at any time without having to refactor the whole system. If you want finer-grained locks at a later date, the code that uses the shared containers doesn’t change! - Writeable containers can be turned into read-only containers, while still retaining the ability for other code to update the contents.
- Read and write guards are
Sendthanks to theparking_lotcrate. - Each synchronization primitive transparently manages the poisoned state (if code
panic!s while the lock is being held). If you don’t want to poison onpanic!, constructors are available to disable this option entirely. staticGlobally-scoped containers for bothSyncand!Syncobjects are easily constructed usingSharedGlobal, and can provideSharedcontainers. Mutable global containers can similarly be constructed withSharedGlobalMut.- The same primitives work in both synchronous and
asynccontents (caveat: the latter being experimental at this time): you can simplyawaitan asynchronous version of the lock usingread_asyncandwrite_async. - Minimal performance impact: benchmarks shows approximately the same performance between the raw
parking_lotprimitives/tokioasync containers and those inkeepcalm.
§Performance
A rough benchmark shows approximately equivalent performance to both tokio and parking_lot primitives in async and sync contexts. While
keepcalm shows performance slightly faster than parking_lot in some cases, this is probably measurement noise.
| Benchmark | keepcalm | tokio | parking_lot |
|---|---|---|---|
| Mutex (async, uncontended) | 23ns | 49ns | n/a |
| Mutex (async, contented) | 1.3ms | 1.3ms | n/a |
| RwLock (async, uncontended) | 14ns | 46ns | n/a |
| RwLock (async, contended) | (untested) | (untested) | (untested) |
| RwLock (sync) | 6.8ns | n/a | (untested) |
| Mutex (sync) | 7.3ns | n/a | 8.5ns |
§Container types
The following container types are available:
| Container | Equivalent | Notes |
|---|---|---|
SharedMut::new | Arc<RwLock<T>> | This is the default shared-mutable type. |
SharedMut::new_mutex | Arc<Mutex<T>> | In some cases it may be necessary to serialize both read and writes. For example, with types that are not Sync. |
SharedMut::new_rcu | Arc<RwLock<Arc<T> | When the write lock of an RCU container is dropped, the values written are committed to the value in the container. |
Shared::new | Arc | This is the default shared-immutable type. Note that this is slightly more verbose: Shared does not std::ops::Deref to the underlying type and requires calling Shared::read. |
Shared::new_mutex | Arc<Mutex<T>> | For types that are not Sync, a Mutex is used to serialize read-only access. |
SharedMut::shared | n/a | This provides a read-only view into a read-write container and has no direct equivalent. |
The following global container types are available:
| Container | Equivalent | Notes |
|---|---|---|
SharedGlobal::new | static T | This is a global const-style object, for types that are Send + Sync. |
SharedGlobal::new_lazy | static Lazy<T> | This is a lazily-initialized global const-style object, for types that are Send + Sync. |
SharedGlobal::new_mutex | static Mutex<T> | This is a global const-style object, for types that are Send but not necessarily Sync |
SharedGlobalMut::new | static RwLock<T> | This is a global mutable object, for types that are Send + Sync. |
SharedGlobalMut::new_lazy | static Lazy<RwLock<T>> | This is a lazily-initialized global mutable object, for types that are Send + Sync. |
SharedGlobalMut::new_mutex | static Mutex<T> | This is a global mutable object, for types that are Send but not necessarily Sync. |
§Basic syntax
The traditional Rust shared object patterns tend to be somewhat verbose and repetitive, for example:
struct Foo {
my_string: Arc<Mutex<String>>,
my_integer: Arc<Mutex<u16>>,
}
let foo = Foo {
my_string: Arc::new(Mutex::new("123".to_string())),
my_integer: Arc::new(Mutex::new(1)),
};
use_string(&*foo.my_string.lock().expect("Mutex was poisoned"));If we want to switch our shared fields from std::sync::Mutex to std::sync::RwLock, we need to change four lines just for types, and
switch the lock method for a read method.
We can increase flexibility, and reduce some of the ceremony and verbosity with keepcalm:
struct Foo {
my_string: SharedMut<String>,
my_integer: SharedMut<u16>,
}
let foo = Foo {
my_string: SharedMut::new("123".to_string()),
my_integer: SharedMut::new(1),
};
use_string(&*foo.my_string.read());If we want to use a Mutex instead of the default RwLock that SharedMut uses under the hood, we only need to change SharedMut::new to
SharedMut::new_mutex!
§SharedMut
The SharedMut object hides the complexity of managing Arc<Mutex<T>>, Arc<RwLock<T>>, and other synchronization types
behind a single interface:
let object = "123".to_string();
let shared = SharedMut::new(object);
shared.read();By default, a SharedMut object uses Arc<RwLock<T>> under the hood, but you can choose the synchronization primitive at
construction time. The SharedMut object erases the underlying primitive and you can use them interchangeably:
fn use_shared(shared: SharedMut<String>) {
shared.read();
}
let shared = SharedMut::new("123".to_string());
use_shared(shared);
let shared = SharedMut::new_mutex("123".to_string());
use_shared(shared);Managing the poison state of synchronization primitives can be challenging as well. Rust will poison a Mutex or RwLock if you
hold a lock while a panic! occurs.
The SharedMut type allows you to specify a PoisonPolicy at construction time. By default, if a synchronization
primitive is poisoned, the SharedMut will panic! on access. This can be configured so that poisoning is ignored:
let shared = SharedMut::new_with_policy("123".to_string(), PoisonPolicy::Ignore);§Shared
The default Shared object is similar to Rust’s std::sync::Arc, but adds the ability to project. Shared objects may also be
constructed as a Mutex, or may be a read-only view into a SharedMut.
Note that because of this flexibility, the Shared object is slightly more complex than a traditional std::sync::Arc, as all accesses
must be performed through the Shared::read accessor.
§Globals
While static globals may often be an anti-pattern in Rust, this library also offers easily-to-use alternatives that are compatible with
the Shared and SharedMut types.
Global Shared references can be created using SharedGlobal:
static GLOBAL: SharedGlobal<usize> = SharedGlobal::new(1);
fn use_global() {
assert_eq!(GLOBAL.read(), 1);
// ... or ...
let shared: Shared<usize> = GLOBAL.shared();
assert_eq!(shared.read(), 1);
}Similarly, global SharedMut references can be created using SharedGlobalMut:
static GLOBAL: SharedGlobalMut<usize> = SharedGlobalMut::new(1);
fn use_global() {
*GLOBAL.write() = 12;
assert_eq!(GLOBAL.read(), 12);
// ... or ...
let shared: SharedMut<usize> = GLOBAL.shared_mut();
*shared.write() = 12;
assert_eq!(shared.read(), 12);
}Both SharedGlobal and SharedGlobalMut offer a new_lazy constructor that allows initialization to be deferred to first
access:
static GLOBAL_LAZY: SharedGlobalMut<HashMap<&str, usize>> =
SharedGlobalMut::new_lazy(|| HashMap::from_iter([("a", 1), ("b", 2)]));§EXPERIMENTAL: Async
NOTE: This requires the --feature async_experimental flag
This is extremely experimental and may have soundness and/or performance issues!
The Shared and SharedMut types support a read_async and write_async method that will block using an async runtime’s spawn_blocking
method (or equivalent). Create a [Spawner] using make_spawner and pass that to the appropriate lock method.
Note that this relies on an async runtime to provide a blocking task thread-pool, so this may not be suitable for all use-cases.
static SPAWNER: Spawner = make_spawner!(tokio::task::spawn_blocking);
async fn get_locked_value(shared: Shared<usize>) -> usize {
*shared.read_async(&SPAWNER).await
}
{
let shared = Shared::new(1);
get_locked_value(shared);
}§Projection
Both Shared and SharedMut allow projection into the underlying type. Projection can be used to select
either a subset of a type, or to cast a type to a trait. The project! and project_cast! macros can simplify
this code.
Note that projections are always linked to the root object! If a projection is locked, the root object is locked.
Casting:
let shared = SharedMut::new("123".to_string());
// Supported for most built-in traits
let shared_asref: SharedMut<dyn AsRef<str>> = shared.cast();
// Any trait may be projected using `project_cast!`
let shared_asref: SharedMut<dyn AsRef<str>> = shared.project(project_cast!(x: String => dyn AsRef<str>));Subset of a struct/tuple:
#[derive(Default)]
struct Foo {
tuple: (String, usize)
}
let shared = SharedMut::new(Foo::default());
let shared_string: SharedMut<String> = shared.project(project!(x: Foo, x.tuple.0));
*shared_string.write() += "hello, world";
assert_eq!(shared.read().tuple.0, "hello, world");
assert_eq!(*shared_string.read(), "hello, world");§Unsized types
Both Shared and SharedMut support unsized types, but due to current limitations in the language (see std::ops::CoerceUnsized for details),
you need to construct them in special ways.
Unsized traits are supported, but you will either need to specify Send + Sync in the shared type, or project_cast! the object:
// In this form, `Send + Sync` are visible in the shared type
let boxed: Box<dyn AsRef<str> + Send + Sync> = Box::new("123".to_string());
let shared: SharedMut<dyn AsRef<str> + Send + Sync> = SharedMut::from_box(boxed);
// In this form, `Send + Sync` are erased via projection
let shared = SharedMut::new("123".to_string());
let shared_asref: SharedMut<dyn AsRef<str>> = shared.project(project_cast!(x: String => dyn AsRef<str>));Unsized slices are supported using a box:
let boxed: Box<[i32]> = Box::new([1, 2, 3]);
let shared: SharedMut<[i32]> = SharedMut::from_box(boxed);Macros§
- project
- Project part of a type as another type.
- project_
cast - Projects a type as another type.
Structs§
- Projector
- Stores a read projection.
- ProjectorRW
- Stores a read/write projection.
- Shared
- The default
Sharedobject is similar to Rust’sstd::sync::Arc, but adds the ability to project.Sharedobjects may also be constructed as aMutex, or may be a read-only view into aSharedMut. - Shared
Global - A global version of
Shared. UseSharedGlobal::sharedto get aSharedto access the contents. - Shared
Global Mut - A global version of
SharedMut. UseSharedGlobalMut::sharedto get aSharedto access the contents, orSharedGlobalMut::shared_mutto get aSharedMut. - Shared
Mut - The
SharedMutobject hides the complexity of managingArc<Mutex<T>>orArc<RwLock<T>>behind a single interface: - Shared
Read Lock - This holds a read lock on the underlying container’s object.
- Shared
Write Lock - This holds a write lock on the underlying container’s object.
Enums§
- Poison
Policy - Determines what should happen if the underlying synchronization primitive is poisoned by being held during
a
panic!.
Traits§
- Castable
- A trait that allows a type to be cast to another, generically unsized type.