Skip to content

28.3 自动推理

Send 和 Sync 就是一种常见的标记 traits(marker traits)。 它们并没有任何方法或关联类型,仅仅标记了该类型可以被安全地在多个线程之间共享(Sync)或传递(Send)。 使用这些标记 traits,编译器可以在编译时对类型进行检查,确保它们可以安全地在多个线程中使用。

在 Rust 中,有一些 trait 是在 std::marker 模块中的特殊 trait。 在 std::marker 这个模块中的 traits,内部都没有任何方法或关联类型,都是给类型做标记的 trait。每一种标记都将类型严格切分成了两个组。

那么为什么要定义这样一组特殊的 trait,还叫做 marker trait ?这些 trait 有什么特别之处?

我们称这些 trait 为“标记”,就是为了区分接口的实现和向编译器提供有关类型属性的信息,这两者都是在 Rust 中通过 trait 完成的。 然而,它们并没有什么特别之处。与所有其他 trait 一样,这些都是正常的 trait 。这只是我们人类的语义上的名称。

摘自:What is a marker trait in Rust?

目前(rustc 1.70.0)std::marker 模块中有 5 个稳定的特征。它们具有以下含义。 如果类型 T 实现:

  • Copy 表示可以通过使用按位(bitwise)复制来克隆
  • Send 表示 T 类型的值可以跨线程边界发送
  • Sync 表示 T 类型的值可以在线程间共享(即 &T 为 Send )
  • Sized 表示类型 T 的大小在编译时已知
  • Unpin 表示 T 类型的值在固定后可以移动

上面的 Send、Sync 和 Unpin 是 auto traits。std::panic 模块中还有两个更稳定的自动特征。

Send 特征在标准库中的 std::marker 模块中具有以下类型签名:

rust
pub unsafe auto trait Send { }

这种特殊语法的 trait 叫作 OIBIT(Opt-in built-in trait),后来改称为自动特征(Auto Trait)。 这是一种不稳定的特性,每个类型都会自动实现一个 trait,除非它们选择退出或包含一个不实现该 trait 的类型。

换言之,Opt-in 对应还有个 Opt-out,可以通过!negative trait impl)语法来实现。

例如:下面代码中第一行表示类型Wrapper实现了Send,但是却没实现Sync

rust
unsafe impl Send for Wrapper {}
unsafe impl !Sync for Wrapper {}

28.3.1 auto trait

需要通过安装 nightly 版使用 auto_traits feature 特性。

sh
rustup toolchain install nightly

下面以自定义 auto trait 实现为例:

rust
#![feature(negative_impls)]
#![feature(auto_traits)]

auto trait IsCool {}
impl !IsCool for String {}

struct MyStruct;
struct HasAString(String);

fn check_cool<C: IsCool>(_: C) {}

fn main(){
    check_cool(42);
    check_cool(false);
    check_cool(HasAString);
    check_cool(MyStruct);
    // the trait `IsCool` is not implemented for `String`
    // check_cool(String::new());
}

这里展示了 auto trait 的用法,当没有实现(通过!方式) auto trait 时,编译器会在编译阶段报:the trait XXX is not implemented for YYY

Auto Trait 有一个重要特点,就是编译器允许用户不用手写 impl,自动根据这个类型的成员“推理”出这个类型是否满足这个 trait。

我们可以手动指定这个类型满足这个 trait 约束,也可以手动指定它不满足这个 trait 约束,但是手动指定的时候,一定要用 unsafe 关键字。

比如,在标准库中就有这样的代码:

rust
unsafe impl<T: ?Sized> !Send for *const T { }
unsafe impl<T: ?Sized> !Send for *mut T { }
unsafe impl<'a, T: Sync + ?Sized> Send for &'a T {}
unsafe impl<'a, T: Send + ?Sized> Send for &'a mut T {}
// 等等

使 !Send 这种写法表示“取反”操作,这些类型就一定不满足 Send 约束。

请大家一定要注意 unsafe 关键字。这个关键字在这里的意思是,编译器自己并没有能力正确地、智能地理解每一个类型的内部实现原理,并由此判断它是否满足 Send 或者 Sync。它需要程序员来提供这个信息。此时,编译器选择相信程序员的判断。 但同时,这两个 trait 对于“线程安全”至关重要,如果程序员自己在这里判断错了,就可能制造出“线程不安全”的问题。

所以,这里的规则和前面讲的“内存安全”的情况是一样的。某些情况下,程序员需要做底层操作的时候,编译器没有能力判断这部分是不是满足内存安全,就需要程序员把这部分代码用 unsafe 关键字包起来,由程序员去负责安全性。 unsafe 关键字的意义不是说这段代码“不安全”,而是说这段代码的安全性编译器自己无法智能检查出来,需要由程序员来保证

标准库中把所有基本类型,以及标准库中定义的类型,都做了合适的 Send/Sync 标记

同时,由于 Auto trait 这个机制的存在,绝大部分用户创建的自定义类型,本身都已经有了合理的 Send/Sync 标记,用户不需要手动修改它。 只有一种情况例外:用户用了 unsafe 代码的时候,有些类型就很可能需要手动实现 Send/Sync。 比如做 FFI,在 Rust 项目中调用 C 的代码。这种时候,类型内部很可能会包含一些裸指针,各种方法调用也会有许多unsafe代码块。 此时,一个类型是否满足 Send/Sync 就不能依赖 Auto Trait 机制由编译器推理了,因为它推理出来的结论很可能是错的。

任何包含裸指针的类型都会自动被标记为 !Send否定实现是编译器使用的不稳定功能)。 但是,如果你肯定在线程之间发送这样的类型是安全的,那您可以显式地为其实现Send(请注意,您必须使用unsafe关键字,因为Send的错误实现会导致未定义的行为:

rust
struct Bar {
    ptr: *const (),
}

unsafe impl Send for Bar {}

但是,如果异常是特征Sized,那它只能由编译器实现。尝试手动实现它会导致 E0322 错误。

程序员需要根据 Send/Sync 所表达的概念去理解这个类型的逻辑,然后自己判断出它是否满足 Send/Sync 的约束。 在这种情况下,写这个库的程序员就成了实现“线程安全”目标的重要一环。 如果写错了,就会对下游用户造成致命的影响,所有依赖于这个库的代码都有可能引发线程不安全。

其他参考 marke trait

Released under the MIT License