深入Rust异步编程:疑难问题与高效解决方案

深入Rust异步编程:疑难问题与高效解决方案

在Rust的异步编程实践中,开发者常常会遇到一些棘手的问题,如async块中的错误处理、Send特征的近似估算、递归异步函数以及Trait中的异步方法等。本文将探讨这些问题的背景,并提供一些实用的解决方案和临时办法,帮助开发者更好地理解和应用Rust的异步编程特性。

本文详细介绍了Rust异步编程中的四个常见问题及其解决方案: 1. async块中的?操作符:讨论了在async块中使用?操作符时可能遇到的类型推断问题,并提供了使用“turbofish”运算符进行类型注释的解决方案。 2. Send近似估算:解释了异步函数状态机是否可跨线程发送的复杂性,并提出了通过引入块作用域隔离非Send变量的临时办法。 3. 递归异步函数:分析了递归异步函数导致的状态机无限增长问题,并建议使用Box包装异步块来解决。 4. Trait中的异步方法:指出了当前Rust版本中无法在Trait中定义异步函数的限制,并推荐使用async-trait crate作为临时解决方案。

Workarounds to Know and Love

一些疑难问题的解决办法

1. async 块中的 ?

  • Async 块中使用 ? 是比较常见的
  • 但是 async 块的返回类型没有明确说明,这可能会导致编译器无法推断 async 块的错误类型
  • 目前没法给 future 一个类型,也无法指明其类型
  • 临时解决办法:使用 “turbofish”运算符,为 async 块提供成功和错误类型
#![allow(unused)]
fn main() {
  struct MyError
  async fn foo() -> Result<(), MyError> {
    Ok(())
  }
  async fn bar() -> Result<(), MyError> {
    Ok(())

  }

  let fut = async {
    foo().await?;
    bar().await?;
    Ok(())  // 报错 cannot infer type for type parameter `E` declared on the enum `Result`
  };
}

可以使用 ::< ... > 的方式来增加类型注释:

#![allow(unused)]
fn main() {
  struct MyError
  async fn foo() -> Result<(), MyError> {
    Ok(())
  }
  async fn bar() -> Result<(), MyError> {
    Ok(())

  }

  let fut = async {
    foo().await?;
    bar().await?;
    Ok::<(), MyError>(()) // <- note the explicit type annotation here
  };
}

2. Send Approximation

  • 有些 async fn 状态机可安全的跨线程发送,有些则不行
  • async future 是否是 Send 的,取决于在 .await 点是否持有非 Send 的类型
  • 当值可能在跨域 .await 点被持有时,编译器会尽力近似估算,但这种估算在很多地方显得过于保守
  • 临时办法:引入块作用域,把 non-Send 变量隔离

Rc无法在多线程环境使用,原因就在于它并未实现 Send 特征

use std::rc::Rc;

#[derive(Default)]
struct NotSend(Rc<()>);

async fn bar() {}
async fn foo() {
    NotSend::default();
    bar().await;
}

fn require_send(_: impl Send) {}

fn main() {
    require_send(foo());
}

即使上面的 foo 返回的 FutureSend, 但是在它内部短暂的使用 NotSend 依然是安全的,原因在于它的作用域并没有影响到 .await,下面来试试声明一个变量,然后让 .await的调用处于变量的作用域中试试:

async fn foo() {
    let x = NotSend::default();
    bar().await;
}

报错:future cannot be sent between threads safely

.await在运行时处于 x 的作用域内,.await有可能被执行器调度到另一个线程上运行,而Rc并没有实现Send。

可以将变量声明在语句块内,当语句块结束时,变量会自动被 drop

async fn foo() {
    {
        let x = NotSend::default();
    }
    bar().await;
}

3. Recursion

  • 在内部,async fn 会创建一个状态机类型,它含有每个被 .awaited 的子 future
  • 这就有点麻烦,因为状态机需要包含其本身
  • 临时办法:引入间接,使用Box,并把 recursive 放入非 async 的函数,它会返回 .boxed() async 块
// This function: foo函数:
async fn foo() {
    step_one().await;
    step_two().await;
}
// generates a type like this:  会被编译成类似下面的类型:
enum Foo {
    First(StepOne),
    Second(StepTwo),
}

// So this function: recursive函数
async fn recursive() {
    recursive().await;
    recursive().await;
}

// generates a type like this:  会生成类似以下的类型
enum Recursive {
    First(Recursive),
    Second(Recursive),
}

这是典型的动态大小类型,它的大小会无限增长,因此编译器会直接报错:

error[E0733]: recursion in an `async fn` requires boxing
 --> src/lib.rs:1:22
  |
1 | async fn recursive() {
  |                      ^ an `async fn` cannot invoke itself directly
  |
  = note: a recursive `async fn` must be rewritten to return a boxed future.

recursive 转变成一个正常的函数,该函数返回一个使用 Box 包裹的 async 语句块:

use futures::future::{BoxFuture, FutureExt};

fn recursive() -> BoxFuture<'static, ()> {
    async move {
        recursive().await;
        recursive().await;
    }.boxed()
}

4. async in Trait

  • 目前 async fn 不可用在 trait 里
  • 临时解决办法:使用 async-trait

在目前版本中,我们还无法在特征中定义 async fn 函数,不过大家也不用担心,目前已经有计划在未来移除这个限制了。

trait Test {
    async fn test();
}

运行后报错:

error[E0706]: functions in traits cannot be declared `async`
 --> src/main.rs:5:5
  |
5 |     async fn test();
  |     -----^^^^^^^^^^^
  |     |
  |     `async` because of this
  |
  = note: `async` trait functions are not currently supported
  = note: consider using the `async-trait` crate: https://crates.io/crates/async-trait

好在编译器给出了提示,让我们使用 async-trait 解决这个问题:

use async_trait::async_trait;

#[async_trait]
trait Advertisement {
    async fn run(&self);
}

struct Modal;

#[async_trait]
impl Advertisement for Modal {
    async fn run(&self) {
        self.render_fullscreen().await;
        for _ in 0..4u16 {
            remind_user_to_join_mailing_list().await;
        }
        self.hide_for_now().await;
    }
}

struct AutoplayingVideo {
    media_url: String,
}

#[async_trait]
impl Advertisement for AutoplayingVideo {
    async fn run(&self) {
        let stream = connect(&self.media_url).await;
        stream.play().await;

        // 用视频说服用户加入我们的邮件列表
        Modal.run().await;
    }
}

不过使用该包并不是免费的,每一次特征中的async函数被调用时,都会产生一次堆内存分配。对于大多数场景,这个性能开销都可以接受,但是当函数一秒调用几十万、几百万次时,就得小心这块儿代码的性能了!

总结

Rust的异步编程虽然强大,但在实际应用中仍存在一些挑战和限制。通过理解这些问题的根源,并采用适当的解决方案,开发者可以更有效地利用Rust的异步特性。随着Rust语言的不断发展和社区的持续贡献,未来这些问题有望得到更好的支持和解决。

全部评论(0)