| Crates.io | keepcalm |
| lib.rs | keepcalm |
| version | 0.4.1 |
| created_at | 2023-03-02 18:58:56.89516+00 |
| updated_at | 2025-06-01 03:28:15.281229+00 |
| description | Simple shared types for multi-threaded programs |
| homepage | |
| repository | https://github.com/mmastrac/keepcalm |
| max_upload_size | |
| id | 799101 |
| size | 172,726 |
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.
This library simplifies a number of shared-object patterns that are used in multi-threaded programs such as web-servers.
Advantages of keepcalm:
Shared] or [SharedMut], no matter whether it's
a mutex, read/write lock, read/copy/update primitive, or a read-only shared [std::sync::Arc].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!Send thanks to the parking_lot crate.panic!s while the lock is being held). If you don't want to
poison on panic!, constructors are available to disable this option entirely.static Globally-scoped containers for both Sync and !Sync objects are easily constructed using [SharedGlobal], and can provide [Shared]
containers. Mutable global containers can similarly be constructed with [SharedGlobalMut].async contents (caveat: the latter being experimental at this time): you can simply await an asynchronous
version of the lock using read_async and write_async.parking_lot primitives/tokio async containers and those
in keepcalm.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 |
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. |
The traditional Rust shared object patterns tend to be somewhat verbose and repetitive, for example:
# use std::sync::{Arc, Mutex};
# fn use_string(s: &str) {}
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:
# use keepcalm::*;
# fn use_string(s: &str) {}
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]!
The [SharedMut] object hides the complexity of managing Arc<Mutex<T>>, Arc<RwLock<T>>, and other synchronization types
behind a single interface:
# use keepcalm::*;
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:
# use keepcalm::*;
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:
# use keepcalm::*;
let shared = SharedMut::new_with_policy("123".to_string(), PoisonPolicy::Ignore);
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.
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]:
# use keepcalm::*;
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]:
# use keepcalm::*;
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:
# use keepcalm::*;
# use std::collections::HashMap;
static GLOBAL_LAZY: SharedGlobalMut<HashMap<&str, usize>> =
SharedGlobalMut::new_lazy(|| HashMap::from_iter([("a", 1), ("b", 2)]));
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.
# use keepcalm::*;
# #[cfg(feature="async_experimental")]
static SPAWNER: Spawner = make_spawner!(tokio::task::spawn_blocking);
# #[cfg(feature="async_experimental")]
async fn get_locked_value(shared: Shared<usize>) -> usize {
*shared.read_async(&SPAWNER).await
}
# #[cfg(feature="async_experimental")]
{
let shared = Shared::new(1);
get_locked_value(shared);
}
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:
# use keepcalm::*;
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:
# use keepcalm::*;
#[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");
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:
# use keepcalm::*;
// 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:
# use keepcalm::*;
let boxed: Box<[i32]> = Box::new([1, 2, 3]);
let shared: SharedMut<[i32]> = SharedMut::from_box(boxed);