详解Rust异步编程

文章目录

    • 多线程编程与异步编程对比
    • 并发模型对比分析
    • 异步编程基础概念及用法

Rust的异步编程通过async/await语法和Future特性提供了一种高效的方式来处理并发任务,尤其在I/O密集型操作中表现出色。async/await异步编程模型性能高,还能支持底层编程,同时又像线程和协程那样无需过多的改变编程模型,但async模型的问题就是内部实现机制过于复杂,对于用户来说,理解和使用没有线程和协程简单。

多线程编程与异步编程对比

Rust的多线程编程和异步编程都是处理并发的常用方式,虽然它们都能够提高程序的并发性能,但它们在实现原理、使用场景、优缺点等方面存在一些重要差异。

1.概念区别
多线程编程
Rust的多线程编程利用操作系统的线程来并行执行任务,每个线程都有自己的执行上下文和栈。Rust通过std::thread模块来创建和管理线程。线程间的共享数据需要通过锁或原子操作来管理,以避免数据竞态。

异步编程
Rust的异步编程基于非阻塞I/O操作,并通过async/await语法实现。异步任务通常是在单线程中通过事件循环和任务调度来实现并发,而不是通过多个操作系统线程。Rust的异步编程主要依赖于Future和Tokio、async-std等库来管理和调度任务。

2.实现机制
多线程编程,每个线程都由操作系统调度,独立执行任务。线程通常会阻塞,直到执行完成,线程间的数据共享需要显式地通过Arc<Mutex>、RwLock或 Atomic等方式来进行同步。

异步函数是基于事件循环和任务调度器的,执行时不会阻塞线程,而是通过协作式多任务调度实现并发。当遇到需要等待的操作(如 I/O、网络请求等)时,异步任务会主动让出控制权,直到操作完成才会继续执行。异步编程依赖于Future和.await来控制任务的调度和执行。

3.使用场景
多线程编程适用于计算密集型任务,如大规模数据处理、图像处理、视频渲染等。当任务需要大量CPU资源并且任务之间的执行是独立的时,使用多线程能够显著提升性能。适合任务需要真实并行执行的场景,比如将任务分配到多个CPU核心上运行。

异步编程适用于I/O密集型任务,如网络请求、文件操作、数据库访问等。当任务的瓶颈在于等待外部资源时,异步编程能够显著提升效率。用于高并发的Web服务器、网络客户端等应用,特别是当大量连接/请求需要同时处理时,异步编程的优势非常明显。

有大量IO任务需要并发运行时,选async模型
有部分IO任务需要并发运行时,选多线程,如果想要降低线程创建和销毁的开销,可以使用线程池
有大量CPU密集任务需要并行运行时,例如并行计算,选多线程模型,且让线程数等于或者稍大于CPU核心数
无所谓时统一选多线程

4.优缺点对比

种类优点缺点
多线程编程1.真正的并行执行,适合CPU密集型任务
2.线程间独立,控制直观,易于理解;
3.无数据竞争和内存问题是安全的
1.创建和销毁线程的成本较高,过多线程可能导致上下文切换的开销。
2.线程间同步需要额外处理,增加了复杂性。
3.线程调度由操作系统管理,不能完全控制线程执行顺序。
异步编程1.适合I/O密集型任务,能在单线程上处理大量并发任务,避免了线程创建和上下文切换的开销。
2.异步编程通过事件循环调度,不需要操作系统线程支持,因此能在较低的系统资源下运行。
3.通过async/await语法,代码更简洁、易于理解
1.无法有效利用CPU,不适合CPU密集型任务。
2.异步代码可能会引入潜在的生命周期和借用问题。
3.异步编程对调度器和运行时(如 Tokio 或 async-std)有一定依赖,这可能增加外部库的复杂性

5.性能对比
多线程每个线程都是由操作系统调度的,具有独立的栈和上下文,因此能够实现真正的并行。在多核CPU上适合处理计算密集型任务。然而线程创建和销毁的开销相对较大。
异步操作在单线程中,通过任务调度来处理并发,可以避免线程的创建和销毁开销。适合I/O密集型任务,但对CPU密集型任务的性能提升有限,可能需要结合多线程或多进程来解决。

async和多线程的性能对比

操作async线程
创建0.3 微秒17 微秒
线程切换0.2 微秒1.7 微秒

并发模型对比分析

对比分析各种并发模型的优缺点及适用场景。

并发模型优点缺点适用场景
OS 线程简单直接,原生支持,易于理解,不需要改变编程模型上下文切换损耗大,线程间同步困难,性能对 I/O 密集型场景不理想适合 CPU 密集型任务、并行计算
事件驱动性能高,处理并发时非常高效回调地狱,非线性控制流导致数据流向和错误传播难以控制,降低可维护性适合 I/O 密集型任务,尤其是网络服务等
协程支持大量并发任务,性能高,易于实现并发编程抽象层次过高,无法触及底层细节,系统编程和自定义异步运行时难用适合需要大量并发任务的场景,但不涉及底层系统编程
Actor 模型贴近现实,易于实现并发计算,消息传递模式适合分布式系统设计流控制、失败重试等复杂场景下不太好用适合分布式系统、松耦合的并发计算场景
async/await高效性能,支持底层编程,同时具备线程和协程的特点,无需改变编程模型实现复杂,理解和使用有一定难度,但已有封装适合高并发、异步 I/O 的场景,尤其是需要精细控制并发行为时

async是Rust选择的异步编程模型

异步编程基础概念及用法

1.async函数与await
通过将函数标记为async。Rust会将其转换为返回Future类型的函数。Future是Rust中表示异步操作的核心类型,表示一个尚未完成但可能会在将来完成的计算。
await用于挂起当前任务,直到一个Future完成并返回结果。调用await时当前任务会被暂停,直到Future完成并返回结果。await并不会阻塞当前的线程,而是异步的等待Future的完成。

有两种方式可以使用async: async fn用于声明函数,async { … }用于声明语句块,它们会返回一个实现Future特征的值.

//该函数返回一个Future<i32>  
async fn foo() -> i32 {42
}
fn bar() -> impl Future<Output = u8> {// 下面的async语句块返回Future<Output = u8>  async {let x: u8 = foo().await;x + 5}
}async fn bar() {//block_on会阻塞当前线程  //let future = foo();//block_on(future); //与block_on不同.await并不会阻塞当前的线程let result = foo().await;println!("Result: {}", result);
}

2.Future类型
在Rust中异步操作通过Future类型表示,Future本身定义了一个状态机,它跟踪操作的进度。一个Future可以处于以下状态:

  • Pending: 操作正在进行中,尚未完成。
  • Ready: 操作已完成,具有结果值。

Future是惰性执行的,意味着它不会在创建时立即运行,而是在调用await或通过poll方法驱动执行时才开始执行

3.async/await的工作机制
async函数在编译时被转换为一个状态机。编译器会为每个await操作生成一个状态转换的过程,这样可以有效地管理执行流程而不阻塞线程,await操作会挂起函数,直到被等待的Future完成。这个过程并不会阻塞当前线程,而是通过poll的方式让任务调度器在适当时机恢复任务。

4.异步执行模型与任务调度
Rust本身并不提供内置的异步运行时,它依赖于外部库(例如Tokio和async-std)来提供任务调度和执行。常见的异步执行模型如下:
单线程模型: 许多异步框架使用一个单线程执行所有异步任务的调度器。在这个模型中,调度器在后台执行多个任务,尽量避免阻塞增加效率。
多线程模型: 某些框架(如Tokio)支持多线程模型,其中多个线程可以同时运行异步任务。

Tokio调用方法

# Cargo.toml
# 配置依赖库  
[dependencies]
tokio = { version = "1", features = ["full"] }
//使用Tokio运行时
use tokio::time::{sleep, Duration};async fn hello_world() {println!("Hello");sleep(Duration::from_secs(1)).await;println!("World");
}#[tokio::main]
async fn main() {hello_world().await;
}

5.异步错误处理
Rust的异步错误处理与同步代码相似,使用Result和Option类型。异步函数通常会返回Result类型。

async fn might_fail() -> Result<(), String> {// Some async operation that might failErr("Something went wrong".to_string())
}#[tokio::main]
async fn main() {match might_fail().await {Ok(_) => println!("Success"),Err(e) => println!("Error: {}", e),}
}

6.并发与并行
Rust的异步模型能够在单线程中并发执行多个异步任务,这意味着即使你只有一个线程,异步任务依然可以并发执行,但它们实际上是通过时间片轮转来实现的。
如果需要并行(例如在多个CPU核心上运行任务),你可以使用多线程运行时(如Tokio或async-std)。

use tokio::task;async fn task1() {println!("Task 1 started");// simulate async worktokio::time::sleep(tokio::time::Duration::from_secs(1)).await;println!("Task 1 done");
}async fn task2() {println!("Task 2 started");// simulate async worktokio::time::sleep(tokio::time::Duration::from_secs(1)).await;println!("Task 2 done");
}#[tokio::main]
async fn main() {let t1 = task::spawn(task1());let t2 = task::spawn(task2());let _ = tokio::try_join!(t1, t2); // Wait for both tasks to complete
}

7.异步流与通道
Rust还提供了异步流(Stream)和通道(Channel)来处理更复杂的异步场景,例如处理一系列异步数据流或者在不同任务之间传递消息。
异步流(Stream): 表示一系列异步值的集合,可以通过Stream提供的next()方法来异步地获取这些值。
通道(Channel): 异步通道允许不同任务之间传递数据。常用的通道库包括tokio::sync::mpsc和async-std::channel。

Stream类似于Future但是它可以生成多个值,直到它完成。它的行为与标准库中的Iterator很像。Stream trait定义了poll_next 方法,用于返回流中的下一个元素,返回值为Poll<Option>。

  • Poll::Pending: 表示流还没有数据。
  • Poll::Ready(Some(item)): 表示流有数据。
  • Poll::Ready(None): 表示流已完成,没有更多数据。
async fn send_recv() {const BUFFER_SIZE: usize = 10;let (mut tx, mut rx) = mpsc::channel::<i32>(BUFFER_SIZE);tx.send(1).await.unwrap();tx.send(2).await.unwrap();drop(tx);assert_eq!(Some(1), rx.next().await);assert_eq!(Some(2), rx.next().await);assert_eq!(None, rx.next().await);
}

8.性能分析
Rust的异步编程具有高效性,特别是在处理I/O密集型任务时。由于Rust的所有权系统和无垃圾回收机制,异步任务的内存管理得到了很好的保证,这使得Rust异步代码非常高效。

  • 无堆分配的异步任务: Rust提供了Pin和Box等类型来确保异步任务的内存位置不会发生变化,避免了运行时的额外开销。
  • 零成本抽象: Rust的异步编程模型通过编译时优化,提供了与同步代码几乎相同的性能,而不会引入额外的运行时开销。

9.同时运行多个Future

//两个future 一个先运行 另一个后运行  
async fn enjoy_book_and_music() -> (Book, Music) {let book = enjoy_book().await;let music = enjoy_music().await;(book, music)
}//发运行两个 Future
//如果希望同时运行一个数组里的多个异步任务,可以使用 futures::future::join_all 方法  
use futures::join;
async fn enjoy_book_and_music() -> (Book, Music) {let book_fut = enjoy_book();let music_fut = enjoy_music();join!(book_fut, music_fut)
}//希望在某一个Future报错后就立即停止所有Future的执行,可以使用 try_join!  
//有一点需要注意传给try_join!的所有Future都必须拥有相同的错误类型。
//如果错误类型不同,可以考虑使用来自 futures::future::TryFutureExt 模块的 map_err 和 err_info 方法将错误进行转换  
use futures::try_join;
async fn get_book() -> Result<Book, String> { /* ... */ Ok(Book) }
async fn get_music() -> Result<Music, String> { /* ... */ Ok(Music) }async fn get_book_and_music() -> Result<(Book, Music), String> {let book_fut = get_book();let music_fut = get_music();try_join!(book_fut, music_fut)
}

10.async函数的生命周期问题
当异步函数接受引用类型的参数时Future的生命周期必须至少与参数的生命周期相同。否则编译器会报错。
解决方法:通过将引用传入async语句块内,使其生命周期延续到Future返回时,避免生命周期不匹配的问题。

async move会捕获外部变量并将其所有权转移到异步任务中。这解决了借用生命周期的问题,避免了变量在任务完成前被释放。
使用async move时,所有的捕获变量的所有权会被转移,且该变量不再受到生命周期的限制,无法与其他代码共享。

// 多个不同的async语句块可以访问同一个本地变量 只要它们在该变量的作用域内执行
async fn blocks() {let my_string = "foo".to_string();let future_one = async {// ...println!("{my_string}");};let future_two = async {// ...println!("{my_string}");};// 运行两个 Future 直到完成let ((), ()) = futures::join!(future_one, future_two);
}//由于async move会捕获环境中的变量,因此只有一个async move语句块可以访问该变量
//有非常明显的好处: 变量可以转移到返回的 Future 中,不再受借用生命周期的限制
fn move_block() -> impl Future<Output = ()> {let my_string = "foo".to_string();async move {// ...println!("{my_string}");}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/483260.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux详解:文件权限

文章目录 前言Linux文件权限基础文件成员与三组权限字符 权限的修改修改文件所有者总结 前言 在浩瀚的操作系统世界中&#xff0c;Linux以其开源、灵活和强大的特性&#xff0c;成为了服务器、开发环境以及众多个人用户的首选。而在Linux的众多特性中&#xff0c;文件权限机制…

openEuler 22.03 使用cephadm安装部署ceph集群

目录 目的步骤规格步骤ceph部署前准备工作安装部署ceph集群ceph集群添加node与osdceph集群一些操作组件服务操作集群进程操作 目的 使用ceph官网的cephadm无法正常安装&#xff0c;会报错ERROR: Distro openeuler version 22.03 not supported 在openEuler上实现以cephadm安装部…

xiaolin coding 图解 MySQL笔记——事务篇

1. 事务隔离级别是怎么实现的&#xff1f; 数据库中的**事务&#xff08;Transaction&#xff09;**先开启&#xff0c;然后等所有数据库操作执行完成后&#xff0c;才提交事务&#xff0c;对于已经提交的事务来说&#xff0c;该事务对数据库所做的修改将永久生效&#xff0c;…

掌握 Spring Boot 中的缓存:技术和最佳实践

缓存是一种用于将经常访问的数据临时存储在更快的存储层&#xff08;通常在内存中&#xff09;中的技术&#xff0c;以便可以更快地满足未来对该数据的请求&#xff0c;从而提高应用程序的性能和效率。在 Spring Boot 中&#xff0c;缓存是一种简单而强大的方法&#xff0c;可以…

408——数据结构(持续更新)

文章目录 一、绪论1.1 相关概念1.2 数据结构三要素1.3 相关习题1.4 复杂度1.4.1 时间复杂度1.4.2 复杂度相关习题 二、线性表 一、绪论 1.1 相关概念 数据&#xff1a;数据是信息的载体&#xff0c;所有能被输入到计算机中&#xff0c;且能被计算机处理的符号的集合。如图片、…

深入浅出:开发者如何快速上手Web3生态系统

Web3作为互联网的未来发展方向&#xff0c;正在逐步改变传统互联网架构&#xff0c;推动去中心化技术的发展。对于开发者而言&#xff0c;Web3代表着一个充满机遇与挑战的新领域&#xff0c;学习和掌握Web3的基本技术和工具&#xff0c;将为未来的项目开发提供强大的支持。那么…

C++学习日记---第16天

笔记复习 1.C对象模型 在C中&#xff0c;类内的成员变量和成员函数分开存储 我们知道&#xff0c;C中的成员变量和成员函数均可分为两种&#xff0c;一种是普通的&#xff0c;一种是静态的&#xff0c;对于静态成员变量和静态成员函数&#xff0c;我们知道他们不属于类的对象…

Leetcode 每日一题 205.同构字符串

目录 问题描述 过题图片 示例 解决方案 代码实现 题目链接 总结 问题描述 给定两个字符串 s 和 t&#xff0c;判断它们是否是同构的。如果 s 中的字符可以按某种映射关系替换得到 t&#xff0c;那么这两个字符串是同构的。具体来说&#xff0c;每个出现的字符都应当映射…

C# 集合(Collection)

文章目录 前言一、动态数组&#xff08;ArrayList&#xff09;二、哈希表&#xff08;Hashtable&#xff09;三、排序列表&#xff08;SortedList&#xff09;四、堆栈&#xff08;Stack&#xff09;五、队列&#xff08;Queue&#xff09;六、点阵列&#xff08;BitArray&…

2.5 特征降维(机器学习)

2.5 特征降维 2.5.1 降维 降维&#xff1a;是指在某些限定条件下&#xff0c;降低随机变量&#xff08;特征&#xff09;个数&#xff0c;得到一组“不相关”主变量的过程。 ndarray 维数 嵌套的层数 0维 标量 1维 向量 2维 矩阵 3维 n维 二维数组 降低的维度…

【小白学机器学习41】如何从正态分布的总体中去抽样?比较不同的取样方差的差别

目录 1 目标&#xff1a;使用2种方法&#xff0c;去从正态分布的总体中去抽样&#xff0c;获得样本 1.1 step1: 首先&#xff0c;逻辑上需要先有符合正态分布的总体population 1.2 从总体中取得样本&#xff0c;模拟抽样的过程 2 从正态分布抽样的方法1 3 从正态分布抽样…

框架5:SpringBoot 2 - 核心功能

SpringBoot2 - 基础入门【一 ~ 五】&#xff0c;详见&#xff1a; 六、配置文件 6.1 properties文件格式 同之前的用法。 6.2 yaml文件格式【推荐】 YAML本意&#xff1a;“YAML”不是一种标记语言。但在开发中&#xff0c;实际把它理解为&#xff1a;“Yet Another Markup Lan…

行为型设计模式之《责任链模式》实践

定义 责任链模式&#xff08;Chain Of Responsibility Pattern&#xff09;顾名思义&#xff0c;就是为请求创建一条处理链路&#xff0c;链路上的每个处理器都判断是否可以处理请求&#xff0c;如果不能处理则往后走&#xff0c;依次从链头走到链尾&#xff0c;直到有处理器可…

Vue前端开发-路由树配置

一个配置路由的文件由导入路由模块、创建路由对象和导出路由对象三个部分组成&#xff0c;在创建路由对象时&#xff0c;需要构建路由数组&#xff0c;路由数组中包括一级、二级和多级路由结构&#xff0c;因此&#xff0c;这种结构的路由配置&#xff0c;又称为路由树配置。 …

2.mysql 中一条更新语句的执行流程是怎样的呢?

前面我们系统了解了一个查询语句的执行流程&#xff0c;并介绍了执行过程中涉及的处理模块。 相信你还记得&#xff0c;一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块&#xff0c;最后到达存储引擎。 那么&#xff0c;一条更新语句的执行流程又…

JavaScript根据数据生成柱形图

分析需求 // 定义一个数组来存储四个季度的数据 dataArray = []// 循环4次,获取用户输入的数据并存储到数组中 for i from 0 to 3// 获取用户输入的数据inputData = 获取用户输入的第(i + 1)季度的数据// 将数据存入数组dataArray[i] = inputData// 遍历数组,根据数据生成柱…

实验13 使用预训练resnet18实现CIFAR-10分类

1.数据预处理 首先利用函数transforms.Compose定义了一个预处理函数transform&#xff0c;里面定义了两种操作&#xff0c;一个是将图像转换为Tensor&#xff0c;一个是对图像进行标准化。然后利用函数torchvision.datasets.CIFAR10下载数据集&#xff0c;这个函数有四个常见的…

【AI系统】代数简化

代数简化 代数简化&#xff08;Algebraic Reduced&#xff09;是一种从数学上来指导我们优化计算图的方法。其目的是利用交换率、结合律等规律调整图中算子的执行顺序&#xff0c;或者删除不必要的算子&#xff0c;以提高图整体的计算效率。 代数化简可以通过子图替换的方式完…

多人聊天室项目 BIO模型实现

BIO模型聊天室项目大体设计 BIO编程模型 Acceptor是服务器端负责监听具体端口的Socket每有一个客户端Client连接到服务器端&#xff0c;Acceptor就创建一个新的线程Handler来处理客户端发送的消息每一个客户端都有一个唯一的Handler来对应处理其事务为保证线程安全&#xff0c…

腾讯云平台 - Stable Diffusion WebUI 下载模型

1&#xff09;进入控制台&#xff0c;点击算力连接 》 JupyterLab 2&#xff09;进入模型目录&#xff08;双击&#xff09; 3&#xff09;上传模型 例如&#xff1a;我要上传大模型