rust基础之并发和面向对象

并发编程

多线程可能导致的问题

  • 竞争状态(Race conditions),多个线程以不一致的顺序访问数据或资源
  • 死锁(Deadlocks),两个线程相互等待对方停止使用其所拥有的资源,阻止程序继续运行
  • 只会发生在特定情况且难以稳定重现和修复的 bug

编程语言的多线程实现方式有

  • 1:1模型,1个 OS 线程对应1个语言线程

  • M:N 模型,M 个绿色线程对应 N 个 OS 线程

但是,绿色线程的 M:N 模型需要更大的语言运行时来管理这些线程,而Rust 标准库只提供了 1:1 线程模型实现

thread::spawn 的返回值类型是 JoinHandle

  • JoinHandle 是一个拥有所有权的值,
  • 调用 join 方法时,会阻塞当前线程直到 handle 所代表的线程结束
use std::thread;
fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    handle.join().unwrap();
}

使用move语义强制闭包获取值的所有权,避免主线程将闭包捕获的值丢弃

  • v的值移动到新建线程后,实际上保证了主线程不会再使用v的值
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
	println!("Here's a vector: {:?}", v);
});
drop(v); // 触发drop操作,避免
handle.join().unwrap();

消息传递

借鉴golang中使用消息传递(message passing)来确保并发安全

mpsc多个生产者,单个消费者multiple producer, single consumer)的缩写

use std::sync::mpsc;
fn main() {
    let (tx, rx) = mpsc::channel();
}

tr的方法调用

  • send ,获取其参数的所有权并移动这个值归接收者所有

rx的方法调用

  • recv,阻塞主线程执行直到从通道中接收一个值
  • try_recv,不会阻塞主线程,立刻返回Result

多个发送者可以通过clone操作来完成

let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();

共享内存

消息传递是单所有权,而共享内存则是多所有权。通常使用锁来访问共享数据。

锁的使用在大多数的编程语言中都是常规设计,但是使用起来较为繁琐

  • Mutex 是一个智能指针,lock 调用 返回 一个叫做 MutexGuard 的智能指针
  • 实现了 Deref 来指向其内部数据
  • 实现了Drop ,当 MutexGuard 离开作用域时自动释放锁
use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
    // let counter = Mutex::new(0); // 多个所有者会出现编译错误
    // let counter = Rc::new(Mutex::new(0)); //当 Rc 管理引用计数时,它必须在每一个 clone 调用时增加计数,并在每一个克隆被丢弃时减少计数。Rc 并没有使用任何并发原语,来确保改变计数的操作不会被其他线程打断。在计数出错时可能会导致诡异的 bug,比如可能会造成内存泄漏,或在使用结束之前就丢弃一个值
    // 因此需要一个类似于Rc但是并发安全的多所有权指针
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    for _ in 0..10 {
        // let counter = Rc::clone(&counter);
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    for handle in handles {
        handle.join().unwrap();
    }
    println!("Result: {}", *counter.lock().unwrap());
}

扩展并发操作

rust的语言层面的并发特性较少,以上涉及到的特性都来自标准库,但是用户可以自行实现并发

rust中的两个并发概念

  • std::marker::Sync
  • std::marker::Send

Send:允许线程间转移所有权

  • 实现Send trait的类型可在线程间转移所有权
  • 几乎所有的类型都实现了Send(Rc没有实现Send,它只用于单线程情景)
  • 任何完全由Send类型组成的类型也被标记为Send
  • 除了原始指针之外,几乎所有的基础类型都实现了Send

Sync:允许从多线程访问

  • 实现Sync的类型可以安全的被多个线程引用(如果T是Sync,那么&T就实现了Send,引用可以被安全的送往另一个线程)
  • 基础类型都实现了Sync
  • 完全由Sync类型组成的类型也是Sync
    • Rc不是Sync的
    • RefCell和Cell家族也不是Sync的
    • Mutex是Sync的

手动实现Send和Sync是不安全的,需要谨慎地进行设计才能确保安全性

面向对象

面向对象的特性为为封装,继承和多态。rust受到多编程范式的影响,通过以下方式实现了面向对象

  • struct,enum封装数据,impl实现方法
  • rust没有继承机制,但是使用trait来进行代码共享
  • 使用泛型trait约束来实现多态

动态派发

pub trait Draw {
    fn draw(&self);
}
pub struct Screen {
    // vec中不会检查元素的类型,只要求实现了Draw trait即可,因此是动态的
    pub components: Vec<Box<dyn Draw>>,
}

Trait对象执行的是动态派发

  • 将rait约束作用于泛型时,RUst编译器会执行单态化:编译器会为我们用来替换泛型类型参数的每一个具体类型生成对应函数和方法的非泛型
    实现。通过单态化生成的代码会执行静态派发(static dispatch),在编译过程中确定调用的具体方法
  • 动态派发(dynamic dispatch),即无法在编译过程中确定调用的究竟是哪一种方法。编译器会产生额外的代码以便在运行时找出希望调用的方法
  • 使用trait对象,会执行动态派发
    • 产生运行时开销
    • 阻止编译器内联方法代码,使得部分优化操作无法进行

trait对象必须保证对象安全

  • 方法的返回值不是self
  • 方法中不包含任何泛型类型参数

你可能感兴趣的:(rust,开发语言,后端)