Rust学习笔记(下)

前言

笔记的内容主要参考与《Rust 程序设计语言》,一些也参考了《通过例子学 Rust》《Rust语言圣经》

Rust学习笔记分为上中下,其它两个地址在Rust学习笔记(上)Rust学习笔记(中)

并发

线程

在大部分现代操作系统中,已执行程序的代码在一个 进程process)中运行,操作系统则负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。运行这些独立部分的功能被称为 线程threads)。

将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这会导致诸如此类的问题:

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

需要调用 thread::spawn 函数并传递一个闭包

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

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));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }

    // 由于无法无法保证线程运行的顺序,很有可能在handle还没执行完,主线程就走到了最后,结束了线程
    // thread::spawn的返回值类型是JoinHandle。它是一个拥有所有权的值,当对其调用join方法时,会阻塞当前线程直到 handle 所代表的线程结束
    // 如果把这句话放到for i in 1..5这句前面,就会看到并不会交替输出,而是输出完handle里的,才走下面
    handle.join().unwrap();
}
move

其经常与 thread::spawn 一起使用,因为它允许我们在一个线程中使用另一个线程的数据。

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

为什么传值要用move,也就是捕获所有权?

可以避免数据竞争,比如别人也调用了,然后更改了数据。还比如主线程在let handle下面drop了v,结果drop先执行了,那么handle也会发生错误

为什么thread::spawn要用这种闭包的写法?

首先可以利用闭包的一些特性吧,比如move,后面也不用写变量什么的,直接自动全拿,函数的话还得一个一个写

为什么let handle这里直接当成个函数?

因为人家就是这么设计的,这样就能handle.join().unwrap();,而且你也可以单独写个函数包起来

消息传递

通过发送包含数据的消息来相互沟通,这个思想来源于 Go 编程语言文档中 的口号:“不要通过共享内存来通讯;而是通过通讯来共享内存。” Rust 中一个实现消息传递并发的主要工具是 通道channel

编程中的通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。当发送者或接收者任一被丢弃时可以认为通道被 关闭closed)了。

这里使用 mpsc::channel 函数创建一个新的通道;mpsc多个生产者,单个消费者multiple producer, single consumer)的缩写。简而言之,Rust 标准库实现通道的方式意味着一个通道可以有多个产生值的 发送sending)端,但只能有一个消费这些值的 接收receiving)端。

use std::thread;
use std::sync::mpsc;

fn main() {
    // 第一个元素是发送端(transmitter),而第二个元素是接收端(receiver)
    let (tx, rx) = mpsc::channel();
    // 如果想要实现多个发送者,直接clone即可
		// let tx1 = tx.clone();
    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });
		// 这个方法会阻塞主线程执行直到从通道中接收一个值。一旦发送了一个值,recv会在一个Result<T, E>中返回它。当通道发送端关闭,recv会返回一个错误表明不会再有新的值到来了。
    let received = rx.recv().unwrap();
    // try_recv不会阻塞,相反它立刻返回一个Result<T, E> Ok值包含可用的信息,而Err值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用try_recv很有用:可以编写一个循环来频繁调用try_recv,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。
    println!("Got: {}", received);
}

可以发送多个值

use std::thread;
use std::sync::mpsc;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });
		// 这里不再显式调用 recv 函数:而是将 rx 当作一个迭代器,当通道被关闭时,迭代器也将结束。
    for received in rx {
        println!("Got: {}", received);
    }
}

共享

在某种程度上,任何编程语言中的通道都类似于单所有权,因为一旦将一个值传送到通道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。

互斥器

互斥器mutex)是 mutual exclusion 的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的 lock)来表明其希望访问数据。锁是一个作为互斥器一部分的数据结构,它记录谁有数据的排他访问权。因此,我们描述互斥器为通过锁系统 保护guarding)其数据。

互斥器以难以使用著称,因为你不得不记住:

  1. 在使用数据之前尝试获取锁。
  2. 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。

正确的管理互斥器异常复杂,这也是许多人之所以热衷于通道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。

单线程上下文使用互斥器

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {:?}", m);
}

像很多类型一样,我们使用关联函数 new 来创建一个 Mutex<T>。使用 lock 方法获取锁,以访问互斥器中的数据。这个调用会阻塞当前线程,直到我们拥有锁为止。如果另一个线程拥有锁,并且那个线程 panic 了,则 lock 调用会失败。在这种情况下,没人能够再获取锁,所以这里选择 unwrap 并在遇到这种情况时使线程 panic。

一旦获取了锁,就可以将返回值(在这里是num)视为一个其内部数据的可变引用了。类型系统确保了我们在使用 m 中的值之前获取锁:Mutex<i32> 并不是一个 i32,所以 必须 获取锁才能使用这个 i32 值。我们是不会忘记这么做的,因为反之类型系统不允许访问内部的 i32 值。

正如你所怀疑的,Mutex<T> 是一个智能指针。更准确的说,lock 调用 返回 一个叫做 MutexGuard 的智能指针。这个智能指针实现了 Deref 来指向其内部数据;其也提供了一个 Drop 实现当 MutexGuard 离开作用域时自动释放锁,这正发生于示例 内部作用域的结尾。为此,我们不会冒忘记释放锁并阻塞互斥器为其它线程所用的风险,因为锁的释放是自动发生的。

丢弃了锁之后,可以打印出互斥器的值,并发现能够将其内部的 i32 改为 6。

在多线程中

// 这是一个很棒的例子理解多线程的运行顺序
// 所有代码都会顺序执行,只不过有子线程出现后,它会自己不停的执行,此时主线程也在执行
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    let mut handles = vec![];

    for _ in 0..10 {
        // 这里会报错因为counter的所有权被移走了
        // 所以可以用Rc,但这仍是错的,当 Rc<T> 管理引用计数时,它必须在每一个 clone 调用时增加计数,并在每一个克隆被丢弃时减少计数。Rc<T> 并没有使用任何并发原语,来确保改变计数的操作不会被其他线程打断。
        // let counter = Rc::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());
}

Arc<T> 正是 一个类似 Rc<T> 并可以安全的用于并发环境的类型。字母 “a” 代表 原子性atomic),所以这是一个原子引用计数atomically reference counted)类型。

你可能会好奇为什么不是所有的原始类型都是原子性的?为什么不是所有标准库中的类型都默认使用 Arc<T> 实现?原因在于线程安全带有性能惩罚,我们希望只在必要时才为此买单。如果只是在单线程中对值进行操作,原子性提供的保证并无必要,代码可以因此运行的更快。

use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        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());
}

也可以使用scope,这样就不需要像Arc那样共享值了,而且也不用加join

thread::scope(|s| {
  s.spawn(|| {
    for chunk in chunks[0] {
      let _ = calculate(chunk.0.clone(), chunk.1.clone());
    }
  });
  s.spawn(|| {
    for chunk in chunks[1] {
      let _ = calculate(chunk.0.clone(), chunk.1.clone());
  }
});
RefCell/Rc 与 Mutex/Arc的相似性

你可能注意到了,因为 counter 是不可变的,不过可以获取其内部值的可变引用;这意味着 Mutex<T> 提供了内部可变性,就像 Cell 系列类型那样。正如第十五章中使用 RefCell<T> 可以改变 Rc<T> 中的内容那样,同样的可以使用 Mutex<T> 来改变 Arc<T> 中的内容。

另一个值得注意的细节是 Rust 不能避免使用 Mutex<T> 的全部逻辑错误。回忆一下第十五章使用 Rc<T> 就有造成引用循环的风险,这时两个 Rc<T> 值相互引用,造成内存泄露。同理,Mutex<T> 也有造成 死锁deadlock) 的风险。这发生于当一个操作需要锁住两个资源而两个线程各持一个锁,这会造成它们永远相互等待。

使用 Sync 和 Send trait 的可扩展并发

Send 是多线程环境里的 move 语义;Sync 是多线程环境里的 borrow 语义。几乎所有类型都是满足它俩的。而 Rc<T> 是不满足的,但String 是不满足的。String 可以 move,那么这个线程就会失去它的所有权,当然它也可以让其它线程 borrow。那为什么 Rc<T> 不行呢?它不能失去所有权吗?我觉得官方讲的不好,我的理解是,这需要考虑 Rc<T> 的使用意义。它的意义是去引用计数,如果它 move 到别的地方,那本地的怎么计数,而且其它线程计数都不一样,可以用吗?对于 Sync 同理,它让别的线程 borrow 有意义吗?而像 String,它 move 和 borrow 是有意义的,别的线程可以读取这个值。

对于 Send 和 Sync 的关系,对于任意类型 T,如果 &T(T 的引用)是 Send 的话 T 就是 Sync 的。这里我有个困惑,引用能转移所有权吗?我觉的官方既然这么写,就这么理解吧,那就也有所有权。那这句话其实就是 &T 能 move,那不就是 borrow 吗,那不就 T 满足 Sync 吗。

最后举个满足 Send 不满足 Sync 的例子(满足 Sync 但不满足 Send 也有例子),比如 Cell,它不用声明 mut 就可以更改其值,因为它改值时是直接将新值接 move 进去,旧值直接 move 出去,所以当你 borrow 时,这个线程 move 新值进去,没有旧值的所有权,所以不行。

测试

单元测试

测试的命令为 cargo test,格式大概如下,通过使用断言判断测试成不成功。assert_eq!:两个值相等,则成功;assert_ne!:两个值不相等,则成功,用在我们不确定值会是什么,不过能确定值绝对不会是什么的时候;assert!:判断是不是 ture。还可以像这样去匹配一个Result值assert!(matches!(result, Ok((vec![52.0, 52.0]))))

// cfg为configuration,这个注解告诉Rust只在cargo test时才编译运行代码,而在cargo build时不这么做
// 在只希望构建库时可以节约时间
#[cfg(test)]
mod tests {
    use super::*;

    //这个属性表明这是一个测试函数
    #[test]
    fn larger_can_hold_smaller() {
        // --snip--
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle { width: 8, height: 7 };
        let smaller = Rectangle { width: 5, height: 1 };

        assert!(!smaller.can_hold(&larger));
    }
}

断言也可以输出更多信息,像下面一样。不过要注意,比如这个 result 是个结构体,也就是自定义的,需要实现 PartialEq(可以比较两个struct是不是想等) 和 Debug(可以打印struct)。只需在结构体上加 #[derive(PartialEq, Debug)] 即可。

#[test]
fn greeting_contains_name() {
    let result = greeting("Carol");
    assert!(
        result.contains("Carol"),
        "Greeting did not contain name, value was `{}`", result
    );
}

也可以检查是否panic,或者panic出的内容相不相同。

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    // expected可以额外加,判断报的错一不一样
    #[should_panic(expected = "Guess value must be less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

还有运行时的命令也可改变。默认时并行的,如果想按顺序执行,比如这个要删这个文件,呢个要读,这就会出问题,可以 cargo test -- --test-threads=1。默认代码中的 println! 是无效的,如果你希望看到它,可以 cargo test -- --nocapture。如果想专门指定某个测试,可以cargo test one_hundred。也可以模糊的,比如只执行 add 开头的 cargo test add。如果想忽略某个测试,可以在文件中标注 [ignore]。如果想单独运行时忽略的可以输入 cargo test -- --ignored

#[test]
#[ignore]
fn expensive_test() {
    // 需要运行一个小时的代码
}

最后一点,它也可以测试私有函数。

集成测试

集成测试的目的是测试库的多个部分能否一起正常工作。为了编写集成测试,需要在项目根目录创建一个 tests 目录,与 src 同级,Cargo 知道如何去寻找这个目录中的集成测试文件。集成测试因为位于另一个文件夹,所以它们并不需要 #[cfg(test)] 注解(因为 cargo build 本身就不会运行)。并且因为它对于需要测试的文件是外部的,所以只能调用共有的 API。下面是一个例子,文件在tests/integration_test.rs。

use adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}

文档测试

也是用 cargo test 启动。

/// 第一行是对函数的简短描述。
/// 接下来数行是详细文档。
/// ```
/// let result = doccomments::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

不安全Rust

Unsafe Rust它没有强制内存安全保证,它提供了额外的“超能力”。

存在的原因:

  • 静态分析是保守的。使用Unsafe Rust也就意味着自己需要承担风险。
  • 计算机硬件本身就是不安全的,Rust需要能够进行底层系统编程。

超能力:

  • 解引用原始指针
  • 调用unsafe函数或方法
  • 访问或修改可变的静态变量
  • 实现unsafe trait

注意:

  • unsafe并没有关闭借用检查或停用其他安全检查,只有上面4种可以使用
  • 任何内存安全相关的错误必须留在unsafe块里
  • 尽可能隔离unsafe代码,最好将其封装在安全的抽象里,提供安全API
解引用原始指针

原始指针:

  • 可变的:*mut T
  • 不可变的:*const T
  • 注意:这里的 * 不是解引用符号,它是类型名的一部分

与引用不同,原始指针:

  • 允许通过同时具有不可变和可变指针或多个指向同一位置的可变指针来忽略借用规则(违背了之前呢些不能同时两个 mut 呢些)
  • 无法保证能指向合理的内存
  • 允许为null
  • 不实现任何自动清理

放弃保证的安全,换取更好的性能/与其它语言或硬件接口的能力

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
    println!("r1 is: {}", *r1);
    println!("r2 is: {}", *r2);
}

// 未知地址
let address = 0x012345usize;
let r = address as *const i32;
unsafe {
    // 执行时这里会报错
    println!("r is: {}", *r);
}

为什么要使用解引用原始指针

  • 与C语言进行接口
  • 构建借用检查器无法理解的安全抽象
调用unsafe函数或方法

unsafe函数或方法:在定义前加上了unsafe关键字

  • 调用前需手动满足一些条件(主要靠看文档),因为Rust无法对这些条件进行验证
  • 需要在unsafe块里进行调用
unsafe fn dangerous() {}

fn main {
  unsafe {
    dangerous();
  }
}
创建 unsafe 代码的安全抽象
  • 函数包含 unsafe 代码并不意味着需要将整个函数标记为 unsafe
  • 将 unsafe 代码包裹在安全函数中是一个常见的抽象
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (slice::from_raw_parts_mut(ptr, mid),
         slice::from_raw_parts_mut(ptr.offset(mid as isize), len - mid))
    }
}

fn main() {
    let address = 0x01234usize;
  	let r = address as *mut i32;

  	let slice: &[i32] = unsafe {
      	slice::from_raw_parts_mut(r, 10000)
  	};
}
使用 extern 函数调用外部代码
  • extern 关键字:简化创建和使用外部函数接口(FFI)的过程
  • 外部函数接口(FFI,Foreign Function Interface):它允许一种编程语言定义函数,并让其它编程语言能调用这些函数
extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
  • 应用二进制接口(ABI,Application Binary Interface):定义函数在汇编层的调用方式
  • “C” ABI 是最常见的 ABI,它遵循 C 语言的 ABI
从其它语言调用Rust函数
  • 可以使用 extern 创建接口,其它语言通过它们调用 Rust 的函数
  • 在 fn 前添加 extern 关键字,并制定 ABI
  • 还需添加 #[no_mangle]注解:避免 Rust 在编译时改变它的名称
#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}
访问或修改一个可变静态变量
  • 在Rust里,全局变量叫做静态(stctic)变量
  • 静态变量可以是可变的,访问和修改静态可变变量是不安全的
// 省略了'static
static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);
    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}
实现不安全 trait
  • 当某个 trait 中存在至少一个方法拥有编译器无法校验的不安全因素时,就称这个 trait 事不安全的
  • 声明 unsafe trait,在定义前加 unsafe 关键字
unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}
何时使用 unsafe 代码
  • 编译器无法保证内存安全,保证 unsafe 代码正确并不简单
  • 有充足理由使用 unsafe 代码时,就可以这样做
  • 通过显示标记 unsafe,可以在出现问题时轻松的定位

一些例子,不细学了

macro_rules! avg {
    // $用来匹配表达式
    // ($num:expr),*:表示一个数字加一个逗号或不加(+代表必须有一个)
    ($($num:expr),*) => {
        // 为什么这里还要加一个{}呢?
        // 因为外部是“let a =”,你不加{}代码就像let a = let mut len = 0;
        {
            let mut sum = 0;
            let mut len = 0;
            // 相当于一个一个的取出上面的表达式
            $(
                sum += $num;
                len += 1;
            )*  // 这里也能用+,表示至少有一个

            sum / len
        }
    }
}


// 使用
let a = avg!(1, 2, 3, 4, 5);
// 我之前有个地方理解错了
    // 首先+表示至少有一个,说的时前面的expr,而不是","
    // 这里的逗号,应该把它叫做分隔符,所以下面“true”后面没有跟逗号也能匹配上
    ($($expr:expr),+; $block:block) => {
        // 重复expr并用||连接
        // 可以省略+后的大括号
        if $($expr)||+ $block
    };


if_any!(false, 0 == 1, true; {
        print_success();
    })
  • 29
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值