12 个版本 (5 个破坏性更新)
0.6.1 | 2022年4月5日 |
---|---|
0.6.0 | 2022年3月31日 |
0.5.2 | 2022年3月31日 |
0.4.0 | 2022年3月29日 |
0.1.0 | 2022年3月26日 |
#922 in Rust 模式
49KB
683 行
sendable
sendable
包定义了类型,这些类型有助于在线程之间发送相互连接的数据
SendRc
,一个线程安全的引用计数指针,可以在线程之间发送。你可以将其视为一个变体,即Rc<T>
,当T
是Send
时,它是Send
。这与Rc<T>
不同,它永远不会是Send
,也与Arc<T>
不同,后者需要T: Send + Sync
才能是Send
。SendOption
,它包含一个Option<T>
,即使在T
不是Send
的情况下也是Send
。它对于存储引用到一个单线程区域很有用,该区域单独发送。
何时使用 SendRc?
如果你
- 值形成一个无环图或带有交叉引用的层次结构;
- 从单个线程构建和使用层次结构;
- 偶尔需要将整个结构移动到另一个线程。
在单个线程的范围内,Rc
和 RefCell
提供了方便且安全的循环图表示。它们还非常高效,因为单线程操作不需要原子操作或锁,使得 deref
(解引用)变得简单,并允许编译器内联 borrow
(借用)和 borrow_mut
(可变借用)操作,甚至在它们不可全局观察的情况下优化它们。
在处理许多此类图的程序中,能够在单个线程中创建它们并将其发送到另一个线程进行(可能还有第三个线程进行拆卸)非常有用。鉴于像 RefCell
和 Cell
这样的类型是 Send
,这个想法并非不可行。问题是 Rc
,它既不是 Send
也不是 Sync
,这是有充分理由的。尽管将整个 Rc
层次从一個线程移动到另一个线程是完全安全的,但借用检查器不允许这样做,因为它无法静态证明你已移动 所有 的它们。如果一些指向共享数据的 Rc
仍然留在原始线程中,那么对非 Sync
单元的未同步访问和对引用计数的未同步操作将是未定义的行为,并造成混乱。
如果有一种方法可以向 Rust 证明您已将所有指向特定共享值的 Rc
指针发送到另一个线程,那么这样做就不会有问题,只要 T
本身是 Send
。这正是 SendRc
所提供的功能。
SendRc
是如何工作的?
当创建一个 SendRc
时,它将当前线程的 ID 与值和引用计数一起存储。在提供对值的访问之前,以及在通过 clone
和 drop
操作修改引用计数之前,它将检查当前线程是否仍然是预期的线程,否则引发恐慌。
在将 SendRc
移动到不同的线程之前,每个指针必须明确地“停放”,即注册为发送。一旦停放,对它所指向的值的访问被禁止,即使在原始线程中也是如此。一旦所有指向共享值的 SendRc
都已停放,它们就可以跨越线程边界发送,并在新线程中一次性重新启用。在两个简单的 SendRc
的情况下,这个过程看起来像这样
// create two SendRcs pointing to a shared value
let mut r1 = SendRc::new(RefCell::new(1));
let mut r2 = SendRc::clone(&r1);
// prepare to send them to a different thread
let pre_send = SendRc::pre_send();
pre_send.park(&mut r1); // r1 and r2 cannot be dereferenced from this point
pre_send.park(&mut r2);
// ready() would panic if there were unparked SendRcs pointing to the value
let post_send = pre_send.ready();
// move everything to a different thread
std::thread::spawn(move || {
// SendRcs are still unusable until unparked
post_send.unpark();
// they're again usable from this point, and only in this thread
*r1.borrow_mut() += 1;
assert_eq!(*r2.borrow(), 2);
});
为什么不用 Arc?
Arc
确实允许线程之间的移动,但它从根本上假设底层值将在不同的线程之间被 共享。为了让 Arc<T>
成为 Send
,Arc
需要 T: Send + Sync
,因为如果它只需要 T: Send
,则可以创建一个 Arc<RefCell<u32>>
,克隆它,将克隆发送到不同的线程,并在同一 RefCell
上从两个线程调用 borrow_mut()
而不进行同步。这是被禁止的,这就是为什么在 Rust 中不存在 Arc<RefCell<T>>
的原因。
SendRc
可以避免这种情况,因为它使用线程检查来保护对数据的每次访问。在跨线程移动数据时,它需要证明在移动之前,前一个线程中所有对分配值的引用都已释放。SendRc<RefCell<u32>>
是有效的,因为如果您克隆它并将克隆发送到不同的线程,您将无法访问数据,也无法克隆或甚至丢弃它——任何这些都会引发 panic。
使用标准库,可以通过切换到完全实现的 Arc<Mutex<T>>
或 Arc<RwLock<T>>
来修复问题。然而,这会减慢对数据的访问速度,因为它需要强有序原语、中毒检查和对 pthread API 的调用。它还由于必须分配系统互斥锁而增加了内存开销。即使是效率最高的互斥锁实现,如 parking_lot
,也不是免费的,并且需要同步的开销。但是,即使不考虑成本,如果既不需要 Arc
也不需要 Mutex
,则使用 Arc<Mutex<T>>
在概念上也是错误的,因为代码 不会 从多个线程并行访问 T
的值。
总的来说,SendRc<T>
是在运行时强制执行某些保证的 Send
,就像 Arc<Mutex<T>>
在运行时强制执行某些保证的 Send + Sync
一样。它们只是服务于不同的目的。
为什么不使用竞技场?或者不安全操作?
要实现一个Send
竞技场,整个设计必须从底层开始就致力于这个理念。简单地将每个Rc
替换为竞技场ID的方法实际上并不奏效,因为除了ID之外,对象还需要对竞技场的引用。它不能有类型为Option<&Arena>
或Option<Rc<Arena>>
的字段,因为这样的字段会使得类型在竞技场包含RefCell
时不再符合Send
。
确实有一些基于竞技场的方案可以工作,但需要做出更彻底的改变,比如将值的存储与访问和共享解耦。所有数据都位于竞技场中,访问器是即时创建的,其生命周期与竞技场的生命周期相关联。这要求在各个地方处理生命周期,对于非专家来说并不容易做对。
最后,可以通过仅使用用于将整个世界发送到新线程的包装类型的unsafe impl Send
来避免竞技场。这种解决方案是黑客式的,并且放弃了Rust提供的保证。如果你出错,比如说在原始线程中留下一个Rc
克隆,你将面临未定义的行为和核心转储,就像在C++中一样。在Rust中,我们希望做得更好,SendRc
旨在提供一种可靠的解决方案,以应对这种场景。
SendOption呢?
SendOption
是一个相关的提议:一个持有Option<T>
的类型,并且总是Send
,无论T
是否Send
。这显然不可能安全吧?
使其工作的原因是SendOption
要求你在将其发送到另一个线程之前将其值设置为None
。如果内部Option<T>
是None
,那么即使T
不是Send
也没有关系,因为实际上没有任何T
被发送到任何地方。如果你将非None
的SendOption<T>
发送到另一个线程,SendOption
将通过panic来阻止你以任何方式访问它(包括通过丢弃它)。不遵守规则将导致实际上从未“发送”到另一个线程的T
,只是它的位被浅拷贝并遗忘,这是安全的。
SendOption
是为由Send
数据组成的类型设计的,除了一个非Send
类型的可选字段。该字段仅应在特定线程内设置和使用,在跨线程传输时会变为None
,但由于Rust无法证明这一点,因此Option<NonSendType>
字段使整个外部类型非Send
。例如,一个具有SendOption<Rc<Arena>>
的字段可以用来创建一个指向单线程场地的Send
类型。
这真的安全吗?
与任何包含不安全代码的crate一样,人们永远不能100%确信没有安全性问题。上述设计的实现代码相当直接。我在确定当前方法之前,对设计和实现进行了多次迭代,虽然我偶尔发现了问题,但基本思想在审查中得到了证明。MIRI在运行测试时没有发现未定义的行为。
欢迎您审查代码——它并不大——并报告您遇到的问题。
运行时检查昂贵吗?
尽管SendRc
和SendOption
执行的运行时检查不是免费的,但它们相对便宜。
SendRc::deref()
将获取并固定到线程的整数id与当前线程(从线程本地存储获取)进行松散的原子加载进行比较。它还通过非原子的整数比较零来检查迁移是否正在进行。松散的原子加载在Intel上编译为普通加载,这是最便宜的,如果您担心,可以保持对引用的持有以避免重复检查。(借用检查器将阻止您在存在未解决引用的情况下将SendRc
发送到另一个线程。)SendRc::clone()
和SendRc::drop()
执行类似的检查。
SendOption::deref()
和SendOption::deref_mut()
仅检查当前线程是否是预期的线程,使用与SendRc
类似的松散加载和比较。
关于内存使用,SendRc
的堆开销是两个u64
用于固定和停靠信息,一个机器字用于引用计数,即64位架构上比Rc
多一个u64
。单个SendRc
有两个机器字宽,因为它必须跟踪每个指针的标识。 SendOption
存储一个u64
,与底层选项一起。
许可
sendable
同时遵循MIT许可协议和Apache许可协议(版本2.0)。有关详情,请参阅LICENSE-APACHE和LICENSE-MIT。贡献更改被视为同意这些许可条款。