6.8 Send、Sync 与编译测试
原文链接: https://rust-unofficial.github.io/too-many-lists/sixth-send-sync.html
好吧,其实还有一对 trait 要想,但它们很特殊。得面对 Rust 的神罗帝国:Unsafe Opt-In Built-In Traits(OIBIT):Send 和 Sync——事实上是 opt-out、built-out(三项里对一项就算不错!)。
和 Copy 一样,这些 trait 没有关联代码,只是标记类型具有某种性质。Send 表示类型可以安全地发送到另一线程。Sync 表示类型可以在线程间共享(&Self: Send)。
LinkedList 协变的同样论证也适用于这里:一般不用花哨内部可变性技巧的常规集合,做成 Send 和 Sync 是安全的。
但我说它们是opt out。那我们已经是了吗?怎么知道?
给代码加点新魔法:随机私有垃圾,除非类型具备预期性质否则编译不过:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #[allow(dead_code)]
fn assert_properties() {
fn is_send<T: Send>() {}
fn is_sync<T: Sync>() {}
is_send::<LinkedList<i32>>();
is_sync::<LinkedList<i32>>();
is_send::<IntoIter<i32>>();
is_sync::<IntoIter<i32>>();
is_send::<Iter<i32>>();
is_sync::<Iter<i32>>();
is_send::<IterMut<i32>>();
is_sync::<IterMut<i32>>();
is_send::<Cursor<i32>>();
is_sync::<Cursor<i32>>();
fn linked_list_covariant<'a, T>(x: LinkedList<&'static T>) -> LinkedList<&'a T> { x }
fn iter_covariant<'i, 'a, T>(x: Iter<'i, &'static T>) -> Iter<'i, &'a T> { x }
fn into_iter_covariant<'a, T>(x: IntoIter<&'static T>) -> IntoIter<&'a T> { x }
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| cargo build
Compiling linked-list v0.0.3
error[E0277]: `NonNull<Node<i32>>` cannot be sent between threads safely
--> src\lib.rs:433:5
|
433 | is_send::<LinkedList<i32>>();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ `NonNull<Node<i32>>` cannot be sent between threads safely
|
= help: within `LinkedList<i32>`, the trait `Send` is not implemented for `NonNull<Node<i32>>`
= note: required because it appears within the type `Option<NonNull<Node<i32>>>`
note: required because it appears within the type `LinkedList<i32>`
--> src\lib.rs:8:12
|
8 | pub struct LinkedList<T> {
| ^^^^^^^^^^
note: required by a bound in `is_send`
--> src\lib.rs:430:19
|
430 | fn is_send<T: Send>() {}
| ^^^^ required by this bound in `is_send`
<a million more errors>
|
天哪,怎么回事!我那个神罗帝国笑话白准备了!
好吧,我说裸指针只有一道安全护栏时骗了你:这是另一道。*const 和 *mut 会显式 opt out Send 和 Sync 以保证安全,所以我们确实得 opt back in:
1
2
3
4
5
6
7
8
| unsafe impl<T: Send> Send for LinkedList<T> {}
unsafe impl<T: Sync> Sync for LinkedList<T> {}
unsafe impl<'a, T: Send> Send for Iter<'a, T> {}
unsafe impl<'a, T: Sync> Sync for Iter<'a, T> {}
unsafe impl<'a, T: Send> Send for IterMut<'a, T> {}
unsafe impl<'a, T: Sync> Sync for IterMut<'a, T> {}
|
注意这里得写 unsafe impl:这些是unsafe trait!unsafe 代码(比如并发库)会依赖我们正确实现这些 trait!既然没有实际代码,保证就是:对,我们确实可以安全 Send 或在线程间 Share!
别随便乱贴,但作为认证专业人士我说:Yep,完全没问题。注意 IntoIter 不用单独实现 Send/Sync:它只包含 LinkedList,会自动推导 Send 和 Sync——我说过它们其实是 opt out!(用 hilariously 的语法 impl !Send for MyType {} opt out。)
1
2
3
| cargo build
Compiling linked-list v0.0.3
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
|
好!
……等等,如果不该是这些东西的类型却是,那真的很危险。尤其 IterMut 绝对不该协变,因为它「像」&mut T。怎么检查?
用魔法!其实是 rustdoc!不必非用 rustdoc,但这是最搞笑的方式。见,如果在 doc 注释里写代码块,rustdoc 会尝试编译运行,就能做不影响主程序的匿名「小程序」:
1
2
3
4
5
6
| /// ```
/// use linked_list::IterMut;
///
/// fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
/// ```
fn iter_mut_invariant() {}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| cargo test
...
Doc-tests linked-list
running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 458) ... FAILED
failures:
---- src\lib.rs - assert_properties::iter_mut_invariant (line 458) stdout ----
error[E0308]: mismatched types
--> src\lib.rs:461:86
|
6 | fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
| ^ lifetime mismatch
|
= note: expected struct `linked_list::IterMut<'_, &'a T>`
found struct `linked_list::IterMut<'_, &'static T>`
|
好,证明它是不变的,但呃,测试失败了。没事,rustdoc 可以用 compile_fail 标注表示预期失败!
(其实只证明了「不协变」,要是你意外搞成错误的逆变,恭喜?)
1
2
3
4
5
6
| /// ```compile_fail
/// use linked_list::IterMut;
///
/// fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
/// ```
fn iter_mut_invariant() {}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| cargo test
Compiling linked-list v0.0.3
Finished test [unoptimized + debuginfo] target(s) in 0.49s
Running unittests src\lib.rs
...
Doc-tests linked-list
running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 458) - compile fail ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s
|
耶!建议先写不带 compile_fail 的测试,确认失败原因正确。比如忘了 use 也会失败(从而通过),那不是我们想要的!概念上能「要求」特定编译器错误很诱人,但会是噩梦,等于编译器改进错误信息也成了 breaking change。我们希望编译器变好,所以不行。
(等等,其实可以在 compile_fail 旁指定错误码但这只在 nightly 有效,依赖它是坏主意,上面说的原因。非 nightly 会静默忽略。)
1
2
3
4
5
6
| /// ```compile_fail,E0308
/// use linked_list::IterMut;
///
/// fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
/// ```
fn iter_mut_invariant() {}
|
……还有,注意到我们让 IterMut 不变了吗?容易漏,因为我「只是」复制粘贴 Iter 丢到末尾。最后一行:
1
2
3
4
5
6
| pub struct IterMut<'a, T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<&'a mut T>,
}
|
试试去掉 PhantomData:
1
2
3
4
5
6
7
8
9
| cargo build
Compiling linked-list v0.0.3 (C:\Users\ninte\dev\contain\linked-list)
error[E0392]: parameter `'a` is never used
--> src\lib.rs:30:20
|
30 | pub struct IterMut<'a, T> {
| ^^ unused parameter
|
= help: consider removing `'a`, referring to it in a field, or using a marker such as `PhantomData`
|
哈!编译器护着我们,不会让我们不用生命周期。试试错误例子:
1
| _boo: PhantomData<&'a T>,
|
1
2
3
| cargo build
Compiling linked-list v0.0.3 (C:\Users\ninte\dev\contain\linked-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.17s
|
编译过了!测试能抓到问题吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| cargo test
...
Doc-tests linked-list
running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 458) - compile fail ... FAILED
failures:
---- src\lib.rs - assert_properties::iter_mut_invariant (line 458) stdout ----
Test compiled successfully, but it's marked `compile_fail`.
failures:
src\lib.rs - assert_properties::iter_mut_invariant (line 458)
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s
|
耶!!!系统管用!我爱能真正干活的测试,这样就不用那么怕潜伏的错误了!