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
Shared
orSharedMut
, 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
Send
thanks to theparking_lot
crate. - 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. static
Globally-scoped containers for bothSync
and!Sync
objects are easily constructed using [SharedGlobal
], and can provideShared
containers. Mutable global containers can similarly be constructed with [SharedGlobalMut
]. NOTE: This requires the--feature global_experimental
flag- The same primitives work in both synchronous and
async
contents (caveat: the latter being experimental at this time): you can simplyawait
an asynchronous version of the lock usingread_async
andwrite_async
. - Minimal performance impact: benchmarks shows approximately the same performance between the raw
parking_lot
primitives/tokio
async 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.
EXPERIMENTAL: Globals
NOTE: This requires the --feature global_experimental
flag
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.
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());
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 part of a type as another type.
- Projects a type as another type.
Structs
- Stores a read projection.
- Stores a read/write projection.
- The default
Shared
object is similar to Rust’sstd::sync::Arc
, but adds the ability to project.Shared
objects may also be constructed as aMutex
, or may be a read-only view into aSharedMut
. - The
SharedMut
object hides the complexity of managingArc<Mutex<T>>
orArc<RwLock<T>>
behind a single interface: - This holds a read lock on the underlying container’s object.
- This holds a write lock on the underlying container’s object.
Enums
- Determines what should happen if the underlying synchronization primitive is poisoned by being held during a
panic!
.