rust高级 异步编程 一 future

Rust中 零成本抽象的 future Rust生态系统的一个关键缺陷是缺少快速高效的异步I/O。虽然我们拥有坚实的基础,例如 mio 库,但它们的级别非常低:您必须直接处理 连接状态机并处理回调。futures我们想要更高层次的东西,具有更好的人体工程学,而且还有更好的可组合性,支持所有协同工作的异步抽象生态系统。这个故事可能听起来很熟悉:正是这个目标导致了许多语言中引入future(又名Promise),其中一些支持 async/await 语法糖。 阅读详情

文章目录

      • Async 编程简介
        • async/.await 简单入门
      • Future 执行器与任务调度
        • Future 特征
        • 使用 Waker 来唤醒任务
        • 构建一个定时器
        • 执行器 Executor
          • 构建执行器
    • 完整代码

Async 编程简介

  • OS 线程, 它最简单,也无需改变任何编程模型(业务/代码逻辑),因此非常适合作为语言的原生并发模型,我们在多线程章节也提到过,Rust 就选择了原生支持线程级的并发编程。但是,这种模型也有缺点,例如线程间的同步将变得更加困难,线程间的上下文切换损耗较大。使用线程池在一定程度上可以提升性能,但是对于 IO 密集的场景来说,线程池还是不够。
  • 事件驱动(Event driven),如果说事件驱动常常跟回调( Callback )一起使用,相信大家就恍然大悟了。这种模型性能相当的好,但最大的问题就是存在回调地狱的风险:非线性的控制流和结果处理导致了数据流向和错误传播变得难以掌控,还会导致代码可维护性和可读性的大幅降低。
  • 协程(Coroutines) Go 语言的协程设计就非常优秀,这也是 Go 语言能够迅速火遍全球的杀手锏之一。协程跟线程类似,无需改变编程模型,同时,它也跟 async 类似,可以支持大量的任务并发运行。但协程抽象层次过高,导致用户无法接触到底层的细节,这对于系统编程语言和自定义异步运行时是难以接受的
  • actor 模型是 erlang 的杀手锏之一,它将所有并发计算分割成一个一个单元,这些单元被称为 actor , 单元之间通过消息传递的方式进行通信和数据传递,跟分布式系统的设计理念非常相像。由于 actor 模型跟现实很贴近,因此它相对来说更容易实现,但是一旦遇到流控制、失败重试等场景时,就会变得不太好用
  • async/await, 该模型性能高,还能支持底层编程,同时又像线程和协程那样无需过多的改变编程模型,但有得必有失,async 模型的问题就是内部实现机制过于复杂,对于用户来说,理解和使用起来也没有线程和协程简单,好在前者的复杂性开发者们已经帮我们封装好,而理解和使用起来不够简单。

对于长时间运行的 CPU 密集型任务,例如并行计算,使用线程将更有优势。 这种密集任务往往会让所在的线程持续运行,任何不必要的线程切换都会带来性能损耗,因此高并发反而在此时成为了一种多余。同时你所创建的线程数应该等于 CPU 核心数,充分利用 CPU 的并行能力,甚至还可以将线程绑定到 CPU 核心上,进一步减少线程上下文切换。

而高并发更适合 IO 密集型任务,例如 web 服务器、数据库连接等等网络服务,因为这些任务绝大部分时间都处于等待状态,如果使用多线程,那线程大量时间会处于无所事事的状态,再加上线程上下文切换的高昂代价,让多线程做 IO 密集任务变成了一件非常奢侈的事。而使用async,既可以有效的降低 CPU 和内存的负担,又可以让大量的任务并发的运行,一个任务一旦处于IO或者其他等待(阻塞)状态,就会被立刻切走并执行另一个任务,而这里的任务切换的性能开销要远远低于使用多线程时的线程上下文切换。

async/.await 简单入门

async/.await 是 Rust 内置的语言特性,可以让我们用同步的方式去编写异步的代码。
通过 async 标记的语法块会被转换成实现了Future特征的状态机。 与同步调用阻塞当前线程不同,当Future执行并遇到阻塞时,它会让出当前线程的控制权,这样其它的Future就可以在该线程中运行,这种方式完全不会导致当前线程的阻塞。
下面我们来通过例子学习 async/.await 关键字该如何使用,在开始之前,需要先引入 futures 包。编辑 Cargo.toml 文件并添加以下内容:

[dependencies]
futures = "0.3"
// `block_on`会阻塞当前线程直到指定的`Future`执行完成,这种阻塞当前线程以等待任务完成的方式较为简单、粗暴,
// 好在其它运行时的执行器(executor)会提供更加复杂的行为,例如将多个`future`调度到同一个线程上执行。
use futures::executor::block_on;

async fn hello_world() {
    hello_cat().await;//等待异步方法完成
    println!("hello, world!");
}

async fn hello_cat() {
    println!("hello, kitty!");
}
fn main() {
    let future = hello_world(); // 返回一个Future, 因此不会打印任何输出
    block_on(future); // 执行`Future`并等待其运行完成,此时"hello, world!"会被打印输出
}

总之,在async fn函数中使用.await可以等待另一个异步调用的完成。但是与block_on不同,.await并不会阻塞当前的线程,而是异步的等待Future A的完成,在等待的过程中,该线程还可以继续执行其它的Future B,最终实现了并发处理的效果。

Future 执行器与任务调度

Future 特征

首先,来给出 Future 的定义:它是一个能产出值的异步计算(虽然该值可能为空,例如 () )。光看这个定义,一个简化版的 Future 特征:

trait SimpleFuture {
    // 设置关联类型
    type Output;
    // 输出计算完成的结果或者输出Pending表示本次不能够完成计算
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}

// Future 需要被执行器poll(轮询)后才能运行,并不能保证在一次 poll 中就被执行完,
enum Poll<T> {
    Ready(T),
    Pending,
}

若在当前 poll 中, Future 可以被完成,则会返回 Poll::Ready(result) ,反之则返回 Poll::Pending, 并且安排一个 wake 函数:当未来 Future 准备好进一步执行时, 该函数会被调用,然后管理该 Future 的执行器会再次调用 poll 方法,此时 Future 就可以继续执行了。

用一个例子来说明下。考虑一个需要从 socket 读取数据的场景:如果有数据,可以直接读取数据并返回 Poll::Ready(data), 但如果没有数据,Future 会被阻塞且不会再继续执行,此时它会注册一个 wake 函数,当 socket 数据准备好时,该函数将被调用以通知执行器:我们的 Future 已经准备好了,可以继续执行。

pub struct SocketRead<'a> {
    socket: &'a Socket,
}

impl SimpleFuture for SocketRead<'_> {
    type Output = Vec<u8>;

    fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
        if self.socket.has_data_to_read() {
            // socket有数据,写入buffer中并返回
            Poll::Ready(self.socket.read_buf())
        } else {
            // socket中还没数据
            // 注册一个`wake`函数,当数据可用时,该函数会被调用,
            // 然后当前Future的执行器会再次调用`poll`方法,此时就可以读取到数据
            self.socket.set_readable_callback(wake);
            Poll::Pending
        }
    }
}

Future 模型允许将多个异步操作组合在一起,同时还无需任何内存分配。不仅仅如此,如果你需要同时运行多个 Future或链式调用多个 Future ,也可以通过无内存分配的状态机实现,例如:


trait SimpleFuture {
    // 关联类型是 trait 定义中的类型占位符。定义的时候,并不定义它的具体的类型是什么。在 impl 这个 trait 的时候,才为这个关联类型赋予确定的类型。
    type Output;//关联类型
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}

enum Poll<T> {
    Ready(T),
    Pending,
}

/// 一个SimpleFuture,它会并发地运行两个Future直到它们完成
///
/// 之所以可以并发,是因为两个Future的轮询可以交替进行,一个阻塞,另一个就可以立刻执行,反之亦然
pub struct Join<FutureA, FutureB> {
    // 结构体的每个字段都包含一个Future,可以运行直到完成.
    // 等到Future完成后,字段会被设置为 `None`. 这样Future完成后,就不会再被轮询
    a: Option<FutureA>,
    b: Option<FutureB>,
}

impl<FutureA, FutureB> SimpleFuture for Join<FutureA, FutureB>
where
    FutureA: SimpleFuture<Output = ()>,
    FutureB: SimpleFuture<Output = ()>,
{
    type Output = ();
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
        // 尝试去完成一个 Future `a`
        if let Some(a) = &mut self.a {
            if let Poll::Ready(()) = a.poll(wake) {
                self.a.take();
            }
        }

        // 尝试去完成一个 Future `b`
        if let Some(b) = &mut self.b {
            if let Poll::Ready(()) = b.poll(wake) {
                self.b.take();
            }
        }

        if self.a.is_none() && self.b.is_none() {
            // 两个 Future都已完成 - 我们可以成功地返回了
            Poll::Ready(())
        } else {
            // 至少还有一个 Future 没有完成任务,因此返回 `Poll::Pending`.
            // 当该 Future 再次准备好时,通过调用`wake()`函数来继续执行
            Poll::Pending
        }
    }
}

上面代码展示了如何同时运行多个 Future, 且在此过程中没有任何内存分配,让并发编程更加高效。 类似的,多个Future也可以一个接一个的连续运行:

/// 一个SimpleFuture, 它使用顺序的方式,一个接一个地运行两个Future
//
// 而真实的`Andthen`允许根据第一个`Future`的输出来创建第二个`Future`,因此复杂的多。
pub struct AndThenFut<FutureA, FutureB> {
    first: Option<FutureA>,
    second: FutureB,
}

impl<FutureA, FutureB> SimpleFuture for AndThenFut<FutureA, FutureB>
where
    FutureA: SimpleFuture<Output = ()>,
    FutureB: SimpleFuture<Output = ()>,
{
    type Output = ();
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
        if let Some(first) = &mut self.first {
            match first.poll(wake) {
                // 我们已经完成了第一个 Future, 可以将它移除, 然后准备开始运行第二个
                Poll::Ready(()) => self.first.take(),
                // 第一个 Future 还不能完成
                Poll::Pending => return Poll::Pending,
            };
        }

        // 运行到这里,说明第一个Future已经完成,尝试去完成第二个
        self.second.poll(wake)
    }
}

这些例子展示了在不需要内存对象分配以及深层嵌套回调的情况下,该如何使用 Future 特征去表达异步控制流。 在了解了基础的控制流后,我们再来看看真实的 Future 特征有何不同之处。

trait Future {
    type Output;
    fn poll(
        // 首先值得注意的地方是,`self`的类型从`&mut self`变成了`Pin<&mut Self>`:
        self: Pin<&mut Self>,
        // 其次将`wake: fn()` 修改为 `cx: &mut Context<'_>`:
        cx: &mut Context<'_>,
    ) -> Poll<Self::Output>;
}

首先这里多了一个 Pin ,现在你只需要知道使用它可以创建一个无法被移动的 Future ,因为无法被移动,所以它将具有固定的内存地址,意味着我们可以存储它的指针(如果内存地址可能会变动,那存储指针地址将毫无意义!),也意味着可以实现一个自引用数据结构: struct MyFut { a: i32, ptr_to_a: *const i32 }。

其次,从 wake: fn() 变成了 &mut Context<’_> 。意味着 wake 函数可以携带数据了。

使用 Waker 来唤醒任务

对于 Future 来说,第一次被 poll 时无法完成任务是很正常的。但它需要确保在未来一旦准备好时,可以通知执行器再次对其进行 poll 进而继续往下执行,该通知就是通过 Waker 类型完成的。

Waker 提供了一个 wake() 方法可以用于告诉执行器:相关的任务可以被唤醒了,此时执行器就可以对相应的 Future 再次进行 poll 操作。

构建一个定时器

下面一起来实现一个简单的定时器 Future 。为了让例子尽量简单,当计时器创建时,我们会启动一个线程接着让该线程进入睡眠,等睡眠结束后再通知给 Future 。

注意本例子还会在后面继续使用,因此我们重新创建一个工程来演示:使用 cargo new --lib timer_future 来创建一个新工程,在 lib 包的根路径 src/lib.rs 中添加以下内容:

use std::{
    future::Future,
    pin::Pin,
    sync::{Arc, Mutex},
    task::{Context, Poll, Waker},
    thread,
    time::Duration,
};

继续来实现 Future 定时器,之前提到: 新建线程在睡眠结束后会需要将状态同步给定时器 Future ,由于是多线程环境,我们需要使用 Arc//> 来作为一个共享状态,用于在新线程和 Future 定时器间共享。

pub struct TimerFuture {
    // Arc是一种能够使得数据在线程间安全共享的智能指针.它的工作方式从本质上来讲,是对将要共享的数据进行包装,并表现为此数据的一个指针。
    // Arc会追踪这个指针的所有拷贝,当最后一份拷贝离开作用域时,它就会安全释放内存。
    shared_state: Arc<Mutex<SharedState>>,
}

/// 在Future和等待的线程间共享状态
struct SharedState {
    /// 定时(睡眠)是否结束
    completed: bool,

    /// 当睡眠结束后,线程可以用`waker`通知`TimerFuture`来唤醒任务
    waker: Option<Waker>,
}

下面给出 Future 的具体实现:

impl Future for TimerFuture {
    type Output = ();
    // 函数没有返回值,那么返回一个 ()
   // 通过 ; 结尾的表达式返回一个 ()
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // 通过检查共享状态,来确定定时器是否已经完成
        let mut shared_state = self.shared_state.lock().unwrap();
        if shared_state.completed {
            // 计算完成,弹出计算数据
            Poll::Ready(())
        } else {
            // 设置`waker`,这样新线程在睡眠(计时)结束后可以唤醒当前的任务,接着再次对`Future`进行`poll`操作,
            // 下面的`clone`每次被`poll`时都会发生一次,实际上,应该是只`clone`一次更加合理。
            // 选择每次都`clone`的原因是: `TimerFuture`可以在执行器的不同任务间移动,如果只克隆一次,
            // 那么获取到的`waker`可能已经被篡改并指向了其它任务,最终导致执行器运行了错误的任务
            shared_state.waker = Some(cx.waker().clone());
            // 设置pending状态
            Poll::Pending
        }
    }
}

代码很简单,只要新线程设置了 shared_state.completed = true ,那任务就能顺利结束。如果没有设置,会为当前的任务克隆一份 Waker ,这样新线程就可以使用它来唤醒当前的任务。

最后,再来创建一个 API 用于构建定时器和启动计时线程:

impl TimerFuture {
    /// 创建一个新的`TimerFuture`,在指定的时间结束后,该`Future`可以完成
    pub fn new(duration: Duration) -> Self {
        let shared_state = Arc::new(Mutex::new(SharedState {
            completed: false,
            waker: None,
        }));

        // 创建新线程
        let thread_shared_state = shared_state.clone();
        thread::spawn(move || {
            // 睡眠指定时间实现计时功能
            thread::sleep(duration);
            let mut shared_state = thread_shared_state.lock().unwrap();
            // 通知执行器定时器已经完成,可以继续`poll`对应的`Future`了
            shared_state.completed = true;
            if let Some(waker) = shared_state.waker.take() {
                waker.wake()
            }
        });

        TimerFuture { shared_state }
    }
}

执行器 Executor

Rust 的 Future 是惰性的。其中一个推动它的方式就是在 async 函数中使用 .await 来调用另一个 async 函数,但是这个只能解决 async 内部的问题,那么这些最外层的 async 函数由执行器 executor控制 。

执行器会管理一批 Future (最外层的 async 函数),然后通过不停地 poll 推动它们直到完成。 最开始,执行器会先 poll 一次 Future ,后面就不会主动去 poll 了,而是等待 Future 通过调用 wake 函数来通知它可以继续,它才会继续去 poll 。这种 wake 通知然后 poll 的方式会不断重复,直到 Future 完成。

构建执行器

下面我们将实现一个简单的执行器,它可以同时并发运行多个 Future 。例子中,需要用到 futures 包的 ArcWake 特征,它可以提供一个方便的途径去构建一个 Waker 。编辑 Cargo.toml ,添加下面依赖:

[dependencies]
futures = "0.3"

在之前的内容中,我们在 src/lib.rs 中创建了定时器 Future ,现在在 src/main.rs 中来创建程序的主体内容,开始之前,先引入所需的包:


use {
    futures::{
        future::{BoxFuture, FutureExt},
        task::{waker_ref, ArcWake},
    },
    std::{
        future::Future,
        sync::mpsc::{sync_channel, Receiver, SyncSender},
        sync::{Arc, Mutex},
        task::{Context, Poll},
        time::Duration,
    },
    // 引入之前实现的定时器模块
    timer_future::TimerFuture,
};

执行器需要从一个消息通道( channel )中拉取事件,然后运行它们。当一个任务准备好后(可以继续执行),它会将自己放入消息通道中,然后等待执行器 poll 。

/// 任务执行器,负责从通道中接收任务然后执行
struct Executor {
    ready_queue: Receiver<Arc<Task>>,
}

/// `Spawner`负责创建新的`Future`然后将它发送到任务通道中
#[derive(Clone)]
struct Spawner {
    task_sender: SyncSender<Arc<Task>>,
}

/// 一个Future,它可以调度自己(将自己放入任务通道中),然后等待执行器去`poll`
struct Task {
    /// 进行中的Future,在未来的某个时间点会被完成
    ///
    /// 按理来说`Mutex`在这里是多余的,因为我们只有一个线程来执行任务。但是由于
    /// Rust并不聪明,它无法知道`Future`只会在一个线程内被修改,并不会被跨线程修改。因此
    /// 我们需要使用`Mutex`来满足这个笨笨的编译器对线程安全的执着。
    ///
    /// 如果是生产级的执行器实现,不会使用`Mutex`,因为会带来性能上的开销,取而代之的是使用`UnsafeCell`
    future: Mutex<Option<BoxFuture<'static, ()>>>,

    /// 可以将该任务自身放回到任务通道中,等待执行器的poll
    task_sender: SyncSender<Arc<Task>>,
}

fn new_executor_and_spawner() -> (Executor, Spawner) {
    // 任务通道允许的最大缓冲数(任务队列的最大长度)
    // 当前的实现仅仅是为了简单,在实际的执行中,并不会这么使用
    const MAX_QUEUED_TASKS: usize = 10_000;
    let (task_sender, ready_queue) = sync_channel(MAX_QUEUED_TASKS);
    (Executor { ready_queue }, Spawner { task_sender })
}

下面再来添加一个方法用于生成 Future , 然后将它放入任务通道中:

impl Spawner {
    fn spawn(&self, future: impl Future<Output = ()> + 'static + Send) {
        let future = future.boxed();
        let task = Arc::new(Task {
            future: Mutex::new(Some(future)),
            task_sender: self.task_sender.clone(),
        });
        self.task_sender.send(task).expect("任务队列已满");
    }
}

在执行器 poll 一个 Future 之前,首先需要调用 wake 方法进行唤醒,然后再由 Waker 负责调度该任务并将其放入任务通道中。创建 Waker 的最简单的方式就是实现 ArcWake 特征,先来为我们的任务实现 ArcWake 特征,这样它们就能被转变成 Waker 然后被唤醒:

impl ArcWake for Task {
    fn wake_by_ref(arc_self: &Arc<Self>) {
        // 通过发送任务到任务管道的方式来实现`wake`,这样`wake`后,任务就能被执行器`poll`
        let cloned = arc_self.clone();
        arc_self
            .task_sender
            .send(cloned)
            .expect("任务队列已满");
    }
}

当任务实现了 ArcWake 特征后,它就变成了 Waker ,在调用 wake() 对其唤醒后会将任务复制一份所有权( Arc ),然后将其发送到任务通道中。最后我们的执行器将从通道中获取任务,然后进行 poll 执行:

impl Executor {
    fn run(&self) {
        while let Ok(task) = self.ready_queue.recv() {
            // 获取一个future,若它还没有完成(仍然是Some,不是None),则对它进行一次poll并尝试完成它
            let mut future_slot = task.future.lock().unwrap();
            if let Some(mut future) = future_slot.take() {
                // 基于任务自身创建一个 `LocalWaker`
                let waker = waker_ref(&task);
                let context = &mut Context::from_waker(&*waker);
                // `BoxFuture<T>`是`Pin<Box<dyn Future<Output = T> + Send + 'static>>`的类型别名
                // 通过调用`as_mut`方法,可以将上面的类型转换成`Pin<&mut dyn Future + Send + 'static>`
                if future.as_mut().poll(context).is_pending() {
                    // Future还没执行完,因此将它放回任务中,等待下次被poll
                    *future_slot = Some(future);
                }
            }
        }
    }
}

恭喜!我们终于拥有了自己的执行器,下面再来写一段代码使用该执行器去运行之前的定时器 Future :

fn main() {
    let (executor, spawner) = new_executor_and_spawner();

    // 生成一个任务
    spawner.spawn(async {
        println!("howdy!");
        // 创建定时器Future,并等待它完成
        TimerFuture::new(Duration::new(2, 0)).await;
        println!("done!");
    });

    // drop掉任务,这样执行器就知道任务已经完成,不会再有新的任务进来
    drop(spawner);

    // 运行执行器直到任务队列为空
    // 任务运行后,会先打印`howdy!`, 暂停2秒,接着打印 `done!`
    executor.run();
}

完整代码

main.rs

use {
    futures::{
        future::{BoxFuture, FutureExt},
        task::{waker_ref, ArcWake},
    },
    std::{
        future::Future,
        sync::mpsc::{sync_channel, Receiver, SyncSender},
        sync::{Arc, Mutex},
        task::{Context, Poll},
        time::Duration,
    },
    // 引入之前实现的定时器模块
    timer_future::TimerFuture,
};

/// 任务执行器,负责从通道中接收任务然后执行
struct Executor {
    ready_queue: Receiver<Arc<Task>>,
}

/// `Spawner`负责创建新的`Future`然后将它发送到任务通道中
#[derive(Clone)]
struct Spawner {
    task_sender: SyncSender<Arc<Task>>,
}

/// 一个Future,它可以调度自己(将自己放入任务通道中),然后等待执行器去`poll`
struct Task {
    /// 进行中的Future,在未来的某个时间点会被完成
    ///
    /// 按理来说`Mutex`在这里是多余的,因为我们只有一个线程来执行任务。但是由于
    /// Rust并不聪明,它无法知道`Future`只会在一个线程内被修改,并不会被跨线程修改。因此
    /// 我们需要使用`Mutex`来满足这个笨笨的编译器对线程安全的执着。
    ///
    /// 如果是生产级的执行器实现,不会使用`Mutex`,因为会带来性能上的开销,取而代之的是使用`UnsafeCell`
    future: Mutex<Option<BoxFuture<'static, ()>>>,

    /// 可以将该任务自身放回到任务通道中,等待执行器的poll
    task_sender: SyncSender<Arc<Task>>,
}

fn new_executor_and_spawner() -> (Executor, Spawner) {
    // 任务通道允许的最大缓冲数(任务队列的最大长度)
    // 当前的实现仅仅是为了简单,在实际的执行中,并不会这么使用
    const MAX_QUEUED_TASKS: usize = 10_000;
    let (task_sender, ready_queue) = sync_channel(MAX_QUEUED_TASKS);//设置缓冲通道
    (Executor { ready_queue }, Spawner { task_sender })
}
impl Spawner {
    // 派生任务函数
    fn spawn(&self, future: impl Future<Output = ()> + 'static + Send) {
        let future = future.boxed();
        let task = Arc::new(Task {
            future: Mutex::new(Some(future)),
            task_sender: self.task_sender.clone(),
        });
        self.task_sender.send(task).expect("任务队列已满");
    }
}
impl ArcWake for Task {
    fn wake_by_ref(arc_self: &Arc<Self>) {
        // 通过发送任务到任务管道的方式来实现`wake`,这样`wake`后,任务就能被执行器`poll`
        let cloned = arc_self.clone();
        arc_self
            .task_sender
            .send(cloned)
            .expect("任务队列已满");
    }
}
impl Executor {
    fn run(&self) {
        while let Ok(task) = self.ready_queue.recv() {
            // 获取一个future,若它还没有完成(仍然是Some,不是None),则对它进行一次poll并尝试完成它
            let mut future_slot = task.future.lock().unwrap();
            if let Some(mut future) = future_slot.take() {
                // 基于任务自身创建一个 `LocalWaker`
                let waker = waker_ref(&task);
                let context = &mut Context::from_waker(&*waker);
                // `BoxFuture<T>`是`Pin<Box<dyn Future<Output = T> + Send + 'static>>`的类型别名
                // 通过调用`as_mut`方法,可以将上面的类型转换成`Pin<&mut dyn Future + Send + 'static>`
                if future.as_mut().poll(context).is_pending() {
                    // Future还没执行完,因此将它放回任务中,等待下次被poll
                    *future_slot = Some(future);
                }
            }
        }
    }
}

fn main() {
    let (executor, spawner) = new_executor_and_spawner();

    // 生成一个任务
    spawner.spawn(async {
        println!("howdy!");
        // 创建定时器Future,并等待它完成
        TimerFuture::new(Duration::new(2, 0)).await;
        println!("done!");
    });

    // drop掉任务,这样执行器就知道任务已经完成,不会再有新的任务进来
    drop(spawner);

    // 运行执行器直到任务队列为空
    // 任务运行后,会先打印`howdy!`, 暂停2秒,接着打印 `done!`
    executor.run();
}

lib.rs

use std::{
    future::Future,
    pin::Pin,
    sync::{Arc, Mutex},
    task::{Context, Poll, Waker},
    thread,
    time::Duration,
};
pub struct TimerFuture {
    // Arc是一种能够使得数据在线程间安全共享的智能指针.它的工作方式从本质上来讲,是对将要共享的数据进行包装,并表现为此数据的一个指针。
    // Arc会追踪这个指针的所有拷贝,当最后一份拷贝离开作用域时,它就会安全释放内存。
    shared_state: Arc<Mutex<SharedState>>,
}

/// 在Future和等待的线程间共享状态
struct SharedState {
    /// 定时(睡眠)是否结束
    completed: bool,

    /// 当睡眠结束后,线程可以用`waker`通知`TimerFuture`来唤醒任务
    waker: Option<Waker>,
}
impl Future for TimerFuture {
    type Output = ();
    // 函数没有返回值,那么返回一个 ()
   // 通过 ; 结尾的表达式返回一个 ()
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // 通过检查共享状态,来确定定时器是否已经完成
        let mut shared_state = self.shared_state.lock().unwrap();
        if shared_state.completed {
            // 计算完成,弹出计算数据
            Poll::Ready(())
        } else {
            // 设置`waker`,这样新线程在睡眠(计时)结束后可以唤醒当前的任务,接着再次对`Future`进行`poll`操作,
            // 下面的`clone`每次被`poll`时都会发生一次,实际上,应该是只`clone`一次更加合理。
            // 选择每次都`clone`的原因是: `TimerFuture`可以在执行器的不同任务间移动,如果只克隆一次,
            // 那么获取到的`waker`可能已经被篡改并指向了其它任务,最终导致执行器运行了错误的任务
            shared_state.waker = Some(cx.waker().clone());
            // 设置pending状态
            Poll::Pending
        }
    }
}

impl TimerFuture {
    /// 创建一个新的`TimerFuture`,在指定的时间结束后,该`Future`可以完成
    pub fn new(duration: Duration) -> Self {
        // 设置状态和唤醒函数
        let shared_state = Arc::new(Mutex::new(SharedState {
            completed: false,
            waker: None,
        }));

        // 创建新线程
        let thread_shared_state = shared_state.clone();
        // thread::spawn 函数的返回值类型是 JoinHandle, 通过 JoinHandle 来等待所有线程完成就可以解决上面执行不完的问题。
        thread::spawn(move || { //move 闭包通常和 thread::spawn 函数一起使用,它允许你使用其它线程的数据。创建线程时,把值的所有权从一个线程转移到另一个线程。
            // 睡眠指定时间实现计时功能
            thread::sleep(duration);
            // 线程加锁
            let mut shared_state = thread_shared_state.lock().unwrap();
            // 通知执行器定时器已经完成,可以继续`poll`对应的`Future`了
            shared_state.completed = true;
            if let Some(waker) = shared_state.waker.take() {
                waker.wake()
            }
        });
        // 返回创建的对象
        TimerFuture { shared_state }
    }
}

Rust 基于 await、async 的异步编程和纤程、协程的实现 它使得在 Rust 中编写异步代码变得更加简单和直观,通过结合 async/await 语法,你可以编写出清晰易读的异步代码,提高程序的并发性能和响应能力。这意味着虽然异步操作本身不会启动新线程,但它们可以在现有的线程池中的线程上执行,从而实现高效的并发处理。async 关键字用于声明一个函数是异步的,这意味着该函数可以包含挂起执行的操作(如网络请求或文件读写),而不会阻塞整个程序的执行。异步操作的执行是由运行时库(如Tokio)管理的,它们可能会在线程池中的线程上调度这些操作,以实现高效的并发处理。 阅读详情

相关推荐

【Rust异步编程】Rust异步编程原理解析、标准库实现与第三方库实现详解

异步编程是一种允许程序在等待某些操作(如I/O)完成时继续执行其他任务的编程范式。与传统的同步编程相比,异步编程可以显著提高程序的吞吐量和资源利用率。 在Rust中,异步编程具有以下特点: 非阻塞执行:任务可以在等待时让出控制权 轻量级:异步任务(称为future)比线程更轻量 显式标记:使用async/await语法明确标识异步代码

景天科技苑 4万+

Rust Future调度器原理,看这一篇就够了

这篇文章主要是讨论Rust异步编程底层调度的原理。

vince1998的博客 2115

一个用 Rust 编写的简单 CLI 番茄钟定时器。

番茄钟一个用 Rust 编写的简单 CLI 番茄钟定时器。基于番茄工作法。适用于任何支持桌面通知的平台。例子pomodoro 25 4以 25 分钟的工作间隔和 4 分钟的休息间隔开始一个番茄钟会话。

rust-异步学习

(1)如果 T: Unpin(默认会实现),那么 Pin<'a, T> 完全等价于 &'a mut T。换言之: Unpin 意味着这个类型被移走也没关系,就算已经被固定了,所以 Pin 对这样的类型毫无影响。(2)如果 T:!Unpin, 获取已经被固定的 T 类型示例的 &mut T需要 unsafe。(3)标准库中的大部分类型实现 Unpin,在 Rust 中遇到的多数“平常”的类型也是一样。但是, async/await 生成的 Future 是个例外。

wangkai6666的博客 2244

YOLOv5实战:无人机巡检图片差异对比与违建标记(附完整代码)

本文详细介绍了如何利用YOLOv5目标检测技术实现无人机巡检图片差异对比与违建标记。通过图像对齐、目标检测、差异分析和可视化标记四个步骤,构建高效的违建识别系统,显著提升城市管理效率。文章包含完整代码实现和性能优化技巧,为计算机视觉在无人机巡检中的应用提供实用指南。

weixin_42535720的博客 282

从命名玄学看单片机江湖:STC、STM32与ESP32的型号密码与设计哲学

本文解析了STC、STM32和ESP32三大主流单片机系列的型号命名规则,揭示了其背后的设计哲学与市场策略。通过详细解读STC89C52、STM32F103C8T6等具体型号,帮助开发者理解芯片核心特性、性能等级及适用场景,为嵌入式系统选型提供实用指南。

g5h6i的博客 764

Rust异步编程深度解析:从Future到Runtime

异步编程已经成为现代高性能应用开发的核心能力。无论是需要同时处理成千上万个连接的 Web 服务器,还是执行大量并发 I/O 操作的数据处理系统,异步模型都能显著提升系统的吞吐量与资源利用率。与 Go 语言的 goroutine 或 JavaScript 的 Promise 不同,Rust 的异步模型建立在零成本抽象的原则之上,通过编译期优化实现了既安全又高效的异步执行。 然而,这种设计也带来了一定的学习曲线 —— async/await 关键字背后隐藏着复杂的状态机转换,Future trait 的工作机制

Allen- 5665

【干货】SAP物料分类账的简单理解

一、SAP物料分类账理解 物料分类账,简称ML(material ledger),其基本用途是可以用八个字概括“还原成本”、“还原库存”。即通过月底运行物料分类账,将库存差异(原材料-材料成本差异、库存商品-自制半成本差异、库存商品-自制产成品差异)自动按当期库存出库数和库存结存数的比例分摊至成本和库存中。为了理解其概念,可以简单举一个例子: 开一工单,生产A产品,数量为3个,当月全部生产完工

ERP运维咨询(目标和成功) 1万+

Rust异步编程进阶:Future手动实现、Stream与异步安全

本文深入探讨了Rust异步编程的高级主题,包括手动实现Future、处理异步数据流(Stream)以及异步安全等核心概念。文章首先讲解了手动实现Future的必要性和具体步骤,包括定义Future结构体、实现Future trait、处理自引用结构体(Pin)以及组合多个Future。然后介绍了异步Stream的概念和实现方法,强调其与Iterator的相似性但操作是异步的。这些高级技术对于调试异步代码、性能优化、实现自定义异步操作以及深入理解Rust异步原理都具有重要意义。

编程世界如山海,探索不止。 9206

STM32F103(二十三)通用同步异步收发器(USART)

学习板:STM32F103ZET6 往期博客: STM32F103五分钟入门系列(一)跑马灯(库函数+寄存器)+加编程模板+GPIO总结 STM32F103五分钟入门系列(二)GPIO的七大寄存器+GPIOx_LCKR作用和配置 STM32F103五分钟入门系列(三)GPIO的常用库函数使用方法总结+一个网络上的误区 STM32F103五分钟入门系列(四)蜂鸣器实验(库函数+寄存器) STM32F103五分钟入门系列(五)按键实验(库函数+寄存器) STM32F103五分钟入门系列(六)时钟框图+相关寄存器

Curnane0_0的博客 1019

AUTOSAE软件配置-项目标准配置 (PSC) 导入达芬奇( DaVinci Configurator)

文件是“可拆分的”,这意味着如果格式符合 AUTOSAR 拆分规则,则可以与其他。)是一个ARXML文件,主要由OEM提供,其中包含。)工件,可以作为输入文件导入到达芬奇配置器专业版。导入的参数将以只读方式处理。不允许在多个 ARXML 文件中导入一个。的已存在数据或从输入文件派生的数据。可以作为输入文件导入到另一个。文件会覆盖用户配置的基本。元素和诊断数据预配置。定义 [数据结构]。中,可以导出一个或多个。

m0_49148264的博客 1017

147、掌握Rust异步编程:Future、Stream与Tokio实战

本文深入探讨了Rust语言的异步编程高级特性,重点介绍了Future、Stream等异步编程概念,并学会了使用Tokio库进行异步任务管理和调度。通过实际应用场景和实用技巧的示例,我们了解了异步编程在网络请求和数据流处理等方面的广泛应用。文章还强调了异步编程的最佳实践,如使用async/await语法、异步错误处理、避免过多的异步任务和使用Stream进行流式传输等。希望本文能够为读者提供一个良好的起点,让大家更好地掌握Rust的异步编程,并在实际项目中发挥出它的巨大潜力。

silenceallat的博客 1774

UG NX二次开发(C++)-库缺少需要的入口点的原因与解决方案

在UG NX二次开发过程中,有时会遇到形形色色的bug,比如有个读者其采用UG NX二次开发(C++)时,遇到“库缺少需要的入口点”这个问题,看了一下他发给我的源代码,感觉有必要讲一下UG NX二次开发(C++)遇到此类问题的可能情况与解决方法。自然,这是博主的经验之谈,有不足之处还请在评论区讨论。

GimiGimmy 2202

Rust 异步编程深度解析:从 Future 到运行时

本文深入解析Rust异步编程的底层机制,从Future trait的设计实现到运行时的工作流程。Rust通过惰性执行的Future状态机、Pin机制解决自引用问题,实现了零成本抽象的并发处理。文章详细剖析了async/await语法糖背后的编译器转换、Waker调度机制,以及Tokio运行时的任务调度和I/O反应器实现。同时提供了性能优化实践和常见陷阱规避策略,帮助开发者充分利用Rust异步编程的优势,在保证内存安全的前提下实现高性能并发。

踏雨歌青春,诗酒趁年华 6123

数据库小技能:数据导入脚本

背景:版本大升级,从2.0升级到3.0 ,服务端开发语言从c#换到java语言。数据库类型从microsalft sql server 换到mysql。数据来源于微信,用于查询卡bin。

专注于计算机研发知识和资讯分享 774

用 YOLOv11‑Seg 做工地实例分割:从 0 起步到效果调优,真香!

这不是一篇“官方文档翻译”,而是一篇。

weixin_43199439的博客 776

Rust 异步编程详解(async/await)

Rust异步编程通过async/await语法和Future trait实现高效I/O处理,核心机制包括状态机模型和运行时调度。文章详细解析了异步编程基础、核心组件(Future trait、async/await语法)、工作流程,并提供了HTTP请求等代码示例。同时探讨了错误处理、任务取消、Pin/Unpin等高级话题,比较了不同运行时的适用场景。最佳实践建议选择合适运行时、避免阻塞操作,并指出异步编程在性能和安全方面的优势,以及调试复杂度等限制。附带的并发网络请求示例展示了tokio运行时的实际应用,包

本博客分享软硬件一体化开发实战。硬件涵盖 PCB 设计、STM32 等 MCU 底层开发;软件包含 C++、Rust,Qt/QML、PyQt、Slint、TouchGFX、LVGL 各类 GUI 框架。方便软硬件开发者查阅学习。欢迎关注! 2224

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

Rust Future Trait 定义与实现:异步编程的基石

Ready(T),Pending,这个定义解决了异步编程的核心问题:如何让计算暂停和恢复?传统的线程模型依赖操作系统的抢占式调度和栈的自动保存,而 Rust 选择了显式的、基于轮询的模型。每次调用poll()时,Future 要么报告已完成(Ready),要么报告仍在等待(Pending关键在于,每当 Future 不再有进展时,它应该注册一个 Waker 回调,使得当条件满足时,执行器能够重新唤醒它。Pin 的引入解决了 self-referential 结构体的安全问题。

2501_94019712的博客 688

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

【Rust中的异步编程】

rust中的异步模型,实际上是使用future(能产出值的异步计算),通过轮询,挂起,唤醒,执行等操作来实现异步,runtime依赖tokio。Tokio是一个Rust 异步运行时库,底层基于epoll/kqueue这样的跨平台多路IO复用以及event loop,目前正在支持io_uring。其线程会执行task,可以充分利用多核,而Task 线程是Rust基于Future抽象出的一种绿色线程,因为不需要预先分配多余的栈内存,可以创建大量的Task,很适合做IO密集型应用。fn poll(

m0_37719524的博客 1676

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

003Rust异步编程,Future trait介绍

Future介绍 Future是Rust异步编程的核心,Rust异步编程基本都是围绕Future来展开。那么,什么是Future呢? 首先,我们来看下简化版的Future,如下: trait SimpleFuture { type Output; fn poll(&mut self, wake: fn()) -> Poll<Self::Output>; } enum Poll<T> { Ready(T), Pending, }

令狐一冲 1088

Rust 精要系列(八)—— 异步编程与 Future 深入解析

Rust 的异步系统是一种零成本抽象通过编译器在编译阶段生成高效的状态机,实现与同步代码几乎相同的性能。同时,借助严格的类型系统,Rust 保证了异步代码在多任务并发下的内存与线程安全。从Future的惰性求值到运行时调度的精妙设计,Rust 让开发者可以在无需垃圾回收的前提下,编写出高效、安全、可预测的并发程序。在下一篇中,我们将继续深入探讨Rust 的模块化设计与大型项目架构实践,帮助你更系统地组织与维护复杂的 Rust 工程。

guanmingyuangmy的博客 790

Rust Async 异步编程(二):Future trait

Rust Async 异步编程(二):Future trait

ProgramNovice的博客 716

Rust Web 进阶(一):Rust异步编程(Tokio)

这一篇将讲解什么是和为什么进行异步编程,Rust 怎么样进行异步编程以及其机制,并且讲解目前 rust 常用的异步编程的函数库。本章的内容来自杨旭老师的教程: https://www.bilibili.com/video/BV16r4y187P4/?spm_id_from=333.999.0.0&vd_source=8595fbbf160cc11a0cc07cadacf22951

weixin_46463785的博客 2681

【Rust异步】async和await异步编程实战:高并发任务处理全解析

async 和 await 是 Rust 中用于异步编程的关键字。 async 用于定义异步函数,表示函数体中包含异步代码。 await 用于等待异步操作完成,并返回异步操作的结果。 • 异步函数使用 async 关键字定义,并返回实现了 Future Trait 的类型。 异步函数可以在其他异步函数中使用 await 关键字等待异步操作完成。 调用异步函数时,会返回一个实现了 Future Trait 的对象,可以通过调用.await 方法等待结果。

景天科技苑 2万+

Rust 异步编程基础

本文意在作为对前两章的导读,希望能帮助读者理解 Rust 异步编程的底层逻辑。读者应该先坚持着把 Rust 异步编程圣经的前两章读上两三遍,不必做到理解,但至少先熟悉它说了什么,再带着问题回来看这个导读。

Vallelonga的博客 1483

Rust异步编程 Async Await 入门

要了解什么是异步代码,我们首先来谈谈同步代码。println!println!上述语句按照明确的顺序执行,从上到下一个接一个地执行。” 打印,然后读取并打印Cargo.toml的内容。这种模式在正常操作下非常好 - 但有时代码需要在当前上下文在等待其他内容时暂停 - 这通常称为阻塞。换句话说,当一段代码被阻塞时,它实际上处于暂停状态,等待特定操作完成才能继续。例如,当等待文件系统、网络通信、数据库事务甚至一段时间过去时,就会发生这种情况。在此阻塞状态期间,程序保持空闲状态,无法同时执行其他任务。

xuejianxinokok的专栏 1657
上一篇: spring cloud 整合Feign经行远程调用
下一篇: rust高级 异步编程 二 pin
过去日记
过去日记 新星创作者: 后端开发技术领域 新星创作者: 后端开发技术领域
博客等级 码龄3年 5343粉丝 225原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

过去日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值