Rust 学习笔记 - 多线程

无畏并发

并发泛指:

  • Concurrent 程序的不同部分之间独立的执行
  • Parallel 程序的不同部分同时运行

Rust 无畏并发:允许你编写没有细微 Bug 的代码,并不在引入新 Bug 的情况下易于重构。

进程和线程

在大部分 OS 里,代码运行在进程(process)中,OS 同时管理多个进程。

在你的程序里,各独立部分可以同时运行,运行这些独立部分的就是线程(thread)。

多线程运行虽然可以提升性能表现,但是也会增加复杂性,无法保障各线程的执行顺序。

多线程可导致的问题:

  • 竞争状态,线程以不一致的顺序访问数据或资源
  • 死锁,两个线程彼此等待对方使用完所持有的资源,线程无法继续
  • 只在某些情况下发生的 Bug,很难可靠地复制现象和修复

实现线程的方式:

  • 通过调用 OS 的 API 来创建线程:1:1 模型,需要较小的运行时
  • 语言自己实现的线程(绿色线程):M:N 模型,需要更大的运行时

Rust 需要权衡运行时的支持,Rust 的标准库仅提供 1:1 模型的线程。

thread::spawn 创建线程

直接上代码:

use std::{thread, time::Duration};

fn main() {
    thread::spawn(|| { // 闭包
        for i in 1..10 {
            println!("number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

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

这段代码里面闭包的内容是执行不完的,因为主程序执行完,这个程序就结束了,而且每次的输出结果也不一定相同。

thread::spawn 函数的返回值类型是 JoinHandle, 通过 JoinHandle 来等待所有线程完成就可以解决上面执行不完的问题。

JoinHandle 持有值的所有权,调用其 join 方法,会阻止当前运行线程的执行,直到 handle 所表示的这些线程终结,这就可以达到等待对应的其它线程的完成的目的。

use std::{thread, time::Duration};

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

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

    handle.join().unwrap();
}
复制代码

这时分线程就可以执行完了。

使用 move 闭包

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,上面代码是会报错的,因为 v 的生命周期有可能比闭包的生命周期短,在闭包指向之前就有可能被 drop 掉了。

消息传递

消息传递是一种很流行且能保证安全并发的技术。线程通过彼此发送消息来就行通信。

Channel

Channel 由标准库提供,分为发送端和接收端,调用发送端的方法发送数据,接收端会检查和接收到达的数据,如果发送端、接收端中任意一端被丢弃了,那么 Channel 就“关闭”了。

创建

使用 mpsc::channel 函数来创建 Channelmpsc 表示 multiple producer, single consumer(多个生产者,一个消费者)。调用这个函数会返回一个 tuple(元组),第一个元素是发送端,第二个元素是接收端。

use std::{thread, sync::mpsc};

fn main() {
    let (tx, rx) = mpsc::channel();
    
    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap(); // 会移交 val 的所有权
    });

    let received = rx.recv().unwrap();

    println!("Got: {}", received);
}
复制代码

发送端的 send 方法,参数为想要发送的数据,返回 Result<T, E>,如果有问题(例如接收端已被丢弃),就会返回一个错误。

接收端的方法:

  • recv 方法:阻止当前线程执行,直到 Channel 中值被送来,一旦有值收到,就返回 Result<T, E>,当发送端关闭,就会收到一个错误。
  • try_recv 方法:不会阻塞,会立即返回 Result<T, E>,有数据到达就返回 Ok,里面也包含着数据,否则,返回错误。通常会使用循环调用来检查 try_recv 的结果。

Channel 和所有权转移

所有权在消息传递中非常重要:能帮你编写安全、并发的代码。

上面的例子中 send 方法就有移交所有权的操作。

发送多个值,看到接受者在等待

use std::{thread, sync::mpsc, 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_millis(1000));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}
复制代码

通过克隆来创建多个发送者

use std::{thread, sync::mpsc, time::Duration};

fn main() {
    let (tx, rx) = mpsc::channel();
    let tx1 = mpsc::Sender::clone(&tx);
    
    thread::spawn(move || {
        let vals = vec![
            String::from("1:hi"),
            String::from("1:from"),
            String::from("1:the"),
            String::from("1:thread"),
        ];

        for val in vals {
            tx1.send(val).unwrap();
            thread::sleep(Duration::from_millis(1000));
        }
    });

    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_millis(1000));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}
复制代码

共享状态的并发

前面的消息传递是通过通信来共享内存的,Rust 其实也支持通过共享状态来实现并发。

Channel 类似单所有权,一旦将值的所有权移至 Channel,就无法使用它了,而共享内存的并发方式类似多所有权,多个线程可以同时访问同一块内存。

Mutex

Mutexmutual exclusion(互斥锁)的简写。在同一时刻,Mutex 只允许一个线程来访问某些数据,想要访问数据线程必须首先获取互斥锁(lock)。

lock 数据结构是 mutex 的一部分,它能跟踪谁对数据拥有独占访问权。

mutex 通常被描述为通过锁定系统来保护它所持有的数据

Mutex 使用规则

  • 在使用数据之前,必须尝试获取锁(lock
  • 在使用完 mutex 所保护的数据后,必须对数据进行解锁,以便其它线程可以获取锁

Mutex<T> 的 API

通过 Mutex::new(数据) 来创建 Mutex<T>,它是一个智能指针。

访问数据前,通过 lock 方法来获取锁,会阻塞当前线程,lock 可能会失败,返回的是 MutexGuard(智能指针,实现了 DerefDrop)。

use std::sync::Mutex;

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

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

    println!("m = {:?}", m);
}
复制代码

lock 方法锁定,出了作用域会自定解锁。

多线程共享 Mutex<T>

首先看一个错误的例子:

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

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

    // 创建十个线程
    for _ in 0..10 {
        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());
}
复制代码

错误的原因就是第一次循环的时候已经将 counter 的所有权移动, 后面的线程无法取得所有权。

前面学过 Rc<T> 可以让一个值有多个所有者,但是实际用 Rc<T> 之后还是会报错,因为它没有实现 send 这个 trait 所以无法在线程间安全的传递 。

在并发场景下,可以使用 Arc<T> 来进行原子引用计数。Arc<T>Rc<T> 类似,其 API 几乎相同。A 代表 atomic,是原子的意思。Arc<T> 标准库中不默认使用,基础类型也都不是原子的,因为需要性能作为代价。

use std::{sync::{Mutex, Arc}, 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());
}
复制代码

RefCell<T>/Rc<T> VS Mutex<T>/Arc<T>

  • Mutex<T> 提供了内部可变性和 Cell 家族一样
  • 我们使用 RefCell<T> 来改变 Rc<T> 里面的内容
  • 我们使用 Mutex<T> 来改变 Arc<T> 里面的内容
  • 注意:Mutex<T> 有死锁风险

Send 和 Sync trait

Rust 语音的并发特性较少,目前将的并发特性都来自标准库(而不是语言本身)。我们无需局限于标准库的并发,可以自己实现并发。

但在 Rust 语言中有两个并发概念,也就是两种 trait:

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

注意:手动实现 SendSync 是不安全的。

Send

  • 允许线程间转移所有权
  • Rust 中几乎所有类型都实现了 Send,但 Rc<T> 没有实现 Send,所以它只适用于单线程场景
  • 任何完全由 Send 类型组成的类型也被标记为 Send
  • 除了原始指针之外,几乎所有的基础类型都是 Send

Sync

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

猜你喜欢

转载自juejin.im/post/7040833967200665613