21 个版本

0.3.5 2023年3月12日
0.3.4 2023年3月11日
0.2.5 2023年3月6日
0.1.8 2023年3月5日

147并发 中排名

Download history 524/week @ 2024-03-27 294/week @ 2024-04-03 223/week @ 2024-04-10 214/week @ 2024-04-17 119/week @ 2024-04-24 125/week @ 2024-05-01 122/week @ 2024-05-08 207/week @ 2024-05-15 155/week @ 2024-05-22 231/week @ 2024-05-29 191/week @ 2024-06-05 171/week @ 2024-06-12 126/week @ 2024-06-19 165/week @ 2024-06-26 90/week @ 2024-07-03 223/week @ 2024-07-10

631 每月下载量
4 crates 中使用

Apache-2.0 OR MIT

115KB
2.5K SLoC

保持冷静(并调用 Clone)

Build Status docs.rs crates.io

多线程 Rust 程序的简单共享类型:keepcalm 允许您在并发 Rust 应用程序中简化同步代码。

名称灵感来自 @luser 的 Keep Calm and Call Clone

概述

该库简化了多线程程序(如 Web 服务器)中使用的许多共享对象模式。

keepcalm 的优势

  • 您无需事先决定同步原语。一切都是一个 SharedSharedMut,无论它是互斥锁、读写锁、读写/复制/更新原语,还是只读共享 std::sync::Arc
  • 一切都可以通过 project! 来调整,这意味着您可以在任何时间调整锁的粒度,而无需重构整个系统。如果您以后需要更细粒度的锁,使用共享容器的代码不会改变!
  • 可写容器可以转换为只读容器,同时仍然保留其他代码更新内容的能力。
  • 由于 parking_lot crate,读写守护者是 Send
  • 每个同步原语都透明地管理中毒状态(如果在持有锁时代码 panic!,则会中毒)。如果您不想在 panic! 上中毒,构造函数可供您完全禁用此选项。
  • static 全局作用域的容器,用于 Sync!Sync 对象,可以通过 SharedGlobal 简单构建,并提供 Shared 容器。类似地,可变的全局容器可以通过 SharedGlobalMut 构建。 注意: 这需要设置 --feature global_experimental 标志
  • 相同的原语在同步和 async 内容中都适用(需要注意的是,后者目前是实验性的):你可以使用 read_asyncwrite_async 来等待异步锁的异步版本。
  • 最小性能影响:基准测试显示,原始的 parking_lot 原语/tokio 异步容器与 keepcalm 中的性能大致相同。

性能

粗略的基准测试表明,在异步和同步上下文中,与 tokioparking_lot 原语的性能大致相当。虽然在某些情况下,keepcalm 的性能略快于 parking_lot,但这很可能是测量噪声。

基准测试 keepcalm tokio parking_lot
互斥锁 (异步,无竞争) 23ns 49ns n/a
互斥锁 (异步,有竞争) 1.3ms 1.3ms n/a
读写锁 (异步,无竞争) 14ns 46ns n/a
读写锁 (异步,有竞争) (未测试) (未测试) (未测试)
读写锁 (同步) 6.8ns n/a (未测试)
互斥锁 (同步) 7.3ns n/a 8.5ns

容器类型

以下容器类型可用

容器 等效 备注
SharedMut::new Arc<RwLock<T>> 这是默认的共享可变类型。
SharedMut::new_mutex Arc<Mutex<T>> 在某些情况下,可能需要同时序列化读取和写入。例如,对于不是 Sync 的类型。
SharedMut::new_rcu Arc<RwLock<Arc<T> 当RCU容器的写锁释放时,写入的值将被提交到容器中的值。
Shared::new Arc 这是默认的共享不可变类型。请注意,这稍微有些冗长:Shared 不指向底层类型,并且需要调用 Shared::read
Shared::new_mutex Arc<Mutex<T>> 对于不是 Sync 的类型,使用 Mutex 来序列化只读访问。
SharedMut::shared n/a 这提供了对读写容器的只读视图,没有直接等效项。

以下全局容器类型可用

容器 等效 备注
SharedGlobal::new staticT 这是一个全局的 const 样式对象,用于 Send + Sync 类型。
SharedGlobal::new_lazy static Lazy<T> 这是一个按需初始化的全局 const 风格对象,用于类型为 Send + Sync 的类型。
SharedGlobal::new_mutex static Mutex<T> 这是一个全局 const 风格对象,用于类型为 Send 但不一定为 Sync 的类型。
SharedGlobalMut::new static RwLock<T> 这是一个全局可变对象,用于类型为 Send + Sync 的类型。
SharedGlobalMut::new_lazy static Lazy<RwLock<T>> 这是一个按需初始化的全局可变对象,用于类型为 Send + Sync 的类型。
SharedGlobalMut::new_mutex static Mutex<T> 这是一个全局可变对象,用于类型为 Send 但不一定为 Sync 的类型。

基本语法

传统的 Rust 共享对象模式通常比较冗长且重复,例如

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

如果我们想将共享字段从 std::sync::Mutex 切换到 std::sync::RwLock,我们只需要更改四行类型,并将 lock 方法切换为 read 方法。

我们可以通过使用 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());

如果我们想使用 Mutex 而不是 SharedMut 在底层默认使用的 RwLock,我们只需要将 SharedMut::new 切换为 SharedMut::new_mutex

SharedMut

SharedMut 对象隐藏了管理 Arc<Mutex<T>>Arc<RwLock<T>> 和其他同步类型背后的复杂性,通过单一接口进行管理。

# use keepcalm::*;
let object = "123".to_string();
let shared = SharedMut::new(object);
shared.read();

默认情况下,SharedMut 对象在底层使用 Arc<RwLock<T>>,但你可以在构造时选择同步原语。SharedMut 对象 擦除 下面的原始类型,你可以相互替换它们。

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

管理同步原语的毒化状态也是一个挑战。如果在你持有锁时发生 panic!,Rust 会毒化 MutexRwLock

SharedMut 类型允许您在构造时指定一个 PoisonPolicy。默认情况下,如果同步原语被毒化,SharedMut 将在访问时引发 panic!。这可以被配置为忽略毒化。

# use keepcalm::*;
let shared = SharedMut::new_with_policy("123".to_string(), PoisonPolicy::Ignore);

Shared

默认的 Shared 对象类似于 Rust 的 std::sync::Arc,但增加了投影功能。对象也可以作为 Mutex 构建,或者是一个 SharedMut 的只读视图。

请注意,由于这种灵活性,Shared 对象比传统的 std::sync::Arc 要复杂一些,因为所有访问都必须通过 Shared::read 访问器来完成。

实验性:全局变量

注意:这需要 --feature global_experimental 标志

虽然 static 全局变量在 Rust 中可能经常是一种反模式,但此库还提供易于使用的替代方案,这些方案与 SharedSharedMut 类型兼容。

可以使用 SharedGlobal 创建全局 Shared 引用。

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

同样,可以使用 SharedGlobalMut 创建全局 SharedMut 引用。

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

SharedGlobalSharedGlobalMut 都提供了一个 new_lazy 构造函数,允许初始化在首次访问时延迟。

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

实验性:异步

注意:这需要 --feature async_experimental 标志

这非常实验性,可能存在安全性和/或性能问题!

SharedSharedMut 类型支持一个 read_asyncwrite_async 方法,该方法将使用异步运行时的 spawn_blocking 方法(或等效方法)来阻塞。使用 make_spawner 创建一个 Spawner 并将其传递给适当的锁定方法。

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

投影

`Shared` 和 `SharedMut` 都允许对底层类型的 投影。投影可以用来选择类型的子集,或者将类型转换为特质。`project!` 和 `project_cast!` 宏可以简化此代码。

注意,投影始终与根对象相关联!如果投影被锁定,根对象也会被锁定。

转换

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

结构体/元组的子集

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

非固定大小类型

`Shared` 和 `SharedMut` 都支持非固定大小类型,但由于语言当前的限制(详见 std::ops::CoerceUnsized),您需要以特殊方式构建它们。

支持非固定大小特质,但您需要在共享类型中指定 Send + Sync,或者使用 project_cast! 对象

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

使用盒子支持非固定大小切片

# use keepcalm::*;
let boxed: Box<[i32]> = Box::new([1, 2, 3]);
let shared: SharedMut<[i32]> = SharedMut::from_box(boxed);

依赖关系

~0.4–5.5MB
~16K SLoC