【跟小嘉学 Rust 编程】十六、无畏并发(Fearless Concurrency)

系列文章目录

【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)
【跟小嘉学 Rust 编程】十一、编写自动化测试
【跟小嘉学 Rust 编程】十二、构建一个命令行程序
【跟小嘉学 Rust 编程】十三、函数式语言特性:迭代器和闭包
【跟小嘉学 Rust 编程】十四、关于 Cargo 和 Crates.io
【跟小嘉学 Rust 编程】十五、智能指针(Smart Point)
【跟小嘉学 Rust 编程】十六、无畏并发(Fearless Concurrency)

文章目录

  • 系列文章目录
    • @[TOC](文章目录)
  • 前言
  • 一、进程(Process)与线程(Thread)
    • 1.1、进程(Process)
    • 1.2、线程(Thread)
      • 1.2.1、线程
      • 1.2.2、多线程可能导致的问题
      • 1.2.3、实现线程方式
  • 二、多线程开发
    • 2.1、使用Thread::spwan 创建线程函数
    • 2.2、使用JoinHandle的join 方法等待所有线程执行完毕
    • 2.3、使用 move 闭包
    • 2.3、使用消息传递
      • 2.3.1、使用消息传递
      • 2.3.2、通道(Channel)
        • 2.3.2.1、使用mpsc::channel 创建 Channel
        • 2.3.2.2、发送端 send 方法
        • 2.3.2.3、接收端的
        • 2.3.2.4、发送多个值和接收等待
        • 2.3.2.5、使用 clone 来创建多个发送者
    • 2.4、共享状态并发(Shared-State Concurrency)
      • 2.4.1、使用 Mutex 每次只允许一个线程来访问数据
        • 2.4.1.1、mutex介绍
        • 2.4.1.2、mutex的规则
        • 2.4.1.3、`Mutex`的API
        • 2.4.1.4、在多线程中共享 Mutext 与原子引用计数
        • 2.4.1.5、RefCell/RC和Mutex/ARC
    • 2.5、通过 Send Trait 和Sync Trait 来扩展并发
      • 2.5.1、Send :允许线程间转移所有权
      • 2.5.2、Sync :允许线程间转移所有权
      • 2.5.3、手动实现 Send 和 Sync是不安全的
  • 总结

前言

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

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

主要教材参考 《The Rust Programming Language》


一、进程(Process)与线程(Thread)

1.1、进程(Process)

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

1.2、线程(Thread)

1.2.1、线程

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

多线程运行的好处:

  • 提升性能表现
  • 增加复杂性:无法保障各线程的执行顺序

1.2.2、多线程可能导致的问题

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

1.2.3、实现线程方式

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

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

二、多线程开发

2.1、使用Thread::spwan 创建线程函数

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

fn main() {
    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));
    }
}

主线程执行完,其他线程还没执行完

2.2、使用JoinHandle的join 方法等待所有线程执行完毕

thread::spawn 函数返回的是 JoinHandle。JoinHandle 持有值的所有权,调用其 join 方法可以等待对应的其他线程的完成。

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.join().unwrap();
}

2.3、使用 move 闭包

move 闭包通常和 thread::spawn 一起使用,它允许你使用其他线程的数据,创建线程的时候把值的所有权从一个线程转移到另外一个线程。

范例:错误示范

use std::thread;

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

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

    handle.join().unwrap();
}

此时存在有如下错误信息

error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `v`
7 |         println!("Here's a vector: {:?}", v);
  |                                           - `v` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:6:18
  |
6 |       let handle = thread::spawn(|| {
  |  __________________^
7 | |         println!("Here's a vector: {:?}", v);
8 | |     });
  | |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

根据提示信息,我们可以使用 move 关键字来修改

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

2.3、使用消息传递

2.3.1、使用消息传递

一种很流行且能够保证安全并发的技术就是:消息传递,线程或者 Actor 通过彼此发送消息(数据)来进行通信。

Go语言名言:不要用共享内存来通信,要用通信来共享内存

Rust 使用标准库中的Channel来

2.3.2、通道(Channel)

Channel 包含发送端和接收端,调用发送端端方法发送数据,接收端会检查和接受到达的数据,如果其中一段被丢弃了,那么 通道就关闭了。

2.3.2.1、使用mpsc::channel 创建 Channel

mpsc(multiple producer single consumer),多个生产者,一个消费者,返回一个 tuple,里面元素分别是发送端和接收端。

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

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

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

    let recevied = rx.recv().unwrap();
    println!("Got :{}", recevied);
}

2.3.2.2、发送端 send 方法

参数要发送的数据:返回 Result,如果有问题,就返回一个错误。 会移交所有权

2.3.2.3、接收端的

1、recv 方法

阻止当前线程执行,直到 Channel 中有值被送来,一旦有值被收到,就返回Result,当发送端关闭就会收到一个错误

2、try_recv 方法

不会阻塞,立即返回 Result,有数据达到,返回OK,里面包含数据,否则返回错误。

通常使用循环来检查 try_recv 的结果。

2.3.2.4、发送多个值和接收等待

use std::sync::mpsc;
use std::thread;
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));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}

2.3.2.5、使用 clone 来创建多个发送者

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

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

        let tx1 = tx.clone();
        thread::spawn(move || {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("thread"),
            ];
    
            for val in vals {
                tx1.send(val).unwrap();
                thread::sleep(Duration::from_secs(1));
            }
        });
    
        thread::spawn(move || {
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];
    
            for val in vals {
                tx.send(val).unwrap();
                thread::sleep(Duration::from_secs(1));
            }
        });
    
        for received in rx {
            println!("Got: {}", received);
        }
}

2.4、共享状态并发(Shared-State Concurrency)

Rust 支持通过共享状态实现并发,Channel类似单所有权,一旦值的所有权转移给Channel 就无法使用了。而共享内存并发类似多所有权,多个线程可以同时访问同一块内存。

2.4.1、使用 Mutex 每次只允许一个线程来访问数据

2.4.1.1、mutex介绍

Mutex 是 mutual exclusion(互斥锁),在同一时刻,Mutex 只允许一个线程来访问某些数据,想要访问数据,线程必须获取互斥锁(lock),lock数据结构是 mutex 的一部分,它能跟着谁对数据拥有独占访问权。

mutext 通常被描述为:通过锁来保护它所持有的数据

2.4.1.2、mutex的规则

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

2.4.1.3、Mutex的API

使用 Mutex::new(数据) 来创建 Mutex,Mutex 是一个智能指针,访问数据前通过 lock 方法来获取锁:

  • 会阻塞当前线程
  • lock可能会失败
  • 返回的是 MutexGuard(智能指针,实现了Deref 和 Drop)
use std::sync::Mutex;

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

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

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

2.4.1.4、在多线程中共享 Mutext 与原子引用计数

use std::sync::Mutex;
use std::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());
}

该代码报错,因为我们使用了move 移动了所有权,所以其他线程使用会报错。

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

Arc 原子引用计数

2.4.1.5、RefCell/RC和Mutex/ARC

  • Mutex 提供了内部可变性和 Cell 家族一样,
  • 使用 Refcell 来 改变RC 里面的内容
  • 使用 Mutex 来改变 ARC 里面的内容

使用 Mutex 存在死锁风险

2.5、通过 Send Trait 和Sync Trait 来扩展并发

Rust 语言的并发特性比较少,目前讲的并发特性都来自于标准库,无需局限于标准库的并发,可以自己实现并发。

在Rust里面有两个并发的概念

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

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

Rust中几乎所有的类型都实现了 Send,实现了 Send Trait 类型可以在线程间转移所有权,但是RC 没有实现 Send,它只适用单线程场景。

任何完全由 Send 类型组成的类型也被标记为 Send。

除了原始指针之外,几乎所有的基础类型都是 Send。

2.5.2、Sync :允许线程间转移所有权

实现 Sync 类型可以完全被多个线程引用,如果 T 是 Sync,那么 &T 就是Send,引用可以被安全的送往另个线程。

基础类型都是 Sync, 完全由 Sync 类型组成的类型也是 Sync,但是 RC不是Sync, RefCell Cell 也不是Sync,Mutex 是 Sync

2.5.3、手动实现 Send 和 Sync是不安全的

总结

以上就是今天要讲的内容

你可能感兴趣的:(跟小嘉学,Rust,编程,rust,开发语言,后端)