第N次入门Rust - 13.并发


前言

这一篇介绍Rust中如何进行并发开发~
相关知识点包括线程、通道、互斥锁、并发相关trait以及异步并发介绍。


13.1 Rust中的线程介绍

不同的编程语言线程实现通常有两种方式:

名称具体实现运行时大小
1:1模型过调用OS的API来创建线程,一个系统线程对应一个程序线程需要较小的运行时
M:N模型语言自己实现的线程(绿色线程),M个绿色线程对应N个系统线程需要更大的运行时
  • 1:1模型和M:N模型对比,前者具有更小的运行时,性能更高,后者对线程运行更精细的控制及更低的上下文切换成本。
  • Rust标准库仅提供1:1模型的线程,当然第三方库应该也有提供绿色线程。
  • 对于其他编程语言,它们通常提供的线程被称为绿色(green)线程,使用绿色线程的语言会在不同数量的OS线程的上下文中执行它们。
  • Rust标准库中的std::thread模块用于支持多线程编程,提供了很多方法来创建线程、管理线程和结束线程。

13.2 Rust线程使用

13.2.1 线程的新建、阻塞和休眠

  • 创建新线程,通过thread::JoinHandle
    use std::thread;
    
    // 返回的handle值类型为thread::JoinHandle
    let handle = thread::spawn(|| {
        /* 线程中的逻辑 */
    })
    
  • 阻塞线程,通过使用thread::JoinHandle结构体的join方法:
    let handle = thread::spawn(|| { /* ... */})
    handle.join().unwrap();
    
    • JoinHandle 是一个拥有所有权的值,调用其的join方法会阻塞当前线程,并等待JoinHandle所持有的的线程结束;
  • 当前线程休眠:
    std::thread::sleep();
    
  • 示例:
    use std::thread;
    use std::time::Duration;
    
    fn main() {
        // 创建第一个子线程
        let thread_1 = thread::spawn(|| {
            for i in 1..=5 {
                println!("number {} from the spawned_1 thread!", i);
                thread::sleep(Duration::from_secs(2));
            }
        });
        
        // 创建第二个子线程
        let thread_2 = thread::spawn(|| {
           for i in 1..=5 {
                println!("number {} from the spawned_2 thread!", i);
                thread::sleep(DUration::from_secs(4));
           } 
        });
        
        // 主线程执行的代码
        for i in 1..=5 {
            println!("number {} from the main thread!", i);
            thread::sleep(Duration::from_secs(8));
        }
        
        // 阻塞主线程直到子线程执行至结束
        thread_1.join().unwrap();
        thread_2.join().unwrap();
    }
    

13.2.2 线程与 move 闭包

  • move闭包通常和thread::spawn函数一起使用,它允许你使用其它线程的数据。
  • 在创建线程使用move,可以把当前线程中的值的所有权转移到新创建的线程中。
  • 闭包虽然可以借用上下文中值,然而在线程场景下,直接在闭包中借用当前线程上下文的值,编译器无法判断新建的线程会借用该值多久,无法知道借用期间该值是否会一直有效,因此在使用spwan配合闭包创建线程时,如果需要在子线程中使用主线程的值,可以通过在闭包之前增加move关键字,强制闭包获取闭包内使用的值的所有权。
    use std::thread;
    
    fn main() {
        let v = vec![1, 2, 3];   
        let handle = thread::spawn(move || {
            println!("Here's a vector: {:?}", v);
        });
        // 如果这里drop,会报错
        // drop(v);    
        handle.join().unwrap();
    }
    
    • 使用move关键字,线程会获得其内部使用到的上下文的值的所有权,因此v的所有权归为子线程。
    • 如果此时在主线程中drop(v),由于v的所有权已经归为子线程了,因此drop操作被认为是非法的。

13.2.3 线程池

题外话

Rust的标准库并没有提供线程池实现,需要使用第三方库。

可能会有人好奇现在居然还有编程语言不提供像线程池这种这么基础的实现,Rust太原始了吧。

之前有看过相关文章,Rust之所以很多基础功能都没有提供,是因为Rust作为系统级别的编程语言,会被用在不同场景下,而不同的应用场景对同一种功能的要求会有区别,比如说开发Web时使用的线程池和开发嵌入式时使用的线程池因为资源的不同设计上应该有所侧重。类似的还有字符串、线程、序列化等基础功能。当然,我觉得Rust标准库中还是可以提供一个通用实现,对于特殊场景再让开发者选用特殊的实现即可,这样可能更能降低Rust的入门成本,这一点像Python、Golang等就就做得不错,标准包中就有线程池、JSON、Http服务器等的实现,方便开发者入门。

使用

  • 先在Cargo.toml中引入threadpool
    [dependencies]
    threadpool = "1.8.1"
    
    • 查了下文档,threadpool应该只能创建固定线程数的线程池,属于比较基础的线程池实现,实际使用应该还是用更高级的实现。
  • 创建一个线程池:
    use threadpool::ThreadPool;
    
    let thread_pool_size = 10;
    let pool = ThreadPool::new(thread_pool_size);
    
  • 使用:
    use threadpool::ThreadPool;
    
    fn main() {
        let pool = ThreadPool::new(3);
        
        for i in 1..=5 {
            pool.execute(move || {
               println!("number {} from the spawned_1 thread!", i); 
            });
        }
        
        for i in 1..=5 {
            pool.execute(move || {
               println!("number {} from the spawned_2 thread!", i); 
            });
        }
        
        for i in 1..=5 {
            println!("number {} from the main thread!", i);
        }
        
        pool.join();
    }
    

13.3 消息传递(mpsc)

  • 消息传递是一种很流行且能保证安全并发的技术。
  • 线程(或Actor)通过彼此发送消息(数据)来进行通信。比如Golang就推荐不要用共享内存来通信,要用通信来共享内存。
  • Rust标准库提供的消息传递并发实现是 通道(channel)
  • 通道由两部分组成:发送者(transmitter)和接收者(receiver),由发送者发送消息,接收者检查接收端收到的消息。当发送者或接收者任一被丢弃时可以认为通道被 关闭(closed)了。
  • 通道在标准库中的位置:std::sync::mpsc:多个生产者,单个消费者(multiple producer, single consumer)。

13.3.1 通道的基础用法

  • Rust 标准库实现通道的方式意味着一个通道可以有多个产生值的 发送(sending)端,但只能有一个消费这些值的 接收(receiving)端。
  • 创建通道、发送者、接收者相关API:
    use std::sync::mpsc;
    
    // 创建通道,返回一个数组
    // tx: 发送者 transmitter 的缩写
    // rx: 接收者 receiver 的缩写
    let (tx, rx) = mpsc::channel();
    
    // 发送者发送消息,会发生所有权转移
    // send 返回 Result<T,E>
    // 如果接收端已经被丢弃了,则没有接收对象,发送操作会返回错误。
    let v = /*传输消息*/;
    let res: Result<T, >tx.send(v);
    
    // 接收者阻塞线程接收消息,当接收到Ok表示收到有效值,通道还没有关闭
    let r1 = rx.recv().unwrap();
    // 接收者不阻塞线程接收消息,当接收到Ok表示收到有效值
    let r2 = rx.try_recv().unwrap();
    // 接收者连续接收消息,当通道关闭后迭代器结束
    for received in rx {
    	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));
            }
        });
    
        for received in rx {
            println!("Got: {}", received);
        }
    }
    

13.3.2 创建多个发送者

  • 默认情况下通过mpsc::channel()方法得到的只有一个发送者和一个接收者。当需要多个发送者给同一个接收者发送消息时,需要使用mpsc::Sender::clone()方法克隆发送者,其中参数为这个发送者值的引用:
    // 创建通道
    let (tx, rx) = mpsc::channel();
    // 创建多个发送者
    let tx1 = mpsc::Sender::clone(&tx);
    let tx2 = mpsc::Sender::clone(&tx);
    let tx3 = mpsc::Sender::clone(&tx);
    
  • 示例:
    let (tx, rx) = mpsc::channel();
    let tx1 = mpsc::Sender::clone(&tx);
    
    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);
    }
    

13.4 互斥锁(Mutex)

不同编程语言都提供了互斥锁作为并发的重要工具,Rust也不例外,互斥锁被包含在标准库中:

use std::sync::Mutex;

13.4.1 基础用法

  • 互斥锁(mutex, mutual exclusion):被加互斥锁的资源在任意时刻,其只允许被一个线程访问。
  • 当一个线程需要访问被加锁的数据,操作流程如下:
    1. 获取互斥锁;
    2. 访问数据;
    3. 归还锁;
  • 基础API:
    use std::sync::Mutex;
    
    // 初始化,将需要加锁的数据传入new方法
    let data = /* 数据 */;
    let m = Mutex::new(data);
    
    // 另一个线程获得锁,得到数据的引用
    let mut p = m.lock().unwrap();
    // 使用数据
    *p
    
  • Mutex<T>是一个智能指针,更准确的说:lock调用返回一个叫作MutexGuard的智能指针。MutexGuard实现了Deref指向其内部数据,同时MutexGuard也实现了Drop,因此当MutexGuard离开作用域时,会自动释放锁。

13.4.2 多个线程共用一个互斥锁

  • 使用互斥锁的目的是共享状态,然而根据前面move闭包的说明可知,多个子线程中直接使用主线程的互斥锁是无法通过编译的,因为move闭包只能将互斥锁移动到一个子线程中。合理的方法是使用Arc(原子引用计数)配合Mutex(互斥锁)。
  • 通过将一个互斥锁值封装进一个引用计数指针值中,并将引用计数指针克隆多份传到不同的子线程中,间接使用互斥锁,可以实现多个线程共同使用一个互斥锁。但是这里有另外一个问题:Rc<T>是线程不安全的,无法在线程间共享,此时可以使用原子引用计数Arc<T>
  • 原子引用计数Arc<T>(std::sync::Arc)是Rc<T>的线程安全版,a代表原子性(atomic)。Arc<T>Rc<T>基础功能一模一样,API基本一样,标准库类型不默认使用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());
    }
    

13.5 SyncSend trait

  • Rust语言的并发特性比较少,前面提到的并发特性都来自标准库而不是语言本身。
  • SyncSend trait 位于std::marker中,是语言层面的并发概念相关trait。 std::marker::Syncstd::marker::Send,这两个trait没有定义任何属性,只是两个标记trait。

13.5.1 Send trait

  • Send 标记 trait 表明类型的所有权可以在线程间传递
  • 几乎所有的 Rust 类型都实现了 Send ,但也有一些没有实现Send的类型,比如Rc<T>
  • 通过类型系统和trait bound确保不会将不安全的类型发送到不同的线程中。
  • 任何完全由 Send 的类型组成的类型也会自动被标记为 Send。几乎所有基本类型都是 Send 的,除了裸指针(原始指针,raw pointer)。

13.5.2 Sync trait

  • Sync 标记 trait 表明一个实现了 Sync 的类型可以安全的在多个线程中拥有其值的引用。
  • 对于任意类型 T,如果 TSync 的,则 &TT 的引用)是 Send 的,这意味着其引用就可以安全的发送到另一个线程。类似于 Send 的情况,基本类型是 Sync 的,完全由 Sync 的类型组成的类型也是 Sync 的。
  • 基础类型都是Sync,完全由Sync类型组成的类型也是Sync
  • 智能指针 Rc<T> 也不是 Sync 的,出于其不是 Send 相同的原因。RefCell<T>Cell<T> 系列类型不是 Sync 的。RefCell<T> 在运行时所进行的借用检查也不是线程安全的。
  • Mutex<T>Sync 的。

13.5.3 手动实现 SendSync 并不安全

  • 通常并不需要手动实现 SendSync trait,因为由 SendSync 的类型组成的类型,自动就是 SendSync 的。因为它们是标记 trait,甚至都不需要实现任何方法。它们只是用来加强并发相关的不可变性的。
  • 手动实现这些标记 trait 涉及到编写不安全的 Rust 代码,在创建新的由不是 SendSync 的部分构成的并发类型时需要多加小心,以确保维持其安全保证。

13.6 异步并发

13.6.1 async.await

  • Rust通过future并发模型和async/.await方案来实现异步并发。
  • async/.await是Rust内置语法,可以使异步代码像普通代码那样易于编写。
    • async:通常与fn函数定义一起使用,用于创建异步函数,返回值的类型实现了Future trait,而这个返回值需要由执行器来运行。
    • .await:不阻塞当前线程,异步等待future完成。在当前future无法执行时,.await将调度当前future让出线程控制权,由其他future继续执行。这个语法只有future对象才能调用,且必须在async函数内使用。
  • 示例:
    use futures::executor::block_on;
    use futures::join;
    
    async fn learn_data_structure() -> DataStructure { ... }
    async fn learn_algorithm(data_structure: DataStructure) { ... }
    async fn learn_rust() { ... }
    
    async fn learn_data_structure_and_algorithm() {
        let data_structure = learn_data_structure().await;
        learn_algorithm(data_structure).await;
    }
    
    async fn async_main() {
        let future1 = learn_data_structure_and_algorithm();
        let future2 = learn_rust();
        
        join!(future1, future2);
    }
    
    fn main() {
        block_on(async_main());
    }
    

async-std库

  • 需要添加依赖:
    # Cargo.toml
    [dependencies]
    async-std = "1.6.3"
    
  • task::spawn函数:生成异步任务。
    use async_std::task;
    
    let handle = task::spawn(async {
        1 + 2
    });
    
    assert_eq!(handle.await, 3);
    
  • task::block_on函数:阻塞当前线程直到任务执行结束。
    use async_std::task;
    
    fn main() {
        task::block_on(async {
            println!("hello async");
        });
    }
    
  • task::sleep函数:通过非阻塞的方式让任务等待一段时间再执行。
    use async_std::task;
    use std::time::Duration;
    
    task::sleep(Duration::from_secs(1)).await;
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要配置rust-analyzer的代码提示,你可以按照以下步骤进行操作: 1. 确保已经在你的项目中安装了rust-analyzer插件。你可以在VSCode的插件市场中搜索并安装"rust-analyzer"插件。 2. 打开VSCode的设置(可以通过菜单栏的"文件" -> "首选项" -> "设置"或者使用快捷键Ctrl + ,打开设置)。 3. 在设置页面的搜索框中输入"rust-analyzer",找到相关的设置选项。 4. 根据你的需求,配置下列常用的代码提示相关的设置: - "rust-analyzer.enable": 设置为true以启用rust-analyzer插件。 - "rust-analyzer.completion.enable": 设置为true以启用代码补全功能。 - "rust-analyzer.completion.addCallArgumentSnippets": 设置为true以自动添加函数调用时的参数提示。 - "rust-analyzer.completion.addCallParenthesis": 设置为true以自动添加函数调用时的括号。 - "rust-analyzer.completion.postfix.enable": 设置为true以启用后缀代码补全功能,例如`.if`、`.let`等。 - "rust-analyzer.hover.enable": 设置为true以启用悬停提示功能。 - "rust-analyzer.inlayHints.enable": 设置为true以启用内联提示功能。 5. 根据你的需求,可以进一步自定义配置rust-analyzer的代码提示行为。你可以在设置中找到更多相关的选项,并根据注释进行配置。 6. 保存设置,并重启VSCode使更改生效。 通过以上步骤,你可以根据自己的喜好和需求来配置rust-analyzer的代码提示功能。请注意,具体的配置选项可能会因rust-analyzer插件版本的不同而有所差异,请参考插件的官方文档或参考其它资源获取更多定制化的配置信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值