Rust高级之异步async/await讲解

文章目录

  • [1 异步编程 async/await](#1 异步编程 async/await)
    • [1.1 简介](#1.1 简介)
    • [1.2 原理](#1.2 原理)
      • [1.2.1 await 挂起的是"任务",不是线程](#1.2.1 await 挂起的是"任务",不是线程)
      • [1.2.2 等待期间任务靠什么"回来"------Waker 机制](#1.2.2 等待期间任务靠什么"回来"——Waker 机制)
      • [1.2.3 就绪了但当前任务不交权怎么办](#1.2.3 就绪了但当前任务不交权怎么办)
      • [1.2.4 同步调用异步](#1.2.4 同步调用异步)
    • [1.3 异步编程说明](#1.3 异步编程说明)
      • [1.3.1 async 关键字](#1.3.1 async 关键字)
      • [1.3.2 await 关键字](#1.3.2 await 关键字)
      • [1.3.3 异步函数返回值](#1.3.3 异步函数返回值)
      • [1.3.4 异步块](#1.3.4 异步块)
      • [1.3.5 异步任务执行 block_on](#1.3.5 异步任务执行 block_on)
      • [1.3.6 错误处理](#1.3.6 错误处理)
      • [1.3.7 异步 trait 方法](#1.3.7 异步 trait 方法)
        • [1.3.7.1 使用](#1.3.7.1 使用)
        • [1.3.7.2 局限](#1.3.7.2 局限)
          • [1.3.7.2.1 用 dyn 就编译失败](#1.3.7.2.1 用 dyn 就编译失败)
          • [1.3.7.2.2 spawn 就报 not Send](#1.3.7.2.2 spawn 就报 not Send)
      • [1.3.8 异步上下文](#1.3.8 异步上下文)
      • [1.3.9 派生异步任务(tokio::spawn)](#1.3.9 派生异步任务(tokio::spawn))
      • [1.3.10 异步 I/O](#1.3.10 异步 I/O)
      • [1.3.11 异步通道](#1.3.11 异步通道)
    • [1.4 实例分析](#1.4 实例分析)
      • [1.4.1 await 和 async](#1.4.1 await 和 async)
      • [1.4.2 异步http请求](#1.4.2 异步http请求)

1 异步编程 async/await

1.1 简介

异步编程是一种在 Rust 中处理非阻塞操作的方式,允许程序在执行长时间的 I/O 操作时不被阻塞,而是在等待的同时可以执行其他任务。

Rust 提供了多种工具和库来实现异步编程,包括 asyncawait 关键字、futures 和异步运行时(如 tokio、async-std 等),以及其他辅助工具。

  • FutureFuture 是 Rust 中表示异步操作的抽象。它是一个可能还没有完成的计算,将来某个时刻会返回一个值或一个错误。
  • async/awaitasync 关键字用于定义一个异步函数,它返回一个 Futureawait 关键字用于暂停当前 Future 的执行,直到它完成。

1.2 原理

1.2.1 await 挂起的是"任务",不是线程

异步任务中,await 挂起的是"任务",不是线程,线程从头到尾没有被挂起。 这是 async阻塞式编程最本质的区别。

准确的过程是:

  • 每个 async fn 编译后会变成一个状态机(保存局部变量 + 执行到哪个 await 点)
  • awaitFuture 未就绪时,任务把自己的状态存进这个状态机,然后向执行器返回"我还没完"(Poll::Pending)
  • 线程就此从执行器的任务队列里取出下一个任务继续跑

打个比方:线程是工人,任务是工单。阻塞等于工人守着一张办不了的工单干等;await 等于工人把办不了的工单放回架子上,转身拿下一张。架子(内存里的状态机)上可以堆几万张工单,工人只需要几个

1.2.2 等待期间任务靠什么"回来"------Waker 机制

任务挂起时不是干等轮询,而是登记了回调:

plain 复制代码
任务 await 未就绪
   │
   ├─ 1. 向事件源(I/O reactor / 定时器 / 通道)注册一个 Waker
   ├─ 2. 任务离开线程,进入"睡眠"状态(不占任何线程)
   │
   ▼ (事件发生了:数据到达 / 通道有消息 / 定时器到点)
事件源调用 waker.wake()
   │
   ├─ 3. 任务被放回执行器的就绪队列
   └─ 4. 某个空闲的工作线程取出它,从上次挂起的 await 点之后继续执行

注意第 4 步------恢复执行的可能不是原来那个线程,任务在 tokio 的多线程池里会漂移(这正是 spawn 要求 Send 的原因)

1.2.3 就绪了但当前任务不交权怎么办

Rust 的异步是协作式调度,不是抢占式调度:

  • OS 线程:操作系统可以强制打断一个正在跑的线程(抢占式
  • async 任务:执行器无法打断一个正在执行的任务,只能靠任务自己在 await 点主动让出(协作式

所以如果一个任务"占着线程不交权":

rust 复制代码
#[tokio::main]
async fn main() {
    tokio::spawn(async {
        // 灾难写法:在 async 任务里做长时间的 CPU 计算/阻塞操作
        std::thread::sleep(std::time::Duration::from_secs(10)); // 阻塞整个 worker 线程!
    });

    tokio::spawn(async {
        println!("我可能 10 秒后才被打印"); // 就绪了也只能排队等
    });
}

thread::sleep 直接把 worker 线程睡死,这个线程队列里所有已就绪的任务全部饿死,哪怕它们早就 wake 了。这就是教科书级的 blocking the executor 事故。

工程上的对策:

  • 阻塞/重 CPU 操作移出 async 任务:用 tokio::task::spawn_blocking 交给专门的阻塞线程池执行
rust 复制代码
let result = tokio::task::spawn_blocking(|| {
    // 这里是被丢到 blocking 线程池执行的同步代码
    std::thread::sleep(std::time::Duration::from_secs(10));
    42
}).await.unwrap(); // await 期间 worker 线程照常跑其他任务,不阻塞
  • 长循环中主动让权:tokio::task::yield_now().await 主动回到队列尾部
  • 运行时兜底:tokiopoll 预算机制(一个任务连续 poll 太多次就绪 Future 会被强制让出),多线程运行时还会 work-stealing(别的空闲线程把就绪任务偷走),缓解但不能根治

1.2.4 同步调用异步

由于 .await 只能写在 async 上下文里(所以异步会向上传染),但同步代码完全可以"调用"异步方法,只是拿到的结果是 Future,想取出里面的值,需要一座"执行器桥梁,这就是著名的函数染色function coloring)问题

程序的最外层 main / #[test] 永远是同步函数,异步链必然要在某处终止。终止方式就是用执行器驱动 Future,这就是同步与异步之间的桥:

rust 复制代码
// 同步函数里调用异步方法:完全合法!
fn sync_caller() {
    let fut = hello();           // 调用本身没问题,fut 是个未执行的 Future
    // 桥 ①:futures 的 block_on(轻量,适合简单场景)
    let h = futures::executor::block_on(hello());
    // 桥 ②:建一个 tokio 运行时来驱动(Future 内部用了 tokio API 时必须用这种)
    let rt = tokio::runtime::Runtime::new().unwrap();
    let h = rt.block_on(hello());

    // 桥 ③:已在运行时环境里,把 Future 丢给运行时执行(不等待结果)
    // tokio::spawn(hello()); 点火即忘(fire-and-forget)
}

桥①②③对比

桥① futures::block_on 桥② Runtime::block_on 桥③ tokio::spawn
当前线程 阻塞等结果 阻塞等结果 不阻塞,立即返回
拿到结果 可以 可以 不能(句柄丢弃,结果没人接)
是否需要运行时上下文 不需要 不需要(自己建) 需要

过桥时的两个坑:

  • futures::executor::block_on 驱动不了依赖 tokioFuture。如果 hello 内部用了 tokio::time::sleeptokio::spawntokio 的网络/文件 API,用 futures 的执行器会在运行时 panic("no reactor running"),这是因为这些 API 需要 tokio 运行时上下文。纯计算型 Future 才能用桥。
  • 不能在 tokio 的工作线程里再 block_on。比如在一个 async 任务里嵌套调 Runtime::block_on,会直接 panic(Cannot start a runtime from within a runtime)。运行中的同步上下文想等异步结果,正确姿势是 tokio::task::block_in_place 配合 Handle::block_on,或者干脆让调用链保持 async。

这里 tokio::spawn 做的事:把 hello() 这个 Future 提交给当前 tokio 运行时的任务队列,并立即返回一个 JoinHandle,之后这个任务由运行时的 worker 线程调度执行,和调用者再无关系,由于同步代码里没法 .await JoinHandle,在桥③里它经常被直接丢弃(let _ = tokio::spawn(...)),所以想要桥③拿到结果的标准做法是通道回传:任务算完把结果 send 进 channel,同步侧用阻塞式 recv(如 std::sync::mpsc 的 recv(),或 tokio mpsc 的 blocking_recv())接收。这也是同步/异步两个世界通信的通用手段

rust 复制代码
fn sync_get_result() -> String {
    let (tx, rx) = std::sync::mpsc::channel();
    tokio::spawn(async move {
        let r = hello().await;
        tx.send(r).unwrap();      // 异步侧算完,往通道里送
    });
    rx.recv().unwrap()            // 同步侧用 std 通道的阻塞 recv 等
}

为什么同步函数里能调 tokio::spawn

关键点:tokio::spawn 不要求调用者是 async,它要求的是运行时上下文tokio 在线程局部变量里记录了"当前运行时",只要这段同步代码正跑在运行时的线程上(比如它是被某个 async 任务调用的),spawn 就能找到运行时并把任务塞进去

注意:不是"项目里存在一个 tokio 就行",而是调用 spawn 的这个线程,此刻正处于某个运行时的上下文里。上下文是线程局部(thread-local)的,跟别的线程上有没有运行时毫无关系。一个线程进入上下文的方式有三种:

  • 它本身就是运行时的 worker 线程(async 任务里同步调用的代码,天然在上下文里)
  • 它正在执行 rt.block_on(...)
  • 它持有 rt.enter() 的守卫(guard 存活期间都算在上下文里)

"那如果有别的完全不相干的 tokio 也可以吗?"------可以。

一个进程里同时存在多个 tokio 运行时完全合法,当前线程进入的是哪个运行时的上下文,任务就提交给哪个运行时,归它调度。比如线程 A 在运行时 X 的 worker 上跑,调 spawn → 任务进 X 的队列,跟运行时 Y 没关系。

结束的 tokio 不行吗?------对,必须活着。 spawn 要找的不是"某个全局的 tokio",而是"当前线程 thread-local 里登记的那个活着的运行时"。

运行时上下文:当前线程的 thread-local 里存放的当前 tokio 运行时引用,tokio 的所有自由函数 API(tokio::spawntokio::time::sleep......)被调用时,都会去这个 thread-local 里查"该把活交给哪个运行时"。

1.3 异步编程说明

1.3.1 async 关键字

async 关键字用于定义异步函数,异步函数的返回值类型实际上是 impl Future<Output = T>

注意:调用异步函数并不会执行函数体,它只是创建一个惰性的 Future 对象,只有当这个 Future.await 或被异步执行器(executorpoll 时,函数体才会开始执行。这与 JavaScript 中 Promise 创建即开始执行的行为有本质区别。

异步函数内部可以包含 await 表达式,用于等待其他异步操作的完成。

rust 复制代码
#[tokio::test]
async fn test_01() {
    println!("test_01 start");
    let h = hello().await;   // 现在可以 await 了,h 是 String
    println!("{:?}", h);
    println!("test_01 end");
}

async fn hello() -> String {
    "Hello, world!".to_string()
}

#[tokio::test] 就是测试版的 #[tokio::main],Cargo.toml 加 dev 依赖

bash 复制代码
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt"] }

,默认是单线程(current_thread)运行时,需要多线程的话

rust 复制代码
#[tokio::test(flavor = "multi_thread")]
async fn test_01() { ... }

更改配置

bash 复制代码
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

1.3.2 await 关键字

await 关键字用于等待异步操作的完成,并获取其结果, await 表达式只能在异步函数或异步块中使用,当被等待的 Future 尚未完成时,await 会挂起当前异步任务,把控制权交还给执行器,让当前线程可以去执行其他异步任务;当被等待的 Future 就绪后,当前任务会被重新调度,继续执行后续的代码。这正是异步非阻塞的核心机制

rust 复制代码
async fn print_hello() {
    let result = hello().await;
    println!("{}", result);
}

1.3.3 异步函数返回值

异步函数的返回值类型通常是 impl Future<Output = T>,其中 T 是异步操作的结果类型,由于异步函数的返回值是一个 Future,因此可以使用 .await 来等待异步操作的完成,并获取其结果。

rust 复制代码
async fn add(a: i32, b: i32) -> i32 {
    a + b
}

1.3.4 异步块

除了定义异步函数外,Rust 还提供了异步块的语法,可以在同步代码中创建 Future。异步块由 async { } 构成,会捕获其所在环境中使用到的变量,其中可以包含异步函数调用和 await 表达式。

与异步函数一样,异步块创建后不会自动执行:单独写一个 async { ... }; 语句只会创建 Future 然后立即丢弃它(编译器会给出 unused 警告),必须对它 .await 或交给执行器才会真正运行。

rust 复制代码
async fn run() {
    let fut = async {
        let result1 = hello().await;
        let result2 = add(1, 2).await;
        println!("Result: {}, {}", result1, result2);
    };
    fut.await; // 必须 await(或交给执行器)才会执行
}
run().await;  

1.3.5 异步任务执行 block_on

Rust 中,异步任务必须由执行器(executor)/ 异步运行时来驱动执行,常用方式包括:#[tokio::main] 属性宏、async_std::task::block_onfutures::executor::block_on 等函数。其中 block_on 这类函数接受一个 Future(来自异步函数调用或异步块),在当前线程阻塞地执行它直到完成。

futures::executor::block_on ------ 来自 futures 基础库,是个裸执行器:

  • 内部就是一个poll 循环 + 线程 park/unpark:Future 没就绪就 park 线程,被 wake 就再 poll
  • 没有 reactor、没有定时器、没有任务调度器,什么运行时设施都不提供
  • 所以 Future 里一旦调用任何依赖运行时的 API(tokio 的、async-std 的),立刻 panic

async_std::task::block_on ------ 来自 async-std,是带完整运行时上下文的入口:

  • 它会惰性启动 async-std 的全局运行时(I/O reactor、定时器、全局 executor 线程池)
  • 在它驱动的 Future 里,async-std 全家桶都能用:async_std::task::sleep、async_std::fs、async_std::net、async_std::task::spawn......
  • 阻塞期间当前线程还会帮忙处理 I/O 事件

选择原则:block_on 必须和 Future 的"生态户口"匹配,每个生态的 异步 API 都要找到自己家的运行时,block_on 只是入口形式不同,上下文的匹配要求不变。

rust 复制代码
async fn pure_logic() -> i32 { 1 + 1 }          // 纯计算,无运行时依赖

async fn uses_async_std() {
    async_std::task::sleep(Duration::from_secs(1)).await;  // async-std 户口
}

async fn uses_tokio() {
    tokio::time::sleep(Duration::from_secs(1)).await;      // tokio 户口
}

fn main() {
    futures::executor::block_on(pure_logic());      // ✅ 裸执行器就够
    // futures::executor::block_on(uses_async_std()); // ❌ panic
    async_std::task::block_on(uses_async_std());     // ✅ 生态匹配
    // async_std::task::block_on(uses_tokio());       // ❌ tokio API 找不到 tokio 运行时,panic
    tokio::runtime::Runtime::new().unwrap().block_on(uses_tokio()); // ✅
}

配置:

bash 复制代码
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt","time","rt-multi-thread"] }
futures = "0.3"
async-std = "1.13.2"

1.3.6 错误处理

await 后面跟一个 ? 操作符可以传播错误。如果 awaitFuture 完成时返回了一个错误,那么这个错误会被传播到调用者。

rust 复制代码
async fn my_async_function() -> Result<(), MyError> {
    some_async_operation().await?;
    // 如果 some_async_operation 出错,错误会被传播
}

1.3.7 异步 trait 方法

1.3.7.1 使用

Rust 1.75(2023 年 12 月) 起,Rust 原生允许在 trait 中定义 async fn。更早的版本需要使用 async-trait crate 来实现同样的功能。

rust 复制代码
// 定义异步 trait(Rust 1.75+ 原生写法)
trait Greeter {
    async fn greet(&self, name: &str) -> String;
}

// 实现 1:纯内存操作
struct ChineseGreeter;

impl Greeter for ChineseGreeter {
    async fn greet(&self, name: &str) -> String {
        format!("你好,{}", name)
    }
}

// 实现 2:带真实异步操作
struct EnglishGreeter;

impl Greeter for EnglishGreeter {
    async fn greet(&self, name: &str) -> String {
        tokio::time::sleep(Duration::from_millis(100)).await; // 模拟 I/O 耗时
        format!("Hello, {}", name)
    }
}

// 泛型 + 静态分发:trait 的标准使用姿势
async fn say_hello<T: Greeter>(g: &T) {
    let msg = g.greet("World").await;
    println!("{}", msg);
}

#[tokio::test]
async fn test_async_trait() {
    let cn = ChineseGreeter;
    let en = EnglishGreeter;

    say_hello(&cn).await;   // 通过 trait 调用
    say_hello(&en).await;

}
1.3.7.2 局限

原生写法有两个重要局限,实际使用时需要注意:

  • 不支持动态分发:不能通过 Box<dyn MyAsyncTrait> 调用 async 方法。如果需要在 trait 对象上使用异步方法,需使用 async-trait crate(它会把返回值装箱为 Pin<Box<dyn Future>>)。
  • 无法附加 Send 约束:pub trait 中的 async fn 会产生编译警告,因为无法要求返回的 Future 实现 Send。在跨 spawn 边界使用的公开 API 中,建议使用 async-traittrait-variant crate 显式控制。
1.3.7.2.1 用 dyn 就编译失败
rust 复制代码
trait MyAsyncTrait {
    async fn async_method(&self) -> String;
}
struct MyType;
impl MyAsyncTrait for MyType {
    async fn async_method(&self) -> String {
        "hello".to_string()
    }
}
// 试着把 trait 当对象用:
fn demo(obj: Box<dyn MyAsyncTrait>) {}
//                ^^^^^^^^^^^^^^^
//  error[E0038]: the trait `MyAsyncTrait` cannot be made into an object
//    note: ... contains a method with a non-object-safe return type `impl Future`

dyn Trait 靠 vtable 调用方法,vtable 里每个方法必须有固定的签名。而 async fn 的返回值是 impl Future------每个实现类型生成一个不同的匿名 Future 类型,编译器根本没法在 vtable 里填一个统一的签名,所以直接禁止 dyn。

async-trait 的解法:它把方法改写成返回装箱的 trait 对象:

rust 复制代码
// #[async_trait] 展开后等价于:
trait MyAsyncTrait {
    fn async_method<'a>(&'a self)
        -> Pin<Box<dyn Future<Output = String> + Send + 'a>>;
    //     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 具体类型了,vtable 能填 ✅
}
1.3.7.2.2 spawn 就报 not Send
rust 复制代码
pub trait MyAsyncTrait {
    async fn async_method(&self);
}
// warning: use of `async fn` in public traits is discouraged
//    as auto trait bounds cannot be specified   ← 这就是那个警告

// 泛型函数里把调用结果 spawn 出去:
fn spawn_it<T: MyAsyncTrait + Send + Sync + 'static>(t: T) {
    tokio::spawn(async move {
        t.async_method().await;
    });
}
//  error: future cannot be sent between threads safely
//    note: the return type of `async_method` is opaque, cannot prove it's Send

tokio::spawn 要求 FutureSend(任务可能跨线程漂移),但 trait 里写 async fn 时,没有任何语法能给返回的那个匿名 Future 附加 Send 约束------编译器无法证明 t.async_method() 返回的 Future 实现了 Send,拒绝编译。

三种解法:

rust 复制代码
// async-trait:默认 desugar 就带 + Send,问题解决
#[async_trait]
pub trait MyAsyncTrait { async fn async_method(&self); }

//trait-variant crate:
#[trait_variant::make(MyAsyncTrait: Send)]
pub trait MyAsyncTrait { async fn async_method(&self); }

//手动:不用 async fn 语法,直接声明返回装箱 Future
pub trait MyAsyncTrait {
    fn async_method(&self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
}

1.3.8 异步上下文

Rust 中,异步代码通常在异步运行时(如 Tokio 或 async-std)中执行。这些运行时提供了调度和执行异步任务的机制。

运行时上下文(runtime context):tokio 会把当前运行时的引用存放在当前线程的 thread-local 中,这就是运行时上下文。tokio 的自由函数 API(如 tokio::spawntokio::time::sleep)的签名里并没有 runtime 参数,它们被调用时会去当前线程的 thread-local 里查找:该把任务提交给哪个运行时,因此需要注意:

  • async 任务内部调用这些 API 不会有问题------worker 线程在 poll 任务之前会自动登记上下文;
  • 在没有任何运行时上下文的普通线程中调用 tokio::spawn 等 API,会直接 panic:there is no reactor running, must be called from the context of a Tokio 1.x runtime
  • 同步代码需要手动进入上下文时,可以使用 Runtime::enter();需要从任意线程向运行时提交任务时,可以使用 Handle::spawn------它自带运行时引用,不依赖 thread-local
rust 复制代码
#[tokio::main]
async fn main() {
    some_async_operation().await;
}

以上代码中,#[tokio::main] 属性宏将 main 函数包装在一个异步运行时中。

1.3.9 派生异步任务(tokio::spawn)

异步运行时提供了派生并发任务的能力,例如 tokiotokio::spawn 函数(注意它是普通函数,不是宏),用于在运行时中启动一个新的异步任务(立即执行),并返回一个 JoinHandle,对它 await 可以等待任务完成并获取结果。

由于任务可能被调度到其他线程执行,且不能依赖外部的栈帧,tokio::spawn 要求传入的 Future 满足 Send + 'static 约束。

rust 复制代码
#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        // 异步逻辑
    });
    handle.await.unwrap();
}

JoinHandle一般是返回直接丢弃:let _ = tokio::spawn(...),但也有四个用途:

用途 说明 同步代码里能用吗
.await 等结果 拿回任务的返回值/panic 不能在同步中用,得在 async 上下文
abort() 取消任务(下一个让权点生效) 可以
is_finished() 轮询任务是否结束 可以
存起来 交给别处(某个 async 任务)稍后 await 可以

1.3.10 异步 I/O

Rust 的标准库本身不提供异步 I/O------标准库中只有 std::future::Future trait 的定义,既没有运行时也没有异步 I/O 操作。异步 I/O 由第三方异步运行时库提供,如 tokio::fs::Fileasync_std::fs::File

rust 复制代码
use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};

#[tokio::main]
async fn main() -> io::Result<()> {
    let mut file = File::open("file.txt").await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    println!("Contents: {}", contents);
    Ok(())
}

1.3.11 异步通道

Rust 的一些异步运行时提供了异步通道(如 tokio::sync::mpsc),允许在异步任务之间传递消息。

rust 复制代码
use tokio::sync::mpsc;
use tokio::spawn;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(32);
    let child = spawn(async move {
        let response = "Hello, world!".to_string();
        tx.send(response).await.unwrap();// ① 等"消息放进通道缓冲区"
    });
    let response = rx.recv().await.unwrap(); // ② 等"通道里有消息可取"
    println!("Received: {}", response);

    child.await.unwrap();// ③ 等"spawn 出来的任务执行完毕"
}

这里有三处等待

# 对象 Future 的来源 await 等待的事 Output 类型 unwrap 在解什么
tx.send(...) async 方法调用 缓冲区有空位、消息入队 Result<(), SendError<String>> 通道已关闭导致的 SendError
rx.recv() async 方法调用 通道中出现消息 Option<String> None(通道关闭且无消息)
child JoinHandle 本身就实现了 Future spawned 任务运行结束 Result<(), JoinError> 任务 panic / 被取消

关键区别:③ 和 ①② 不一样,这里有一个很重要的概念差异:

  • ①② 是惰性 Future------不 await 就不发生。 tx.send(...) 不被 await,发送动作根本不会执行;rx.recv() 不被 await,就永远拿不到消息。
  • ③ 不一样------spawn 是立即(eager)执行的。 tokio::spawn 一被调用,任务立刻被调度到运行时上开始跑了,JoinHandle 并不能"驱动"任务,它只是一个用来等待和观察任务完成的句柄。所以即使不写 child.await,任务照样会跑完,区别只在于:主任务不等它、也感知不到它 panic
  • child.await 在这里的价值是:保证主流程退出前子任务已彻底结束,子任务 panic 会以 JoinError 的形式在这里暴露出来

1.4 实例分析

1.4.1 await 和 async

以下实例展示了如何使用 async 和 await 关键字编写一个异步函数,以及如何在异步函数中执行异步任务并等待其完成。

rust 复制代码
// 引入所需的依赖库
use tokio;
use tokio::time::{self, Duration};
// 异步函数,模拟异步任务
async fn async_task() -> u32 {
    // 模拟异步操作,等待 1 秒钟
    time::sleep(Duration::from_secs(1)).await;
    // 返回结果
    42
}
// 异步任务执行函数
async fn execute_async_task() {
    // 调用异步任务,并等待其完成
    let result = async_task().await;
    // 输出结果
    println!("Async task result: {}", result);
}
// 主函数
#[tokio::main]
async fn main() {
    println!("Start executing async task...");
    // 调用异步任务执行函数,并等待其完成
    execute_async_task().await;
    println!("Async task completed!");
}

使用 tokio::time::delay_for() 方法来等待 1 秒钟,然后返回结果 42。接着定义了一个异步任务执行函数 execute_async_task(),在其中调用了异步函数,并使用 await 关键字等待异步任务的完成。最后在 main 函数中使用 tokio::main 宏来运行异步任务执行函数,并等待其完成。

1.4.2 异步http请求

以下实例使用 tokio 库执行异步 HTTP 请求,并输出响应结果:

rust 复制代码
// 引入所需的依赖库
use std::error::Error;
use tokio::runtime::Runtime;
use reqwest::get;

// 异步函数,用于执行 HTTP GET 请求并返回响应结果
async fn fetch_url(url: &str) -> Result<String, Box<dyn Error>> {
    // 使用 reqwest 发起异步 HTTP GET 请求
    let response = get(url).await?;
    let body = response.text().await?;
    Ok(body)
}

// 异步任务执行函数
async fn execute_async_task() -> Result<(), Box<dyn Error>> {
    // 发起异步 HTTP 请求
    let url = "https://jsonplaceholder.typicode.com/posts/1";
    let result = fetch_url(url).await?;
    // 输出响应结果
    println!("Response: {}", result);
    Ok(())
}

// 主函数
fn main() {
    // 创建异步运行时
    let rt = Runtime::new().unwrap();
    // 在异步运行时中执行异步任务
    let result = rt.block_on(execute_async_task());
    // 处理异步任务执行结果
    match result {
        Ok(_) => println!("Async task executed successfully!"),
        Err(e) => eprintln!("Error: {}", e),
    }
}
相关推荐
luj_17681 小时前
星火科技助力边远地区防病攻坚
c语言·开发语言·c++·经验分享·算法
xcLeigh2 小时前
Go入门:变量声明的五种方式详解
java·开发语言·golang
AI大模型-小华2 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
花褪残红青杏小2 小时前
Rust图像处理第24节- 噪声模型 + 中心极限定理:椒盐噪声 + CLT 可视化
rust·webassembly·图形学
Mark_ZP3 小时前
【锁1】Synchronized vs ReentrantLock区别
java
立心者03 小时前
SpringBoot中使用TOTP实现MFA(多因素认证)
java·spring boot·后端
枕星而眠3 小时前
C++ STL Map容器完全指南:从有序红黑树到无序哈希表
java·开发语言
逃逸线LOF4 小时前
Spring配置数据源{连接池}(Druid、c3p0)
java·数据库·spring
geats人山人海4 小时前
数据结构第六章c语言 树的存储下二叉树的概念和存储
c语言·数据结构·算法
爱吃牛肉的大老虎4 小时前
Rust对象之结构体,枚举,特性
开发语言·后端·rust