编程并行计算工程师(Rust)

Rust 中安全的多线程是如何工作的:marker-traits Send 和 Sync 代表什么,它们如何控制线程之间的数据传递和共享,开发者如何正确实现(或禁止)这些特性用于自定义类型?

用 Hintsage AI 助手通过面试

答案。

关于安全的多线程编程的问题,程序员们早已面临,常常遇到竞争条件、不一致的数据和内存泄漏。在 Rust 中,采用了一种独特的方法,使用 marker-traits Send 和 Sync,以便在编译阶段尽量减少这些问题。

问题在于缺乏对共享数据在线程之间访问的控制,这导致难以调试的错误。在许多语言中,这完全依赖于程序员,而在 Rust 中,编译器会检查哪些数据可以在线程之间传递或共享。

解决方案:trait Send 确保对象可以安全地从一个线程传递到另一个线程。Sync 则意味着可以安全地通过引用在不同线程中访问对象。几乎所有在 Rust 中的标准类型都会自动实现这些特性,而自定义类型可以手动实现它们或通过 impl !Send 或 impl !Sync 来禁止特定情况。

代码示例:

use std::sync::{Arc, Mutex}; use std::thread; let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } // counter 始终等于 10,且没有竞争!

关键特性:

  • Send 意味着对象可以在线程之间转移所有权。
  • Sync 意味着可以通过引用安全地共享对象。
  • 为自定义类型实现或禁止特性允许在编译阶段控制行为。

有陷阱的问题。

包含不安全指针的类型可以是 Send 或 Sync 吗?

不可以,如果类型包含原始指针或没有线程安全保证的资源,则不会实现这些特性,或者开发者必须完全负责手动实现它们(通常是使用 unsafe impl Send/Sync)。

Rc<T> 和 RefCell<T> 是 Send 或 Sync 吗?

不,Rc<T> 和 RefCell<T> 对于多线程使用是不安全的(既不是 Send,也不是 Sync)。在多线程场景中使用 Arc<T> 和 Mutex/RwLock。

如果静态变量包含未实现 Sync 的类型会发生什么?

Rust 不允许这样的静态变量存在:它必须是 Sync,否则编译器会报错。

常见错误和反模式

  • 在多个线程中共享时使用 Rc<T> 而不是 Arc<T>
  • 开发包含内部不安全指针的数据结构并自动信任 Send trait。
  • 通过使用 unsafe impl Send/Sync 缺乏严格控制来破坏不变性。

实际案例

消极案例

年轻的开发者将一个 Rc 对象放入 thread::spawn 中——如果 Rc 不在线程之间传递,代码将编译通过。尝试从 thread::spawn 中导出 Rc 会出现编译错误,因为 Rc 没有实现 Send,且没有保护以防竞争。

优点:

  • 编译器会立即防止数据竞争的错误。

缺点:

  • 如果不清楚 Rc 和 Arc 之间的区别,处理错误会很困难。

积极案例

使用 Arc+Mutex 实现多线程计数器,所有线程通过线程安全的接口使用相同的数据。没有竞争,代码安全且稳定。

优点:

  • 没有竞争,内存安全,使用 marker-traits 来管理行为。

缺点:

  • Mutex 和 Arc 有开销,需要了解线程安全的原语。