Skip to content

rust中的sync和send #79

@BruceChen7

Description

@BruceChen7

参考资料

如何保证共享数据多线程安全

  • 共享只读数据。
  • 不共享数据。
  • 保护好共享的数据
  • 保证数据被访问的时候是 valid 的

共享只读数据

  • 共享只读数据就是在多线程中,只能对变量进行读操作,不能进行写操作。
  • 只对数据进行只读操作呢?是 Rust 类型系统里面规定了,引用在任何时候只能是下面两种情况之一,而不能同时存在:
    • 有任意个只读引用
    • 有一个可变引用(通过这个引用可以修改引用的变量)
  • 这样就会防止数据在一个线程被写,而在另外的线程被读取或者写。

(concept:: Sync 和 Send, 'static)

Send

  • 不共享数据就是**不要在多线程中共享变量,
  • Rust 对此在编译器做了保证,防止不共享的变量被共享了,而这靠的就是Send trait
  • Send 在多线程程序中起到了什么作用呢?满足 Send,说明这个变量可以安全的在线程间转移。
  • 实现了 Send trait 表示是所有权类型
  • 不共享数据的时候,可以直接将变量 move 给生成的线程,这样,数据就被这个线程独有了
    let v = vec![1, 2, 3];
    
    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });
    
    handle.join().unwrap();
  • vector v 在主线程创建以后,直接 move 给了生成的线程,那么除了那个线程,没有其他的地方可以使用这个 vector。
  • 如果其他地方使用这个 vector(比如,在handle.join().unwrap() )前面尝试打印 vector,Rust 就会报错
  • 数据要在线程之间被 move 需要满足 Send trait。如果move 的变量不满足 Send,那么 Rust 将禁止程序编译通过。
    • Rc<usize>,如果 move 它,在多线程编程中就会报错
    • 报错的重要一句话是 Rc<i32> 不能 send 在 threads safely
    • 比如上面报错的 Rc。因为Rc 不是多线程安全的引用计数,对应 Rc 并且线程安全的引用计数是 Arc。

Send 的要求

  • 满足Send约束的类型,能在多线程之间安全的排它使用(Exclusive access is thread-safe),所有权类型
  • 满足Send约束的类型T,表示T&mut Tmut表示能修改这个引用,甚至于删除即drop这个数据)这两种类型的数据能在多个线程之间传递
  • 直白些:能在多个线程之间move值以及修改引用到的值

Sync

  • 需要又读又写共享的数据时,Rust 要求共享的被读写的数据满足Sync trait
  • 如果数据不满足 Sync trait,但被共享于多线程中,编译器就会报错。
  • 因为只有被保护的数据才会满足 Sync,而被保护了,就说明它可以安全地在多线程间共享
  • 比如当用 RefCell 不能用于多线程,主要的信息是RefCell cannot be shared between threads safely
  • 因为 RefCell 里面的数据结构没有被保护,所以不能用于多线程中。需要使用 Mutex 对数据进行保护,才能将数据用于多线程中读和写。所以需要将RefCell<usize>改成Mutex<RefCell<usize>>
  • 类型的实例可以被存储在可以跨线程访问的集合中,例如sd::sync::Arcstd::sync::Mutexstd::sync::RwLock

Sync 和 Send 的关系

  • Sync 可以理解为是 Send 的辅助

  • 一个类型实现了Sync trait,不一定就实现了Send trait

  • 如果一个类型实现了Send trait,那么它一定也实现了Sync trait

  • Sync:

  • 满足Sync约束的类型,能在多线程之间安全的共享使用(Shared access is thread-safe)。

  • 满足Sync约束的类型T,只表示该类型能在多个线程中读共享,即:不能move,也不能修改,仅仅只能通过引用&T来读取这个值。

  • 一个类型&T 的只有在满足Send约束的条件下,类型T 才能满足Sync约束 (a type T is Sync if and only if &T is Send)。即:T: Sync ≡ &T: Send

  • 这是因为将数据的引用发送到另一个线程并不会实际移动数据,而只是共享对数据的指针

  • 因此,如果安全地将数据的引用发送到另一个线程,则该数据本身也可以同时从多个线程访问

  • 对于那些基本的类型(primitive types)而言,比如i32类型,大多是同时满足SendSync这两个约束的,因为这些类型的共享引用(&)既能在多个多个线程中使用。

'static

  • Send 和 Sync,规定了多线程中,只能使用线程安全的数据
  • 在 Rust 里面每一个数据都具有 owner,当 owner 失效的时候,数据就被释放/析构。
  • 如果数据被用于多线程中,那么线程要么单独 own 这个数据,也就是前文所说的数据被 move 到线程中;
  • 要么数据具有'static 的生命周期

什么是'static 的生命周期呢?

  • 'static 的生命周期表示 这个变量需要存活多久就可以存活多久。满足这个条件的有三种情况:
  • 这个数据存活得跟包含它的程序一样长,也就是数据在程序退出的时候才会被释放/析构。
    • 比如static 的变量,它们在程序被 load 的时候存在,在程序被 unload 的时候释放;
    • 比如 literal string,它们保存在程序的二进制代码中
  • 这个变量是 owned type。
    • 什么是 owned type 呢?,就是这个变量是完全占有 (own) 这个数据,也就是所有权类型
    • 比如 String,Vector,还有 primitive type, usize, i64 等等。
    • 所以编写多线程程序的时候,可以选择使用 owned type 将变量 move 到线程里面(这就是第一点不共享数据);
    • 当数据是share onwership 的时候就使用引用计数,结合 Send 和 Sync 来进行多线程编程
  • 'static 生命周期的第三种情况是如果变量包含引用,那么它只包含其他'static 生命周期的引用
    • 显然,虽然这种情况,变量不拥有对应的数据,但是引用是'static,那么它也可以存活得想要多长就多长。

#type/rust #public

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions