细说Rust语言的所有权机制和线程安全

文章目录

      • 1. 先来定义一个基础类Point
      • 2. Rust新开一个线程
      • 3. Rc引用计数
      • 4. Arc原子引用计数
      • 5. Mutex给内存“上锁”
      • 6. Rust多线程共享读写的实现
      • 7. 总结

Rust所有权机制的设计是这门语言的最大特点,也是其内存安全多线程并发安全的基石。

上一篇详细介绍了Rust语言的所有权机制和内存安全。

本文接着探讨Rust所有权机制下,如何实现多线程的并发数据读写,以及解释为什么说Rust是天然并发安全的一门语言

1. 先来定义一个基础类Point

struct Point {
    x: i32,
    y: i32,
}

struct从C语言借鉴而来,用来定义一个结构体。

Point结构体中包含x和y两个32位int类型的成员。

2. Rust新开一个线程

通过thread::spawn传入一个闭包,开启Rust的线程,返回线程的引用。

fn test_thread() {
    let h = thread::spawn(|| {
        for i in 0..10 {
            println!("{}", i);
            thread::sleep(Duration::from_millis(1000));
        }
    });
    // 主线程等待h线程执行完毕
    h.join();
}

如何实现主线程与新开线程的数据共享呢?

通过move关键字,能够解决所有权的单向转移

// 通过move关键字将转移所有权到闭包里面
// 注意转移所有权后,p1就失去了所有权。
fn test_thread_move() {
    let p1 = Point { x: 25, y: 25 };

    let h = thread::spawn(move || {
        println!("{} {}", p1.x, p1.y);
    });

    // p1的所有权已经通过move转移,下面这句编译报错"borrow of moved value: `p1`"
    // println!("{} {}", p1.x, p1.y);
    h.join();
}

到目前为止,一块内存数据的所有权,在同一时刻实际上只会有一个线程拥有所有权。

那么如何实现,多个线程的共享读、甚至是共享读写呢?

3. Rc引用计数

RcReference Count,引用计数。Rc保证引用的内存数据在堆内存上进行分配,其实堆内存数据才适合在多线程间共享访问。反过来思考,在线程私有栈上分配的内存,当然不应该在多线程间共享啦。

引用计数的计数值增加是通过Rc::clone来完成的。

fn test_Rc() {
    //多线程数据共享,数据应该在堆上分配,通过Rc智能指针
    let p = Rc::new(Point { x: 25, y: 25 });
    
    // Rc所有权的克隆
    let p1 = Rc::clone(&p);
    let p2 = Rc::clone(&p);
	
    // p/p1/p2这3个智能指针都指向了同一块堆内存(通过引用计数值来跟踪还有几个引用者)
    println!("{} {} {}", p.x, p1.x, p2.x);
}

4. Arc原子引用计数

尽管Rc引用计数能够克隆出多个独立的引用,但是不好意思,Rc引用无法转移到新的线程去(Rc类型没有实现Send trait)进行访问,否则编译器直接报错。

`std::rc::Rc<Point>` cannot be sent between threads safely

要真正实现跨线程的访问,需要借助ArcAtomic Reference Count),原子引用计数。

// 通过Arc在多线程之间共享读
fn test_Arc() {
    let p = Arc::new(Point { x: 25, y: 25 });

    // 克隆出所有权p1,然后转移到新线程进行访问
    let p1 = Arc::clone(&p);
    let h1 = thread::spawn(move || {
        println!("{} {}", p1.x, p1.y);
    });

    // 同理
    let p2 = Arc::clone(&p);
    let h2 = thread::spawn(move || {
        println!("{} {}", p2.x, p2.y);
    });
    h1.join();
    h2.join();
}

至此,我们了解到了,如何在多个线程中对同一块儿内存数据进行共享读操作。

5. Mutex给内存“上锁”

介绍多线程共享写之前,必须先了解Mutex,因为Rust限制多个线程写同一块内存,线程必须先获得这块内存相应的锁。

fn test_Mutex() {
    // Rust Mutex保护一份数据,被保护的数据,任何时刻只有一个线程可以修改
    // 猜测内存分布上是,p_mutex指向一个智能指针对象(里面含有一把锁),智能指针才指向具体数据
    // 另外要完成修改,必须加mut(mutable)关键字
    let mut p_mutex = Mutex::new(Point { x: 0, y: 0 });

    // Mutex包装的对象上会持有一把锁,写数据之前必须先获得锁
    let mut p = p_mutex.lock().unwrap();
    p.x += 1;
    println!("{} {}", p.x, p.y);
}

6. Rust多线程共享读写的实现

通过前面的介绍,我们知道Arc能够实现多线程共享读数据,mut Mutex能够保证对数据的写操作是互斥的。

那么一个自然的想法就是,Arc包装Mutex 就能实现多线程的共享读和互斥写。

fn test_ArcMutex() {
    // 内存分布:栈变量p_arc_mutex-->堆Arc对象(内含引用计数)-->堆Mutex对象(内含锁)-->堆对象Point(x,y)
    let mut p_arc_mutex = Arc::new(Mutex::new(Point { x: 0, y: 0 }));

    // 可以看到,共享的是不可变的堆Arc对象,这叫做“共享不可变”
    let p1 = Arc::clone(&p_arc_mutex);
    let h1 = thread::spawn(move || {
        let mut p = p1.lock().unwrap();
        p.x += 1;
        println!("{} {}", p.x, p.y);
    });

    h1.join();
    let p = p_arc_mutex.lock().unwrap();
    println!("{} {}", p.x, p.y);
}

7. 总结

  1. Rust语言通过所有权机制的设计,严格限定一块内存在同一个时刻只会被一个变量所持有,有效避免了其他语言中经常出现的野指针等内存错误,从而保证了内存安全。
  2. Rust语言保证,编译通过的代码,在多线程环境下是并发安全的。
    • 多线程下写一个变量,这个变量必须是Mutex包装过的。Mutex智能指针保护的数据,任何时刻只有一个线程可以修改,这叫“可变不共享”。
    • Arc通过增加引用计数值,在多个线程之间共享,但Arc这个对象本身是不变的,这叫做“共享不可变”。

你可能感兴趣的:(Rust编程小知识,rust所有权,rust线程安全,rust,Mutex,rust,Rc/Arc,rust,move语义)