Send && Sync In Rust


注意:该文章的理解都只是个人理解和推理,欢迎指出错误

Why Send && Sync

**SendSync**是Rust语言中用于确保并发安全的两个marker特征(marker trait),这意味着Send和Sync本身不提供任何额外功能,只是用于标记。Rust标准库中提供的并发类型会被标记好表示该工具能够用于并发场景,而对于用户自己的实现,当其确保其能够用于并发场景下(这意味着用户需要自己实现互斥等并发安全的操作),可以通过unsafe进行标记。

Rust是如何保证并发安全呢?

Rust通过类型系统进行保证,具体来说,实现**SendSync** trait的类型表示能够在并发场景下被安全的使用。因此,在涉及到并发场景的函数中,可以通过trait bound来限制传入的参数必须实现以上两个trait,保证其能够安全访问。

例如,**thread::spawn函数用来创建新线程并执行某个函数,它要求传递进来的函数的类型必须实现Send**特征。

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T + Send + 'static,
    T: Send + 'static,

如下面,如果传入的函数中使用了没有实现Send trait的类型,则编译器会报错

let pointer = Rc::new(1);
thread::spawn(move || {
  println!("{}",pointer)
});

error[E0277]: `Rc<i32>` cannot be sent between threads safely
   --> src/main.rs:7:19
    |
7   |       thread::spawn(move || {
    |       ------------- ^------
    |       |             |
    |  _____|_____________within this `{closure@src/main.rs:7:19: 7:26}`
    | |     |
    | |     required by a bound introduced by this call
8   | |         println!("{}",pointer)
9   | |     });
    | |_____^ `Rc<i32>` cannot be sent between threads safely
    |
    = help: within `{closure@src/main.rs:7:19: 7:26}`, the trait `Send` is not implemented for `Rc<i32>`
note: required because it's used within this closure

本文将从定义,再到具体例子,去探究:为什么Send和Sync可以保证安全性?

定义

  • Send的定义

    Types that can be transferred across thread boundaries.

    **Send特征的定义是,任何被标记为Send**的类型都可以安全地传递到另外一个线程。

    在Rust中,将一个值传递到另外一个线程这件事,本质是通过将closure捕获值,然后将closure传递到spawn函数完成的。比如上面的例子,在使用**thread::spawn函数创建新线程时,该函数要求传入的闭包(或函数)必须满足Send特征。这意味着,如果你想要在这个闭包中使用某个变量,那么这个变量的类型也必须实现Send特征。这样的要求确保了只有实现了Send**特征的类型(即,可以被安全地传递到另外一个线程的类型)才可以被传递到其他线程。

  • Sync的定义通常是:&T is Send if and only if T is Sync

    对于这句话的理解,我觉得最直观的方式是直接看实现:

    unsafe impl<T: Sync + ?Sized> Send for &T {}
    

    对于实现了Sync的type,会为其引用类型自动实现Send。因此,我认为Sync的本质是对其引用类型实现Send,如果我们将引用看作一种类型,对于Sync如何保证安全性的讨论其实就等价于Send如何保证安全性的讨论。

安全性保证

💡 前提:以下的分析将不可变引用,可变引用以及类型本身视为三种类型,即T, &T, &mut T是三种类型。

对于并发安全问题,本质是由于一块可变内存被共享,导致内存可能被同时修改,发生了一些在线性执行下不会发生的问题。

将Rust中类型做一个简单归类,分别讨论他们在并发场景下的安全性:

  1. 可变,且共享:&RefCell<Vec<i32>>
  2. 不可变,且共享:&Vec<i32>
  3. 可变,不共享:&mut Vec<i32>
  4. 不可变,不共享:Vec<i32>

这里的共享指的是,当前实例化的值同时也可以被其他值访问到。

对于2,3,4类,由于其不会涉及到并发修改,因此都是并发安全的,下面的程序进行2,3,4的行为,结果是可以通过编译的。

fn main() {
    use std::thread;
    use std::rc::Rc;
    
    let mut v = vec![1];

    // 2: Move &Vec<T> to another thread
    // 3: Move &mut Vec<T> to another thread
    thread::scope(|s| {
        s.spawn(||{
            (&mut v).push(1);
            let _ = (&v).len();
        });
    });
 
    println!("after: {:?}",v);   
    
    // 4: Move Vec<T> to another thread
    let handle = thread::spawn(move || {
        v.push(2);
    });
    handle.join();
}

从Vec的定义可以看到,以上的类型确实实现了Send(Sync表示&Vec实现了Send

Untitled.png

而对于1,可变且共享意味着该值会存在并发访问,因此对于这种类型的并发安全性,一般有两种可能

  1. !Send,防止该类型被用于并发场景
  2. Send,对于访问操作需要实现互斥操作

几个具体的例子:

  • RefCell<Vec>是Send&&!Sync。
    • RefCell:RefCell的值本身可以被传递到另外一个线程,因为RefCell以及Vec都是对持有的内存进行独占。因此,RefCell本身属于不共享,因此标记了Send,可以被安全的传递到另外一个线程。
    • &RefCell:&RefCell可以用于获取其内部的mutable reference,且没有做任何互斥保护。因此&RefCell属于可变可共享,由于RefCell本身没有做互斥保护,因此需要标记为!Send,即RefCell标记为!Sync。
  • Mutex和RefCell一样,可以通过其引用获取到内部数据的mutable reference。但是,Mutex对这个获取的操作做了互斥保护,可以在运行时保证同一时间只有一个线程会获取mutable reference,因此它是Sync的。
  • Rc是一个带引用计数的智能指针,当Rc被drop时,会减少其引用计数,若为0,则将对象释放掉。但是Rc对其引用计数的操作不是原子的,因此Rc只能用于单线程,Rc是Not Send和Not Sync。
    • 首先Rc会共享引用计数,且在Drop时会操作引用计数。因此Rc本身属于可变可共享,由于Drop操作引用计数的方法不是互斥的,因此Rc是Not Send的。
    • &Rc可以通过Clone来增加引用计数,这意味&Rc同样是可变可共享,同理,其也是!Send,即Rc是!Sync。

Arc

Arc是一个比较特殊例子,因此单独拎出来。

A thread-safe reference-counting pointer.
The type Arc<T> provides shared ownership of a value of type T

💡 简单说明下Arc:
Arc是一种延长引用生命周期的方式(本质是共享指针,使用了引用计数),在下面的例子中,1.会报错,因为线程运行中会使用来自five的引用,但其无法确保five的生命周期会一直持续到线程结束,因此Rust注明spawn必须使用生命周期为全局的数据。通过Arc,可以使得five的生命周期会一直持续到所有Arc Drop为止。

fn main() {
	use std::sync::Arc;
	use std::thread;
	
	// 1. 报错
	let five = 5;
	thread::spawn(|| {
        println!("{}", &five);
  });

  // 2. 成功
	let five = Arc::new(5);
  thread::spawn(move || {
    println!("{}", &five);
  });
}

从上面的例子可以看到,其实可以把Arc的本质看作引用,Arc看作&T,意味着Arc是可共享的。

因此,而由于Arc内部的ref count都是原子实现的,因此Arc本身的数据是并发安全的,因此,可以推断:Arc具体是不是并发安全,取决&T是不是并发安全的。

可以看到Arc对Send和Sync的实现符合推断:

#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Sync + Send, A: Allocator + Send> Send for Arc<T, A> {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Sync + Send, A: Allocator + Sync> Sync for Arc<T, A> {}

Send for Async block && Closure

关于AsyncBlock和Closure是否为Send的规则,可以参考:

  1. https://rust-lang.github.io/async-book/07_workarounds/03_send_approximation.html
  2. https://doc.rust-lang.org/reference/types/closure.html#other-traits

Async Block

async block其实是状态机,状态机需要存储await之间需要的状态,因此其是否为Send的规则为:当async block在await时持有一个非Send的变量,则不满足Send,(因为该变量需要保存在状态机中)。

接下来通过例子说明,如case1,由于value没有跨越await point,因此第一个case1可以通过;如case2,由于value跨越await point,因此第二个case2无法通过。

// Pass! 
async fn case1() {
    let t = async move {
        let value = Rc::new(RefCell::new(0));
        println!("{}", value.borrow());
    };
    require_send(t);
}

// Fail!
async fn case2() {
    let t = async move {
        let value = Rc::new(RefCell::new(0));
        async {}.await;
        println!("{}", value.borrow());
    };
    require_send(t);
}

此外,若async block捕捉了外部非Send的变量,由于该变量需要记录为状态机的初始状态,因此此时也不满足Send。

如下的case,async block捕获了外部的value,因此不满足Send

async fn case() {
    let value = Rc::new(RefCell::new(0));
    let t = async move {
        let value = value;
        println!("{}", value.borrow());
    };
    require_send(t);
}

Closure

closure可以视为没有await point的async block,因此Send主要取决于捕获的变量。

如下的case,closure捕获了外部!Send的变量,因此其为!Send。

    let value = Rc::new(RefCell::new(0));
    let t = move || {
        println!("{}", value.borrow());
    };
    require_send(t);

总结

到了这里,我们梳理了Send和Sync是如何保证安全性的:

  1. 通过类型系统,Send和Sync只是一种标记
  2. 会出现并发安全的类型在于可变可共享
  3. 对于可变可共享的类型,如果内部实现了互斥机制,会被标志为Send,如果没有,就通过标记!Send防止其被并发使用

Ref

  1. https://www.youtube.com/watch?v=yOezcP-XaIw&t=1374s&ab_channel=JonGjengset
#Rust