Appearance
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 。这只是我们人类的语义上的名称。
目前(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