Simple shared types for multi-threaded Rust programs

Overview

Keep Calm (and call Clone)

Build Status docs.rs crates.io

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] or [SharedMut], no matter whether it's a mutex, read/write lock, read/copy/update primitive, or a read-only shared [std::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 the parking_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 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]. 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 simply await an asynchronous version of the lock using read_async and write_async.
  • Minimal performance impact: benchmarks shows approximately the same performance between the raw parking_lot primitives/tokio async containers and those in keepcalm.

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:

# 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]!

SharedMut

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);

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]:

# use keepcalm::*;
# #[cfg(feature="global_experimental")]
static GLOBAL: SharedGlobal<usize> = SharedGlobal::new(1);

# #[cfg(feature="global_experimental")]
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::*;
# #[cfg(feature="global_experimental")]
static GLOBAL: SharedGlobalMut<usize> = SharedGlobalMut::new(1);

# #[cfg(feature="global_experimental")]
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;
# #[cfg(feature="global_experimental")]
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.

# use keepcalm::*;
# #[cfg(feature="global_experimental")]
static SPAWNER: Spawner = make_spawner!(tokio::task::spawn_blocking);

# #[cfg(feature="global_experimental")]
async fn get_locked_value(shared: Shared<usize>) -> usize {
    *shared.read_async(&SPAWNER).await
}

# #[cfg(feature="global_experimental")]
{
    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:

# use keepcalm::*;
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:

# 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");

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:

# 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);
You might also like...
Synchronized shadow state of Solana programs available for off-chain processing.

Solana Shadow The Solana Shadow crate adds shadows to solana on-chain accounts for off-chain processing. This create synchronises all accounts and the

A collection of Solana-maintained on-chain programs

Solana Program Library The Solana Program Library (SPL) is a collection of on-chain programs targeting the Sealevel parallel runtime. These programs a

Deploy your Solana programs during high load.

solana-deployer IMPORTANT: There is a known bug with the current program that will be fixed soon. In the meantime you should deploy from Solana Playgr

CLI tool for deterministically building and verifying executable against on-chain programs or buffer accounts

Solana Verify CLI A command line tool to build and verify solana programs. Users can ensure that the hash of the on-chain program matches the hash of

Write Anchor-compatible Solana programs in TypeScript

Axolotl Write Achor-compatible Solana programs using TypeScript. Writing Rust is hard, but safe. It's also the go-to language for writing Solana progr

Gum Program Library, a collection of on chain programs maintained by Gum
Gum Program Library, a collection of on chain programs maintained by Gum

Gum, at its core, is a decentralized social media protocol on Solana. It unbundles traditional social media into Social Legos similar to how Defi unbu

Extract data from helium-programs via Solana RPC and serves it via HTTP

hnt-explorer This application extracts data from helium-programs via Solana RPC and serves it via HTTP. There are CLI commands meant to run and test t

Avalanche primitive types in Rust (experimental)

AvalancheGo Compatibility Crate Version(s) AvalancheGo Version(s) Protocol Version v0.0.134-155 v1.9.2,v1.9.3 19 v0.0.156-176 v1.9.4 20 v0.0.177-200 v

🐺 Starknet Rust types 🦀

types-rs 🐺 Starknet Rust types 🦀 This repository is an initiative by a group of maintainers to address the fragmentation in the Starknet Rust ecosys

Comments
  • Async interface for Shared and SharedMut

    Async interface for Shared and SharedMut

    This is an experimental branch that attempts to make the primitives work using tokio::task::spawn_blocking and similar methods in other frameworks.

    I'm not a fan of this approach after playing with it a bit, so I'm leaving a PR to experiment more.

    opened by mmastrac 0
  • Synchronization implementation: lazy non-globals

    Synchronization implementation: lazy non-globals

    Currently only globals support lazy initialization, but after the most recent refactoring this should be much simpler to implement for all shared primitives.

    opened by mmastrac 0
  • Synchronization implementation: block read() until first write()

    Synchronization implementation: block read() until first write()

    This would be a primitive where the container is initially empty, and any reads will block until the first write. It overlaps with lazy primitives somewhat, but is useful when the lazy initialization cannot be easily turned into a non-capturing function.

    opened by mmastrac 0
Owner
Matt Mastracci
Matt Mastracci
A tool for secret-shared passphrases.

harpo harpo is a tool and library that provides the following functionality: It can generate a seed phrase. Given a seed phrase, it can generate any n

Thomas Locher 11 Jun 30, 2022
Ursa - Hyperledger Ursa is a shared cryptography library

HYPERLEDGER URSA Introduction Features Libursa Libzmix Dependencies Building from source Contributing Introduction Ursa was created because people in

Hyperledger 307 Dec 20, 2022
Simple Multi-User Chat

4at Simple Multi-User Chat. The code has fallen to "Rewrite it in Rust" disease. You can find the legacy Go version in ./legacy-go-version if you stil

Tsoding 89 Nov 13, 2023
A template for command-line Rust programs

This is a template for command-line Rust programs. Clone and run the rename script with both a lowercase and UpperCase name. In the dev profile, depen

ludios 2 Oct 3, 2022
⛏ An open protocol for launching liquidity mining programs on Solana.

⛏ Quarry An open protocol for launching liquidity mining programs on Solana. Background Quarry was built with the intention of helping more Solana pro

Quarry Protocol 207 Dec 19, 2022
The Solana Program Library (SPL) is a collection of on-chain programs targeting the Sealevel parallel runtime.

Solana Program Library The Solana Program Library (SPL) is a collection of on-chain programs targeting the Sealevel parallel runtime. These programs a

null 6 Jun 12, 2022
⬆ A program for deploying and upgrading programs.

DeployDAO Migrator WARNING: This code is a work in progress. Please do not use it as is. A program for deploying and upgrading programs. About The Mig

Deploy DAO 28 May 28, 2022
A suite of programs for Solana key management and security.

?? goki Goki is a suite of programs for Solana key management and security. It currently features: Goki Smart Wallet: A wallet loosely based on the Se

Goki Protocol 157 Dec 8, 2022
Examples of Solana on-chain programs

spl-examples List of Solana on-chain programs which demonstrate different aspects of Solana architecture. 01__state It's a counter program. Each user

Max Block 51 Dec 6, 2022
🧑‍✈ Version control and key management for Solana programs.

captain ??‍✈️ Version control and key management for Solana programs. Automatic versioning of program binaries based on Cargo Separation of deployer a

Saber 35 Mar 1, 2022