跟着谷歌安卓团队学习Rust - 并发编程使用手册

201 阅读8分钟

大家好,我是梦兽编程。欢迎回来与梦兽编程一起刷Rust的系列。

这是由 Google 的 Android开发团队的分享Rust课程。本课程涵盖了 Rust 的方方面面,从基本语法到泛型和错误处理等高级主题。

该课程的最新版本可以在 google.github.io/comprehensi…

如果你喜欢看梦兽编程的版本可以订阅跟着谷歌安卓团队学Rust订阅最新内容,梦兽编程也期待大家关注我的个人网站。

加入梦兽编程微信群,微信搜索【梦兽编程】公众号回复111即可加入交流群与梦兽进行交流。

1.最常用的线程模式

线程模型是我们众多语言中常用的并发编程模型,线程就像是同一个程序中的多个“工人”,可以同时执行不同的任务。就像一家餐厅里,服务员们同时为多个顾客提供服务一样。

在你的聊天应用程序中,每个用户的聊天信息都可以由一个独立的线程来处理。这样,程序就可以同时响应多个用户,而不会因为处理一个用户的请求而阻塞其他用户。

线程可以并行执行,这意味着它们可以同时运行,而不需要等待彼此完成。这就像是多个服务员同时为不同的顾客服务,而不是一个接一个地服务。

假设你需要同时执行两个任务:一个是下载一个大文件,另一个是生成一个报告。如果你只有一个“工人”,那么当下载任务正在执行时,报告任务就必须等待。但是如果你有两个“工人”,一个负责下载,另一个负责生成报告,那么这两个任务就可以同时执行,从而提高整体的效率。

在Rust中同样支持这种并发编程的能力,使用起来非常简单。

use std::thread;

fn main() {
    // 创建一个新线程
    let handle = thread::spawn(|| {
        // 在新线程中执行的代码
        println!("这是新线程!");
    });

    // 等待新线程完成
    handle.join().unwrap();
}

「在线程间传递数据」

我们继续沿用刚才的聊天应用程序的例子。你知道每个用户的聊天信息都由一个独立的线程来处理。但是,这些线程之间如何共享和传递数据呢?

线程之间可以通过共享内存来传递数据就像是多个服务员共用同一个储物柜来存放顾客的订单信息一样。

在你的聊天应用程序中,每个线程都可以访问和修改一个共享的数据区域,比如存放聊天记录的数据库。当一个用户发送消息时,它的线程会将消息写入到共享的数据区域中。而其他用户的线程则可以从这个共享区域中读取最新的聊天消息,并显示给用户。

这种共享内存的方式存在**「一些风险」**。因为多个线程可以同时访问和修改同一块内存区域,所以可能会出现数据冲突的情况。比如两个线程同时试图更新同一条聊天记录,就可能会导致数据丢失或者不一致。

为了解决这个问题,我们会使用锁的概念去解决上面线程模型带来的问题。

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

fn main() {
    // 创建一个共享的 Mutex
    let counter = Arc::new(Mutex::new(0));

    // 创建多个线程,并共享 counter
    let handlesVec<_> = (0..10)
        .map(|_| {
            let counter = Arc::clone(&counter);
            thread::spawn(move || {
                // 获取锁并增加计数器
                let mut num = counter.lock().unwrap();
                *num += 1;
            })
        })
        .collect();

    // 等待所有线程完成
    for handle in handles {
        handle.join().unwrap();
    }

    // 打印最终的计数器值
    println!("计数器值: {}", *counter.lock().unwrap());
}

2.线程通信模型

这种并发的模型,有点想消息通知。和Golang语言中的Channel模型类似。

在生活中有点像工厂中的流水线例子。

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

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

    // 创建一个新线程
    thread::spawn(move || {
        tx.send("hello").unwrap();
    });

    // 在主线程中接收数据
    let message = rx.recv().unwrap();
    println!("Received: {}", message);
}

假设你是一个小餐馆的老板,你需要同时处理多个顾客的订单。为了提高效率,你决定雇佣多名服务员来分工合作。

首先,你为每个服务员都配备了一个专属的订单本(相当于 Rust 中的 tx和 rx)。每个服务员都可以独立记录顾客的订单,不会互相干扰。

当顾客下单时,你会将订单信息告诉最近空闲的服务员,由他们记录到自己的订单本上。这就相当于 Rust 中的 tx.send()

而厨师们就相当于 Rust 中的消费者线程。他们会定期查看所有服务员的订单本(也是 rx.recv())。一旦发现有新的订单,就立即开始准备。

这样一来,多个服务员可以同时记录订单,而厨师们也可以专注于自己的工作,整个餐厅的运转就变得更加高效和有序。

不过,你也意识到了一个潜在的问题。如果两个服务员同时想要记录同一个顾客的订单,就可能会造成数据冲突。为了解决这个问题,你给每个服务员配备了一支专用的笔(相当于 Rust 中的互斥锁)。这样,同一时刻只能有一个服务员使用订单本,从而避免了数据竞争。

也就是说,这种模型你只要写入的时候处理好竞争关系即可。

通过这种模式,你的餐厅运营得非常顺利。顾客的订单都能得到及时的处理,而且员工之间也能很好地协作。这就相当于 Rust 中的线程通信模型 mpsc::channel()所带来的好处。

3.异步并发模型

再次假设你是一家小餐馆的老板,随着生意的不断增长,你发现有时候顾客的等待时间会比较长,因为厨师们忙不过来。你决定寻找一种更高效的方式来处理订单。

于是,你想到了引入“外卖”业务。这样一来,顾客可以通过手机下单,而厨师们就可以在空闲的时候处理这些外卖订单,而不会影响到堂食的服务。

你将这个想法告诉了你的员工。大家一起讨论后,决定采用一种“异步”的方式来处理外卖订单。

具体做法是,当顾客下单时,服务员会立即将订单信息记录在一个共享的中,然后立即返回给顾客,不必等待厨师准备。厨师们则会定期检查这个池”,看是否有新的订单需要处理。

这样一来,服务员和厨师就可以并行工作了。服务员可以继续接待堂食的顾客,而厨师则专注于准备外卖订单。整个过程是异步的,不需要等待彼此完成。

为了确保订单不会丢失,你还引入了一个状态”的概念。每个订单都有一个状态标记,比如“待处理"、“正在准备"、完成”等。服务员和厨师可以随时查看订单的状态,以协调他们的工作。

通过这种异步的模式,你的餐厅的运转效率得到了大幅提升。顾客的等待时间大大缩短,而厨师和服务员也可以更好地利用自己的时间。

这就相当于编程中的异步编程模型。在异步编程中,我们可以将耗时的操作(如 I/O 操作)分离出来,让程序继续执行其他任务,而不必阻塞等待。这种并发模型可以大大提高程序的性能和响应速度。

在Rust中我们一般会依赖**「tokio」**依赖库,实现这个功能,代码如下。

use std::future::Future;
use std::task::{Context, Poll};
use std::time::Duration;

struct DelayedTask {
    duration: Duration,
}

impl Future for DelayedTask {
    type Output = ();

    fn poll(self: std::pin::Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        // 模拟一个延迟任务
        std::thread::sleep(self.duration);
        Poll::Ready(())
    }
}

#[tokio::main]
async fn main() {
    let task1 = DelayedTask { duration: Duration::from_secs(2) };
    let task2 = DelayedTask { duration: Duration::from_secs(3) };

    // 并发执行两个异步任务
    let ((), ()) = futures::join!(task1, task2);

    println!("两个任务都完成了!");
}

这篇文章为我们深入浅出地介绍了 Rust 中的并发编程概念和常用模式,从最基本的线程创建和数据共享,到更高级的线程通信和异步编程,给我们一个全面的认知。通过餐厅的生动比喻,让抽象的编程概念变得更加贴近生活,非常容易理解。

最后,我相信这些知识不仅对 Rust 开发者很有帮助,对于其他语言的并发编程实践也会有所启发。如果你觉得这篇文章对你有所收获,不妨分享给身边的程序员朋友,一起学习和探讨 Rust 的并发编程技巧吧。让我们携手,一起在 Rust 的道路上越走越远!

本文使用 markdown.com.cn 排版