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

耶!!!系统管用!我爱能真正干活的测试,这样就不用那么怕潜伏的错误了!

最后修改 August 23, 2026: 更新 (499855b16)