【一起学Rust | 基础篇】rust线程与并发

文章目录


前言

并发编程 (Concurrent programming),指的是程序的不同部分相互独立的执行。而并行编程 (parallel programming)代表程序不同部分于同时执行,这两个概念随着计算机越来越多的利用多处理器的优势时显得愈发重要。由于历史原因,在此类编程中一直是困难且容易出错的:Rust

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


一、创建线程

Rust创建线程是通过thread::spawn函数来创建的,我们只要通过这个函数,并且传入一个闭包即可创建出一个线程。

以两个线程同时输出数字为例,主线程输出1到5,子线程输出1到10。其代码如下所示

thread::spawn函数内的闭包就是子线程的内容,以外的全都是主线程的内容。
thread::sleep是一个线程延时的函数,需要传入DurationDuration是个表示时间单位的,这里Duration::from_millis(1)代表1毫秒,Rust支持更加精确的时间,你可以去看一下这个类,支持微秒纳秒等。

rust 复制代码
    thread::spawn(|| {
        for i in 1..10 {
            println!("数字是:{},来自spawn创建的线程", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    for i in 1..5 {
        println!("数字是:{},来自主线程线程", i);
        thread::sleep(Duration::from_millis(1));
    }

这样就创建好一个线程了,执行效果如下图
从图中可以看出主线程输出了从1到4的数字编号,因为到了5的时候已经执行完毕了,但是子线程却是执行到了5,并没有向后执行,这是因为主线程已经结束了,此时会把子线程也销毁,简而言之就是子线程的生存周期超出了主线程的生存周期,主线程提前结束。

原因:无法保证其执行顺序,主线程提前结束

为了解决这个,就需要接收这个线程的返回值,然后调用join方法来让主线程等待子线程执行完毕再结束运行。

thread::spawn返回一个JoinHandle,其有一个join方法,可以让主线程等待该线程完毕后再结束。

JoinHandle 是一个拥有所有权的值,当对其调用 join 方法时,它会等待其线程结束。join 放在循环之前可以实现先执行子线程【这种操作会变得同步,影响程序运行,因此需要方队地方,仔细斟酌】

处理以上的问题,因此对代码进行一下改动

rust 复制代码
    let handle =thread::spawn(|| {
        for i in 1..10 {
            println!("数字是:{},来自spawn创建的线程", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    
    for i in 1..5 {
        println!("数字是:{},来自主线程线程", i);
        thread::sleep(Duration::from_millis(1));
    }
    handle.join().unwrap();

此时代码正常运行,结果如下图所示
如果将handle.join().unwrap();放到主线程for循环之前则会出现同步运行的效果,如下图所示

move关键字主要的作用是用来避免在使用闭包的时候遇到的所有权问题的,它起到的一个作用就是在你代码有疑问时,会提示你代码哪里错了,为什么错了。当然,你也可以理解为捕获了外部的变量的所有权。

move关键字的使用是相当简单的,只要在闭包前面加上move就可以了,会自动进行变量的捕获。示例代码如下所示

rust 复制代码
let v = vec![1, 2, 3];
    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });
    handle.join().unwrap();

二、mpsc多生产者单消费者模型

mpsc多个生产者,单个消费者(multiple producer, single consumer)的缩写。简而言之,Rust 标准库实现信道的方式意味着一个信道可以有多个产生值的 发送(sending)端,但只能有一个消费这些值的 接收(receiving)端。

1.创建一个简单的模型

mpsc是通过mpsc::channel来创建的,返回一个发送者(tx),一个接收者(rx)。

创建一个简单的模型代码如下,发送端在线程内部发送一个字符串Hello,然后接收端接收这个字符串。

rust 复制代码
let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        let hello = String::from("Hello");
        tx.send(hello)
    });
    let received = rx.recv().unwrap();
    println!("接收到的数据是:{}", received)

代码运行效果如下

2.分批发送数据

在使用时,发送端可能会发送多次数据,或者是分批来发送数据,如果再使用

rust 复制代码
let received = rx.recv().unwrap();

来接收数据,则会起不到想要的效果,因为它只能接收一次,所以可以使用for循环来接收数据

rust 复制代码
for received in rx {
        println!("接收到的值: {}", received);
    }

这样只要是发送端发送的数据,接收端就都能接收到了,详细代码如下

rust 复制代码
    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 函数
    for received in rx {
        println!("接收到的值: {}", received);
    }

3. 使用clone来产生多个生产者

rust 复制代码
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);
    }

三、共享状态:互斥锁

如果你对多线程开发有所了解,就一定了解过锁的概念。

1. 创建一个简单的锁

rust 复制代码
let m = Mutex::new(5);
    {
        // 使用 lock 方法获取锁,以访问互斥器中的数据。
        // 如果另一个线程拥有锁,并且那个线程 panic 了,则 lock 调用会失败。
        // MutexGuard也提供了一个 Drop 实现当离开作用域时自动释放锁
        let mut num = m.lock().unwrap();
        *num = 6;
    }
    println!("Mutex:{:?}", m);

2. 使用互斥锁解决引用问题

rust 复制代码
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());
相关推荐
板板正7 分钟前
Spring Boot 整合MongoDB
spring boot·后端·mongodb
码界筑梦坊27 分钟前
98-基于Python的网上厨房美食推荐系统
开发语言·python·美食
光爷不秃37 分钟前
Go语言中安全停止Goroutine的三种方法及设计哲学
开发语言·安全·golang
bobz96543 分钟前
恶补 vhost,vDPA
后端
lpfasd1231 小时前
非中文语音视频自动生成中文字幕的完整实现方案
开发语言·python
泉城老铁1 小时前
在高并发场景下,如何优化线程池参数配置
spring boot·后端·架构
泉城老铁1 小时前
Spring Boot中实现多线程6种方式,提高架构性能
spring boot·后端·spring cloud
昵称为空C1 小时前
SpringBoot 实现DataSource接口实现多租户数据源切换方案
后端·mybatis
hqwest1 小时前
C#WPF实战出真汁05--左侧导航
开发语言·c#·wpf·主界面·窗体设计·视图viewmodel
NEUMaple2 小时前
python爬虫(四)----requests
开发语言·爬虫·python