Android程序员初学Rust-Send,Sync,Share

100 阅读5分钟

1.webp

Send 和 Sync

Rust 是如何知道数据支持跨线程的共享访问呢?答案在于两个 trait

  • Send:如果将类型 T 跨线程边界移动是安全的,那么类型 T 就实现了 Send
  • Sync:如果将 &T 跨线程边界移动是安全的,那么类型 T 就实现了 Sync

SendSync 是不安全的 trait。只要你的类型仅包含实现了 SendSync 的类型,编译器就会自动为你的类型推导这两个 trait。如果你确定可以这样做,也可以手动实现它们。

如果将类型 T 的值移动到另一个线程是安全的,那么类型 T 就可以是 Send 类型。

将所有权转移到另一个线程 B 的结果是,析构函数将在那个线程 B 中运行。所以问题在于,何时可以在一个线程中分配一个值,而在另一个线程中释放它 。

如果可以安全地在多个线程中同时访问类型 T 的值,那么类型 T 就可以是 Sync 类型。

更确切地说,定义如下: 当且仅当 &T 实现了 Send 时,T 才可以是 Sync 类型。

这句话本质上是一种简略的说法,意思是如果一个类型对于共享使用是线程安全的,那么跨线程传递它的引用也是线程安全的。

这是因为如果一个类型是 Sync,就意味着它可以在多个线程间共享,而不存在数据竞争或其他同步问题的风险,所以将它移动到另一个线程是安全的。对该类型的引用移动到另一个线程也是安全的,因为它所引用的数据可以在任何线程中安全地访问。

举个例子

2.jpeg

Send + Sync

你遇到的大多数类型都是 Send + Sync

  • i8f32boolchar&str 等等;
  • (T1, T2)[T; N]&[T]struct { x: T } 等等;
  • StringOption<T>Vec<T>Box<T> 等等;
  • Arc<T>:通过原子引用计数实现显式线程安全;
  • Mutex<T>:通过内部锁定实现显式线程安全;
  • mpsc::Sender<T>:自 1.72.0 版本起;
  • AtomicBoolAtomicU8 等等:使用特殊的原子指令 。

当类型参数是 Send + Sync 时,泛型类型通常也是 Send + Sync

Send + !Sync

这些类型可以移动到其他线程,但它们不是线程安全的。通常是因为内部可变性:

  • mpsc::Receiver<T>
  • Cell<T>
  • RefCell<T>

!Send + Sync

这些类型可以通过共享引用在多个线程中安全访问,但不能移动到另一个线程:

MutexGuard<T: Sync>:使用操作系统级原语,必须在创建它们的线程上释放。不过,一个已经锁定的互斥锁的受保护变量可以被任何共享该锁的线程读取。

!Send + !Sync

这些类型既不是线程安全的,也不能移动到其他线程:

  • Rc<T>:每个 Rc<T> 都有一个指向 RcBox<T> 的引用,其中包含一个非原子的引用计数;
  • *const T*mut T:Rust 假定裸指针可能有特殊的并发方面的考虑。

Arc

3.jpg

Arc<T> 允许通过 Arc::clone 进行共享的只读所有权:

use std::sync::Arc;
use std::thread;

/// 一个结构体,用于打印是哪个线程释放了它。
#[derive(Debug)]
struct WhereDropped(Vec<i32>);

impl Drop for WhereDropped {
    fn drop(&mut self) {
        println!("Dropped by {:?}", thread::current().id())
    }
}

fn main() {
    let v = Arc::new(WhereDropped(vec![10, 20, 30]));
    let mut handles = Vec::new();
    for i in 0..5 {
        let v = Arc::clone(&v);
        handles.push(thread::spawn(move || {
            // 阻塞 0-500ms.
            std::thread::sleep(std::time::Duration::from_millis(500 - i * 100));
            let thread_id = thread::current().id();
            println!("{thread_id:?}: {v:?}");
        }));
    }

    // 现在只有新创建的线程会持有 `v` 的克隆。
    drop(v);

    // 当最后一个新创建的线程运行完毕时,它将释放 `v` 中的内容。
    handles.into_iter().for_each(|h| h.join().unwrap());
}

输出如下:

// Output
ThreadId(6): WhereDropped([10, 20, 30])
ThreadId(5): WhereDropped([10, 20, 30])
ThreadId(4): WhereDropped([10, 20, 30])
ThreadId(3): WhereDropped([10, 20, 30])
ThreadId(2): WhereDropped([10, 20, 30])
Dropped by ThreadId(2)

运行结果可能不会完全相同。但我们发现,在主线程调用 drop(v) 并没有触发 v 的释放,v 的释放是在其他线程中进行的。

Arc 代表 “原子引用计数”(Atomic Reference Counted),是 Rc 的线程安全版本,它使用原子操作。

无论 T 是否实现 CloneArc<T> 都实现了 Clone。当且仅当 T 同时实现了 SendSync 时,Arc<T> 才实现 SendSync

Arc::clone() 有执行原子操作的开销,但在此之后对 T 的使用是无额外开销的。

注意引用循环问题,Arc 不会使用垃圾回收器来检测它们(std::sync::Weak 可以提供帮助)。

Mutex

Mutex<T> 确保互斥,并允许通过只读接口对 T 进行可变访问(另一种内部可变性形式):

use std::sync::Mutex;

fn main() {
    let v = Mutex::new(vec![10, 20, 30]);
    println!("v: {:?}", v.lock().unwrap());

    {
        let mut guard = v.lock().unwrap();
        guard.push(40);
    }

    println!("v: {:?}", v.lock().unwrap());
}

查看源码看看如何为 Mutex<T> 进行泛型实现 impl<T: Send> Sync for Mutex<T> 的。

Rust 中,Mutex 看起来就像是一个只有一个元素(即受保护的数据)的集合。在访问受保护的数据之前,不可能忘记获取互斥锁。

通过获取锁,可以从 &Mutex<T> 得到一个 &mut TMutexGuard 确保 &mut T 的生命周期不会超过持有的锁的生命周期。

当且仅当 T 实现 Send 时,Mutex<T> 同时实现 SendSync。与之对应的读写锁是:RwLock

为什么 lock() 会返回一个 Result 呢?

如果持有 Mutex 的线程发生了panic,那么 Mutex 就会变成 “中毒” 状态,以此表明它所保护的数据可能处于不一致的状态。对处于中毒状态的互斥锁调用 lock() 会失败,并返回一个 PoisonError。无论如何,你都可以在这个错误上调用 into_inner() 来恢复数据。