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

一、Rust 异步调度底层原理

    在Rust异步编程中,有一个async 关键字,async 修饰的函数具备异步非阻塞的执行能力,原理是async修饰的函数具备了Future特征,而Future是Rust异步调度的关键单元,这篇文章主要是讨论Rust异步编程底层调度的原理。

1、Rust调度模型设计

在讨论底层如何进行调度之前,我们先明确Rust的调度模型是怎么样的。

Future相当于一个函数,在调度系统中可以理解成一个任务,这个任务是运行在某个线程上的。

GolangGMP调度模型中,通过go关键字,可以创建个协程G协程GOS的内核级线程M:N的关系。

RustFuture调度模型中,通过async关键字,可以创建多个Future,在一个线程上创建的多个FutureFuture该线程的关系是N:1的关系,

修正:如果执行器是多线程的,Future该线程的关系可能是M:N的关系

非常关键!!!!
Rust Future的执行是被执行器poll(轮询)后才能运行poll其实是Future特征的一个函数,当轮到这个Future执行时,执行器会去执行这个future的poll函数,推进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
        }
    }
}

2、Rust调度模型详细过程

先说结果,我们再通过代码中验证我们的结果。有点像Epoll,执行器类似epoll中的操作系统调度,Future是监听事件,执行器不需要全部逐个检查Future是否能执行,而是类似future通知执行器,我可以执行。通知是通过waker来通知的。

  1. 创建一个管道任务队列,用来存放Task任务(Future),sender,receiver。sender给spawner用于发送任务、receiver给executor接收任务并执行。
  2. spawner::spawn方法调用,创建一个Future(async函数),并将这个Future包裹为一个Task,这个Task克隆了一份spawner的发送管道引用(其实还是用那个管道,但是有两个入口了)。Spawner将task发送进管道
  3. Executor会死循环检查管道任务队列,如果管道队列中有任务,取出任务future任务不是空的 => 给Future任务创建一个Waker
  4. Poll这个Future,如果结果是Pending, 那么将这个future放回Task的future槽。
  5. 如果不是Pending那么该任务结束。
  6. 该任务线程会在准备继续的时候调用第3步给他创建的Waker,并将自己放回任务管道。
  7. 再次被Executor接收到,取出Future,创建waker,poll执行…循环4~6

角色大概可以分成几种:Future(Task)、执行器(负责执行Future)、任务队列(存放可以执行,但是等待执行的Future)、Waker(Future不阻塞后,通过Waker将自己放进任务队列中)

步骤一,创建一个管道任务队列,并返回一个接收者、发送者,我们可以直接引入包mpsc,当然还包括其他

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,
};

步骤二主要是讲Future(Task)如何放到任务管道中的,这个我们后面再说
步骤三提到一个执行器,我们来看看执行器构建的代码

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

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

/// 一个Future,它可以调度自己(将自己放入任务通道中),然后等待执行器去`poll`
struct Task {
    /// 包着一个进行中的Future,在未来的某个时间点会被完成
  
    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;
    // task_sender::发送器,给Spawner
    //ready_queue管道任务队列接收器,给执行器
    let (task_sender, ready_queue) = sync_channel(MAX_QUEUED_TASKS);
    (Executor { ready_queue }, Spawner { task_sender })
}

步骤三还提到执行器死循环检查管道任务队列,我们来看看执行器运行的代码

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);
                }
            }
        }
    }
}

步骤四 步骤三代码用到一个let waker = waker_ref(&task);,正好可以弥补4说的,如果future结果是Pending,给Future任务创建一个Waker,将这个future放回Task的future槽,等future阻塞结束,调用wake函数,future又放回管道任务队列中,等待执行器再次执行该futurepoll

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("任务队列已满");
    }
}

到此结束了哈哈,写完我也更清晰啦
欢迎查看我的博客
在这里插入图片描述

  • 28
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值