< 返回版块

洋芋 发表于 2020-01-09 10:02

Tags:rust, 每周一知, 线程安全

本文以RcRefCell为例(参考了张汉东老师《Rust编程之道》书中的示例),讨论Rust中的SendSync是如何保证线程安全的。

基本概念

SendSync位于标准库std::marker模块中。它们属于标记trait,也就是说,它们没有方法,也没有内置任何功能。它们的作用是:

  • 如果类型T实现了Send,则将类型T的值传递给另一个线程不会导致数据争用(data rases)或其他不安全性
  • 如果类型T实现了Sync,则将类型T的引用&T传递到另一个线程中不会导致数据争用或其他不安全性(T: Sync暗含着&T: Send

也就是说,Sync与类型跨多个线程共享时有关,而Send则讨论类型被move到另一个线程的行为方式。

Allowing Transference of Ownership Between Threads with Send Send允许在线程间转移所有权

Allowing Access from Multiple Threads with Sync Sync允许多线程访问

在Rust的标准库std::marker模块中,为所有类型默认实现了SendSync

线程

Rust与线程相关的内容位于标准库std::thread模块中。Rust中的线程,是对操作系统线程的直接封装。也就是说是本地线程,每个线程都有自己的栈和本地状态。

use std::thread;
use std::time::Duration;

fn main() {
	 // 使用Builder模式为创建的线程t指定一些参数
    let t = thread::Builder::new()
        .name("t".to_string())
        // 创建线程t,使用move转移所有权
        .spawn(move || {
        	  // 线程t的执行逻辑
            println!("enter child thread.");
            // 暂停当前线程,进入等待状态
            thread::park();
            println!("resume child thread");
        }).unwrap();
    println!("spawn a thread");
    // 使当前线程等待一段时间继续执行
    thread::sleep(Duration::new(5,0));
    // 恢复线程t的执行
    t.thread().unpark();
    // 主线程等待线程t结束
    t.join();
    println!("child thread finished");
}

std::thread::spawn函数的函数签名如下:

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

通过函数签名我们可以看出,spawn()接受一个可调用的(通常是一个闭包),其被调用一次,并包含Send'static的数据。也就是说只有实现了Send的类型才可以在线程间传递。

同时'static限定阻止线程之间共享借用的数据。闭包可以捕获外部变量,但默认情况下它是通过引用捕获的。示例代码中如果没有move关键字,则闭包将不会是'static的,因为它包含借用的数据。

RcRefCell示例

线程间传递可变字符串。

Rc表示“Reference Counted”(引用计数),单线程引用计数指针。

use std::thread;
use std::rc::Rc;

fn main() {
    let mut s = Rc::new("example".to_string());
    for _ in 0..2 {
        let mut s_clone = s.clone();
        thread::spawn(move || {
            s_clone.push_str(" Send and Sync!");
        });
    }
}

编译会报错,错误信息告诉我们,std::rc::Rc<std::string::String>无法在线程之间安全地发送。

这是因为Rc<String>没有实现Send。我们可以使用Arc<T>来共享所有权。

use std::thread;
//use std::rc::Rc;
use std::sync::Arc;

fn main() {
    //let mut s = Rc::new("example".to_string());
    let s = Arc::new("example".to_string());
    for _ in 0..2 {
        //let mut s_clone = s.clone();
        let s_clone = s.clone();
        thread::spawn(move || {
            s_clone.push_str(" Send and Sync!");
        });
    }
}

编译还是报错,错误信息告诉我们,把不可变借用当作可变借用了。

这是因为Arc<T>默认是不可变的。我们可以使用之前文章中提到的具有内部可变性的类型。

RefCell表示可变的内存位置,运行时检查借用规则。

use std::thread;
//use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;

fn main() {
    //let mut s = Rc::new("example".to_string());
    let s = Arc::new(RefCell::new("example".to_string()));
    for _ in 0..2 {
        //let mut s_clone = s.clone();
        let s_clone = s.clone();
        thread::spawn(move || {
            let s_clone = s_clone.borrow_mut();
            s_clone.push_str(" Send and Sync!");
        });
    }
}

编译再次报错,错误信息告诉我们,std::cell::RefCell<std::string::String>无法在线程之间安全地共享。

这是因为RefCell<String>没有实现Sync

结语

Rust通过SendSync这两个标记trait,将类型贴上“标签”,由编译器识别类型是否可以在多个线程之间移动或共享,在编译期间发现问题,消除数据竞争,从而保证线程安全。

评论区

写评论

还没有评论

1 共 0 条评论, 1 页