Loading

tokio官方文档中一些值得记录的

前言

Rust真tema难啊...

本文是Tokio官方文档中一些值得记录的点的翻译,并非全部原文。更多细节请看:tokio.rs

Tokio是Rust的一款高性能的异步运行时

任务

Tokio任务是一个异步绿色线程,它们通过向tokio::spawn中传递一个async块来创建。tokio::spawn函数返回一个JoinHandle,调用者可能使用它来与被创建的任务交互。async块可能有一个返回值,调用者可以通过在JoinHandle.await来获取这个返回值。

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        // Do some async work
        "return value"
    });

    // Do some other work

    let out = handle.await.unwrap();
    println!("GOT {}", out);
}

JoinHandle返回一个Result,当任务遇到一个错误,JoinHandle将返回一个Err,这会发生在任务panic或者由于运行时关闭而被强制取消。

任务是被调度器所管理的执行单元,产生(spawning)任务就是向tokio的调度器提交它,也就是确保任务会在它有事要做时得到执行。产生的任务可能会在它产生时相同的线程执行,或者可能在一个不同的运行时线程执行,任务也可以在产生后在线程间移动

'static bound

当你向tokio运行时产生一个task时,它的生命周期必须是'static的,这意味着被生成的task必须不能包含任何被任务外部所拥有的数据的引用

比如,下面的代码是无法编译的

use tokio::task;

#[tokio::main]
async fn main() {
    let v = vec![1, 2, 3];

    task::spawn(async {
        println!("Here's a vec: {:?}", v);
    });
}

这是很重要的,因为编译器并不知道一个新产生的任务会驻留多久,所以我们必须让确保让任务能够永远运行下去。

Send bound

tokio::spawn调用产生的Task必须实现了Send,这允许Tokio运行时在Task在.await上挂起时在线程间移动它们

当Task中所有的被await调用所保持住的数据是Send的,Task就是Send的。这可能并不容易察觉。当.await被调用了,任务就会返回(yield)给调度程序,当下次任务被执行时,它从上一次返回的点恢复。为了使这成为可能,所有在.await之后要用到的状态都必须被任务保存。如果状态是Send,任务本身就是Send,反之亦然。

比如,下面的代码可以工作:

use tokio::task::yield_now;
use std::rc::Rc;

#[tokio::main]
async fn main() {
    tokio::spawn(async {
        // scope迫使`rc`在`.await`前drop
        {
            let rc = Rc::new("hello");
            println!("{}", rc);
        }

        // `rc`不会再被用到,当任务返回到调度器时,它不会被持久化
        yield_now().await;
    });
}

下面的不行:

use tokio::task::yield_now;
use std::rc::Rc;

#[tokio::main]
async fn main() {
    tokio::spawn(async {
        let rc = Rc::new("hello");

        // `rc`在`.await`之后也会用到,他必须被持久化到任务状态中
        yield_now().await;

        println!("{}", rc);
    });
}

共享状态 策略

在Tokio中有一些共享状态的策略

  1. 使用Mutex来保护共享状态
  2. 使用一个任务来管理状态,并且使用消息来向它传递操作

任务,线程以及争用

使用一个阻塞互斥量(Mutex)来保护较短的关键部分是一个在争用较小情况下可接受的策略。当锁被争用,执行任务的线程必须阻塞并等待锁,这不仅会阻塞当前任务,也会阻塞所有在当前线程上被调度的其它任务。

默认情况下,Tokio运行时使用多线程调度器,任务会在任意数量的被运行时管控的线程内执行,如果很大数量的任务被调度执行,并且它们都需要获取互斥量,就将会出现争用。另一方面,如果current_thread运行时flavor被使用了,互斥量将永远不会被争用。

current_thread运行时flavor是一个轻量级,单线程的运行时flavor,当你只会产生少量的任务并且只打开少数Socket时,它是一个不错的选择。当你想在一个异步客户端库上提供一个同步的API bridge时,这是一个很好的选择。

如果同步互斥量的争用成为一个问题时,最好的选择几乎很少是切换到Tokio的mutex上(tokio提供了一个自己的mutex实现),相反,你可以考虑如下选择:

  • 切换到专用的管理状态的任务,以及使用消息传递
  • 对mutex进行分片
  • 重构代码以避免mutex

官方文档中编写了一个分片的小示例,非常有意思,由于本篇不想卷入代码,所以有兴趣请参考官方文档

.await间持有MutexGuard

只记录了其中的一小段,实际上和标题已经没啥关系了

如果Tokio在一个.await处挂起了你的任务,并且你的任务正持有互斥锁时,其它的任务可以在相同的线程上被调度,并且这个其它任务可能也会尝试锁定这个mutex,这将导致死锁,因为等待锁定mutex的任务将会阻止正在持有mutex的任务释放它。

即不要在.await时仍持有锁,可以通过作用域来使锁释放,也可以使用tokio::sync::Mutex,它可以在.await之间被正确的持有,但是它要比普通的mutex更加昂贵。

接收响应(tokio::sync::oneshot::channel的使用)

oneshot::Sender上调用send总会立即完成,不需要一个.await。这是因为在一个oneshot通道上的send总是立即成功或失败,无需任何等待。

当接收者端销毁之后,在一个oneshot通道上发送一个值会返回Err,这代表接收者不再对响应感兴趣。

背压和无界通道

当引入并发或排队后,确保队列是有界的,并且系统会优雅地处理负载是很重要的。无界的队列最终会填满所有可能内存并且导致系统发生不可预测的后果。

tokio在避免隐式派对上下了功夫,最大的事实就是异步操作是惰性的。考虑如下:

loop {
    async_op();
}

如果异步操作非惰性的运行,那循环将会重复的将一个async_op入队执行,而不会确定前一个操作是否已经完成。这将造成隐式的无界队列。基于回调的系统以及基于非惰性的future的系统在这一点上特别敏感。

然而,在Tokio与异步Rust中,上面的代码片段甚至不会导致async_op运行。这是因为.await尚未被调用。如果代码段更新成使用.await,那么循环将会等待操作结束后开始。

loop {
    // 不会循环直到`async_op`结束
    async_op().await;
}

并发和排队必须被显式引入,可以使用以下方式:

  • tokio::spawn
  • select!
  • join!
  • mpsc::channel

当你这样做时,请注意确保并发总量是有界的,比如,当你编写一个TCP accept循环时,确保开放的套接字的总数是有界的。当你使用mpsc::channel时,选择一个可管理的通道容量。具体使用什么特定的值,这依赖于具体的程序。

时刻想着并选择一个好的界限是编写可靠的tokio应用的一个大部分。

AsyncReadAsyncWrite

这两个特质提供了异步读取和写入字节流的能力。这些Trait上的方法通常并不直接调用,就像你从不直接调用Futurepoll方法一样。取而代之的是,你将通过AsyncReadExtAsyncWriteExt提供的工具方法来使用它们。

我们简单的看一下这些方法,所有这些方法都是async的并且必须使用.await

async fn read()

AsyncReadExt::read提供了一个读取数据到buffer中的异步方法,返回读了多少字节。

注意:当使用read()返回Ok(0)时,这代表流已经关闭了,任何对read()的进一步调用都将立即完成,并返回Ok(0)。对于TcpStream示例,这意味着套接字的读端已经关闭了。

async fn read_to_end()

AsyncReadExt::read_to_end读取流中所有字节直到EOF。

async fn write()

AsyncWriteExt::write向writer中写入一个buffer,返回写入了多少个字节。

async fn write_all()

AsyncReadExt::write_all将整个buffer写入writer。

helper函数

就像stdtokio::io模块包含了一系列helper函数。比如,tokio::io::copy异步的从一个reader中拷贝所有数据到writer。

官方文档中使用tokio::io::copy仅用了几行代码就实现了一个回声服务器,感兴趣的可以去看看

Buf特质

// 前置代码片段
pub struct Connection {
    stream: TcpStream,
    buffer: BytesMut, // [+] 注意这行代码
}

pub async fn read_frame(&mut self)
    -> Result<Option<Frame>>
{
    loop {
        // 尝试从缓冲的数据中解析一个帧,如果buffer中有足够的数据,帧将被返回
        if let Some(frame) = self.parse_frame()? {
            return Ok(Some(frame));
        }

        // 没有足够的缓冲数据可以构建一个帧
        // 尝试从socket读取更多数据
        // 在成功时,如果返回的字节数量是0,代表“end of stream”
        if 0 == self.stream.read_buf(&mut self.buffer).await? { // [+] 注意这行代码
            // 远端关闭了连接,如果是一个干净的关闭,那么读缓冲中应该没有数据
            // 如果有,证明远端在发送一个帧时关闭了socket
            if self.buffer.is_empty() {
                return Ok(None);
            } else {
                return Err("connection reset by peer".into());
            }
        }
    }
}

当我们从流中读取时,read_buf被调用了,这个读取函数需要一个实现了BufMut的值,BufMut来自于bytescrate中。

首先,考虑如果我们使用read()来实现这个读循环,并且用Vec<u8>代替BytesMut

pub struct Connection {
    stream: TcpStream,
    buffer: Vec<u8>, // BytesMut被替换成了更加原始的`Vec<u8>`
    cursor: usize,   // 我们必须维护一个游标来代表当前我们在buffer中的未知
}
pub async fn read_frame(&mut self)
    -> Result<Option<Frame>>
{
    loop {
        if let Some(frame) = self.parse_frame()? {
            return Ok(Some(frame));
        }

        // 确保buffer还有足够的容量(如果游标到达最后,则容量不足)
        if self.buffer.len() == self.cursor {
            // 增长buffer
            self.buffer.resize(self.cursor * 2, 0);
        }

        // 读到buffer中,跟踪读了多少字节
        let n = self.stream.read(
            &mut self.buffer[self.cursor..]).await?;

        if 0 == n {
            if self.cursor == 0 {
                return Ok(None);
            } else {
                return Err("connection reset by peer".into());
            }
        } else {
            // 更新游标
            self.cursor += n;
        }
    }
}

当我们使用字节数组以及read函数,我们必须维护一个游标来跟踪我们已经缓冲了多少数据,我们必须确保将缓冲区的空部分传递给read(),否则,我们将覆盖掉已缓冲的数据。如果buffer被填满,我们必须增长buffer以确保继续读取。在parse_frame()中(代码中没有体现),我们必须解析self.buffer[..self.cursor]之间的数据。

因为将一个字节数组和一个游标绑定的需求太常见了,bytescrate提供了一个代表字节数组和一个游标的抽象。Buf特质是由可读取的数据类型实现的,而BufMut是由可写入的数据类型实现的。当你传递一个T: BufMutread_buf()中,buffer内部的游标自动的被read_buf更新,因此,在我们的read_frame版本中,我们并不需要管理自己的游标。

缓冲区写

函数以self.stream.flush().await调用结束。因为BufWriter在其内部的buffer中存储你的写入,调用write并不保证数据会被写入到socket中。在返回之前,我们希望帧被写入到socket了,flush()调用写入任何还在buffer中等待的数据到socket中。

Futures

use tokio::net::TcpStream;

async fn my_async_fn() {
    println!("hello from async");
    let _socket = TcpStream::connect("127.0.0.1:3000").await.unwrap();
    println!("async TCP operation complete");
}

当我们调用这个函数,他返回了某种值,我们在这个值上调用了.await

#[tokio::main]
async fn main() {
    let what_is_this = my_async_fn();
    // 目前还没有输出
    what_is_this.await;
    // 文字被输出,并且socket已经被建立并关闭
}

my_async_fn返回了一个future,future是标准库提供的一个实现了std::future::Future特质的值,他们是一种包含了正在执行的异步计算的值。

std::future::Future特质的定义如下:

use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context)
        -> Poll<Self::Output>;
}

Output关联类型是当future完成时产生的值的类型,Pin类型是Rust用于允许在async函数中进行借用的类型。更多细节请查看标准库文档。

不像其它语言中的future实现,Rust future并不代表一个正在后台执行的计算,Rust的future本身就是这个计算。future的拥有者负责通过轮询future来推动计算,这将通过调用Future::poll来完成。

实现Future

让我们来实现一个非常简单的future,这个future将:

  1. 等待到特定的时刻
  2. 向STDOUT输出一些文本
  3. 返回一个字符串
use std::future::Future;
use std::ops::Add;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};

struct Delay {
    when: Instant
}

impl Future for Delay {
    type Output = &'static str;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if Instant::now() >= self.when {
            println!("Hello World!");
            Poll::Ready("done")
        } else {
            // 先忽略这一行
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

#[tokio::main]
async fn main() {
    Delay {
        when: Instant::now().add(Duration::from_secs(10))
    }.await;
}

异步函数作为一个Future

在main函数中,我们实例化了future并且在其上调用了.await异步函数中,我们可能在任意实现了Future的值上调用.await,而反过来,调用一个async函数返回了一个实现了Future的匿名类型,在async fn main()这个例子中,生成的future大概是如下这样:

enum MainFuture {
    // Initialized, never polled
    State0,
    // Waiting on `Delay`, i.e. the `future.await` line.
    State1(Delay),
    // The future has completed.
    Terminated,
}

impl Future for MainFuture {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>)
        -> Poll<()>
    {
        use MainFuture::*;

        loop {
            match *self {
                State0 => {
                    let when = Instant::now() +
                        Duration::from_millis(10);
                    let future = Delay { when };
                    *self = State1(future);
                }
                State1(ref mut my_future) => {
                    match Pin::new(my_future).poll(cx) {
                        Poll::Ready(out) => {
                            assert_eq!(out, "done");
                            *self = Terminated;
                            return Poll::Ready(());
                        }
                        Poll::Pending => {
                            return Poll::Pending;
                        }
                    }
                }
                Terminated => {
                    panic!("future polled after completion")
                }
            }
        }
    }
}

Rust的future是状态机。这里,MainFuture被表示为一个Future的可能状态的enum。future从State0开始,当poll被调用,future尝试尽可能地推进它的内部状态,如果future已经能够完成了,Poll::Ready会被返回,其内部包含着异步计算的输出。

如果future不能完成,通常这是由于等待的资源还没有准备好,Poll::Pending就会被返回。Poll::Pending的接收向调用者指明了future将会在稍后完成,调用者将在稍后调用poll

我们也会看到future与其它future组合,调用外部future的poll将导致内部future的poll也得到调用。

执行器(Executors)

异步Rust函数返回futures,必须通过在其上调用poll来推进它们的状态,Future是其它future的组合。问题是,在最外层的future上,是谁来调用的poll

回顾之前,为了运行异步函数,它们必须被传入tokio::spawn或者在被#[tokio::main]所标注的main函数中。这将导致提交一个自动生成的外层future到Tokio的执行器中。执行器负责调用外层future的Future::poll,驱动异步计算的完成。

Mini Tokio

为了更好的理解这些东西是如何写作的,让我们实现我们自己的tokio最小版本,完整代码可以从这里找到。

use std::collections::VecDeque;
use std::future::Future;
use std::ops::Add;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use futures::task;

fn main() {
    let mut mini_tokio = MiniTokio::new();

    mini_tokio.spawn(async {
        Delay {
            when: Instant::now().add(Duration::from_secs(10))
        }.await;
    });

    mini_tokio.run();
}

type Task = Pin<Box<dyn Future<Output = ()> + Send>>;
struct MiniTokio {
    tasks: VecDeque<Task>
}

impl MiniTokio {
    fn new() -> MiniTokio {
        MiniTokio {
            tasks: VecDeque::new()
        }
    }

    fn spawn<F>(&mut self, future: F)
        where F: Future<Output = ()> + Send + 'static {
        self.tasks.push_back(Box::pin(future));
    }

    fn run(&mut self) {
        let waker = task::noop_waker();
        let mut cx = Context::from_waker(&waker);

        while let Some(mut task) = self.tasks.pop_front() {
            if task.as_mut().poll(&mut cx).is_pending() {
                self.tasks.push_back(task);
            }
        }
    }
}

它(MiniTokio)执行异步代码块。一个Delay实例被创建,携带了需要的延时时间并在上面等待(awaited on)。然而,我们的实现目前有一个大问题,我们的执行器永远不会睡眠。这个执行器不停地循环所有已提交的future并且轮询(poll)它们,大多数时间,future并没有准备好执行更多的工作,并且它会再次返回Poll::Pending。这个过程将不断占用CPU时钟周期并且十分低效。

理想状态下,我们希望mini-tokio仅仅在future可以推进时轮询它,这发生在任务当前阻塞在其上的资源已经准备好去执行请求的操作时。如果任务想要从一个TCP套接字上读取数据,那么我们应该仅当TCP套接字接收到数据时才轮询任务。在我们的例子中,task阻塞在给定的Instant到达这一事件上,理想情况下,mini-tokio将仅仅在到达的那一时刻轮询任务。

为了达到至一点,当一个资源被轮询,并且它还没有准备好时,一旦它进入就绪状态,资源将发送一个通知。

Waker(唤醒者)

Waker,就是我们要找的部分。通过这个系统,资源可以通知正在等待的任务,它已经准备好在某些操作上执行了。

我们再次看一下Future::poll的定义:

fn poll(self: Pin<&mut Self>, cx: &mut Context)
    -> Poll<Self::Output>;

pollContext参数具有一个waker()方法,这个方法返回一个绑定到当前任务的Waker。这个Waker具有一个wake()方法。调用这个方法通知执行器关联任务应该被再次调度执行。当资源进入就绪状态,它就可以调用wake()以通知执行器,现在轮询任务已经能够向下推进了。

更新Delay

我们可以更新Delay以使用waker:

impl Future for Delay {
    type Output = &'static str;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if Instant::now() >= self.when {
            println!("Hello World!");
            Poll::Ready("done")
        } else {
            let waker = cx.waker().clone();
            let when = self.when;

            thread::spawn(move || {
                let now = Instant::now();

                if now < when {
                    thread::sleep(when - now);
                }

                waker.wake();
            });
            Poll::Pending
        }
    }
}

现在,一旦请求的延时时间到达,调用任务就会被通知,并且执行器可以保证任务会再次被调度。下一步,就是更新mini-tokio来监听wake通知。

我们的Delay实现依然有一些问题,我们将会在后面修复它:

当一个future返回了Poll::Pending,它必须确保waker在某一时间点被通知,忘了做这件事将会导致任务永久的挂起。

在返回Poll::Pending后忘记唤醒任务是一个常见的bug来源。

再看一下Delay的第一个版本,这是我们的future实现:

impl Future for Delay {
    type Output = &'static str;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>)
        -> Poll<&'static str>
    {
        if Instant::now() >= self.when {
            println!("Hello world");
            Poll::Ready("done")
        } else {
            // Ignore this line for now.
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

在返回Poll::Pending之前,我们调用了cx.waker().wake_by_ref(),这是为了满足future契约。在我们返回Poll::Pending时,我们必须负责通知waker,因为我们那时尚未实现timer线程,所以我们直接发出了唤醒信号。这样做将导致future立即被重调度并再次执行,那是他还可能没有准备好完成。

注意,你被允许比必要情况下更加频繁的通知waker,在特殊情况下,我们会向waker发起信号,即使它们还没有完全准备好。除了浪费一些CPU时钟周期,这没什么错的。然而,这个特定的实现(第一版Delay)将会导致一个繁忙的循环。

我是译者:Rust中的异步编程的一个目标是非阻塞,也就是不阻塞线程,以多路复用尽可能少的线程。我不知道这是不是官方的用意,但在我读的两本关于Rust异步编程的书中都有这样写,而上面的Delay第二版实现创建了一个线程并让它阻塞一会儿。

作为一个示例代码,我们不评价这样做的好坏,我也不知道有什么更好的办法,但是我们可以通过这一点看出,Rust的异步编程并没那么神奇,并不是你用它就能写出完全不阻塞线程的代码,你实际上也可以在里面写出会阻塞线程的代码。

译者水平有限,如果上面有什么说错的地方,欢迎指正。

更新Mini Tokio

下一步就是更新Mini Tokio来接收waker通知。我们希望执行器仅仅在任务被唤醒后执行任务,为了做到这一点,Mini Tokio将提供它自己的waker。当waker被调用,其关联的任务将会被入队,以被执行。Mini Tokio在它轮询future时将waker传递给future。

更新的Mini Tokio将使用一个通道来保存被调度的任务,通道允许任务从任何线程中入队以待执行。Waker必须是SendSync的,所以我们使用crossbeam crate中的channel,因为标准库中的通道不是Sync的。

SendSync特质是Rust提供的有关并发的标记trait。可以发送到另一个线程的类型是Send的。很多类型都是Send的,但是像Rc这种不是。可以通过不可变引用被并发访问的类型是Sync的,一个类型可以是Send的但不是Sync的——Cell就是一个很好的例子,它可以通过一个不可变引用被修改,所以因此,他并不是并发访问安全的。

下面,更新MiniTokio结构:

use crossbeam::channel;
use std::sync::Arc;
struct Task {
    // 稍后编写
}
struct MiniTokio {
    scheduled: channel::Receiver<Arc<Task>>,
    sender: channel::Sender<Arc<Task>>,
}

Wakers是Sync的并且可以被克隆,当wake被调用,task必须被放进计划以执行。为了实现这一点,我们用了一个channel。当waker上的wake()被调用,任务就会被推到通道的发送端。我们的Task结构将实现wake逻辑,为此,它需要包含生成的future以及通道的发送端。

struct Task {
    // `Mutex`是为了让`Task`实现`Sync`。
    // 只有一个线程能在给定时间访问future。
    // `Mutex`并不是实现正确性所必须的,真实的Tokio
    // 并没有在这里使用mutex,但是真实的Tokio的
    // 代码太多了,无法放在一个教程页面里
    future: Mutex<Pin<Box<dyn Future<Output = ()> + Send>>>,
    executor: channel::Sender<Arc<Task>>
}

impl Task {
    fn schedule(self: &Arc<Self>) {
        self.executor.send(self.clone());
    }
}

为了计划调度这个任务,Arc被克隆并发送到通道中。现在,我们需要将我们的schedule函数与std::task::Waker挂钩(Task现在还不是一个Waker)。标准库提供了一个低级API来做这件事,你可以使用手动vtable构建。这个策略给实现者提供了最大的灵活性,但是需要一些unsafe模板代码。为了避免直接使用RawWakerVTable,我们使用futurescrate提供的ArcWake工具。这允许我们实现一个单一的trait来将我们的Task结构体作为一个waker暴露。

use futures::task::{self, ArcWake};
use std::sync::Arc;
impl ArcWake for Task {
    fn wake_by_ref(arc_self: &Arc<Self>) {
        arc_self.schedule();
    }
}

当上面的timer线程调用waker.wake(),task就会被推送到channel。下面,我们在MiniTokio::run()函数中实现接收并执行任务。

// 完整代码
use std::collections::VecDeque;
use std::future::Future;
use std::ops::Add;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::thread;
use std::time::{Duration, Instant};
use futures::task;
use crossbeam::channel;
use std::sync::{Arc, Mutex};
use futures::task::ArcWake;

fn main() {
    let mut mini_tokio = MiniTokio::new();

    mini_tokio.spawn(async {
        Delay {
            when: Instant::now().add(Duration::from_secs(10))
        }.await;
    });

    mini_tokio.run();
}

struct Task {
    // `Mutex`是为了让`Task`实现`Sync`。
    // 只有一个线程能在给定时间访问future。
    // `Mutex`并不是实现正确性所必须的,真实的Tokio
    // 并没有在这里使用mutex,但是真实的Tokio的
    // 代码太多了,无法放在一个教程页面里
    future: Mutex<Pin<Box<dyn Future<Output = ()> + Send>>>,
    executor: channel::Sender<Arc<Task>>
}

impl Task {
    fn poll(self: Arc<Self>) {
        let waker = task::waker(self.clone());
        let mut cx = Context::from_waker(&waker);

        let mut  future = self.future.lock().unwrap();

        let _ = future.as_mut().poll(&mut cx);
    }

    fn spawn<F>(future: F, sender: &channel::Sender<Arc<Task>>)
        where F: Future<Output = ()> + Send + 'static {
        let task = Arc::new(Task {
            future: Mutex::new(Box::pin(future)),
            executor: sender.clone()
        });
        let _ = sender.send(task);
    }

    fn schedule(self: &Arc<Self>) {
        self.executor.send(self.clone());
    }
}

impl ArcWake for Task {
    fn wake_by_ref(arc_self: &Arc<Self>) {
        arc_self.schedule();
    }
}
struct MiniTokio {
    scheduled: channel::Receiver<Arc<Task>>,
    sender: channel::Sender<Arc<Task>>,
}

impl MiniTokio {
    fn new() -> MiniTokio {
        let (sender, scheduled) = channel::unbounded();
        MiniTokio { sender, scheduled }
    }

    fn spawn<F>(&mut self, future: F)
        where F: Future<Output = ()> + Send + 'static {
        Task::spawn(future, &self.sender);
    }

    fn run(&mut self) {
        while let Ok(task) = self.scheduled.recv() {
            task.poll();
        }
    }
}

struct Delay {
    when: Instant
}

impl Future for Delay {
    type Output = &'static str;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if Instant::now() >= self.when {
            println!("Hello World!");
            Poll::Ready("done")
        } else {
            let waker = cx.waker().clone();
            let when = self.when;

            thread::spawn(move || {
                let now = Instant::now();

                if now < when {
                    thread::sleep(when - now);
                }

                waker.wake();
            });
            Poll::Pending
        }
    }
}

这短短的代码里发生了很多事,首先,MiniTokio::run()被实现了。函数在一个循环中运行,接收channel过来的排队任务。当任务被唤醒,它们会被推送到channel中,这些任务在被执行时就能够推进了。

另外,MiniTokio::new()以及MiniTokio::spawn()函数被调整成使用channel代替VecDeque。当一个新任务被生成时,将为它们提供到通道发送端的克隆,已让任务可以在运行时(使用通道发送端)计划调度自己。

Task::poll()函数使用futurescrate的ArcWake工具创建了waker,waker被用于创建一个task::Context,这个task::Context被传递到poll中。

总结

  • 异步Rust操作是惰性的,需要一个调用者来poll它们
  • waker被传递到future中,以连接future和调用它的任务
  • 当一个资源没有准备好完成一个操作时,Poll::Pending将被返回,并记录任务的唤醒器
  • 当资源就绪,任务唤醒器被通知
  • 执行器接收通知并调度任务以执行
  • 任务被再次轮询,这次,资源已经准备好并且任务已经可以推进

记录waker

Rust的异步模型允许一个future在执行时跨任务迁移,考虑下面的代码:

use futures::future::poll_fn;
use std::future::Future;
use std::pin::Pin;

#[tokio::main]
async fn main() {
    let when = Instant::now() + Duration::from_millis(10);
    let mut delay = Some(Delay { when });

    poll_fn(move |cx| {
        let mut delay = delay.take().unwrap();
        let res = Pin::new(&mut delay).poll(cx);
        assert!(res.is_pending());
        tokio::spawn(async move {
            delay.await;
        });

        Poll::Ready(())
    }).await;
}

poll_fn使用闭包创建了一个Future实例,上面的代码片段创建了一个Delay实例,先对它进行了poll,然后将它发送到一个新任务中进行await。在这个例子中,Delay::poll被调用了两次,并且这两次使用了不同的Waker实例。当这件事发生时,你必须保证在最近一次poll调用时传入的waker上调用wake()

当实现一个Future时,假设每一个poll都有可能传入不同的Waker实例是很有必要的,poll函数必须使用后面的Waker实例更新前一次调用时的。

我们之前实现的Delay在每次它被poll时都创建了一个新线程,这很好,但是在频繁轮询的情况下是非常低效的(比如你在哪个future和一些其它future上使用select!,一旦有一个事件发生,所有的都会被轮询)。一个解决办法是,记住你是否已经创建了一个新线程,并且只有当你从没创建线程时才创建一个新的。然而如果你这么做了,你必须确保线程的Waker会在后面的poll调用中被更新,因为如果不这样的话,你将不会在最近的Waker上wake。

我们可以这样修复前一个实现:

struct Delay {
    when: Instant,
    // 如果有一个已创建的线程,这是Some,否则,是None
    waker: Option<Arc<Mutex<Waker>>>
}

impl Delay {
    fn new(when: Instant) -> Delay {
        Delay {
            when, waker: None
        }
    }
}

impl Future for Delay {
    type Output = &'static str;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // 首先,如果这是future第一次被调用,生成计时器线程
        // 如果计时器线程已经运行了,确保存储的waker是当前任务的waker
        if let Some(waker) = &self.waker{
            let mut waker = waker.lock().unwrap();
            // 检测存储的waker是否是当前任务的waker
            // 这是必要的,因为`Delay`future实例可能
            // 在多次调用`poll`之间移动到一个不同的任务上
            // 如果这发生了,给定`Content`中的waker就不同
            // 我们必须更新存储的waker以反应这个改变
            if !waker.will_wake(cx.waker()) {
                *waker = cx.waker().clone();
            }
        } else {
            let when = self.when;
            let waker = Arc::new(Mutex::new(cx.waker().clone()));
            self.waker = Some(waker.clone());

            // 第一次`poll`被调用,生成timer线程
            thread::spawn(move || {
                let now = Instant::now();

                if now < when {
                    thread::sleep(when - now);
                }
                // 时间到达,通过调用waker来通知调用者
                let waker = waker.lock().unwrap();
                waker.wake_by_ref();
            });
        }
        // 一旦waker已经被存储并且timer线程已经开启
        // 就是时候检查delay是否已经完成了,这是通过
        // 检查当前实例来完成的,如果时间已经到达,那么
        // future就完成了,并且`Poll::Ready`要被返回
        if Instant::now() >= self.when {
            println!("Hello World!");
            Poll::Ready("done")
        } else {
            // 延时尚未到达,future还未完成,所以返回`Poll::Pending`
            // 
            // `Future`特质约定,当`Pending`被返回,future能够确保
            // 给定waker会在future可以再次被poll时收到通知
            // 在我们的例子里,通过在这里返回`Pending`,我们
            // 承诺我们会在请求的延时到达时调用给定的,在`Context`
            // 参数中的waker,我们通过上面生成的timer线程来确保。
            // 
            // 如果我们忘了调用waker,任务将永久挂起
            Poll::Pending
        }
    }
}

Notify工具

我们已经展示了用waker手动实现Delayfuture。Waker是异步Rust工作的基础,通常来说,并不需要深入到这个等级。举个例子,在Delay的例子中,我们可以完全通过tokio::sync::Notify工具,以async/await语法来实现它。这个工具提供了一个基本的任务通知机制,它处理了waker的细节,包含确保记录与当前任务匹配的waker。

使用Notify,我们可以使用async/await语法来实现一个delay函数,就像下面这样:

use std::sync::Arc;
use std::thread;
use std::time::Duration;
use tokio::sync::Notify;
use tokio::time::Instant;

async fn delay(dur: Duration) {
    let when = Instant::now() + dur;
    let notify = Arc::new(Notify::new());
    let notify2 = notify.clone();

    thread::spawn(move || {
        let now = Instant::now();

        if now < when {
            thread::sleep(when - now);
        }

        notify2.notify_one();
    });

    notify.notified().await;
}

Select

tokio::select!

tokio::select!宏允许在多个异步计算上等待,并且当单一计算完成时返回。

举个例子:

#[tokio::main]
async fn main() {
    let (tx1, rx1) = tokio::sync::oneshot::channel();
    let (tx2, rx2) = tokio::sync::oneshot::channel();

    tokio::spawn(async {
        tx1.send("msg from sender 1");
    });
    tokio::spawn(async {
        tx2.send("msg from sender 2");
    });

    tokio::select! {
        val = rx1 => {
            println!("rx1 completed first with : {:?}", val);
        }
        val = rx2 => {
            println!("rx2 completed first with : {:?}", val);
        }
    }

}

我们使用了两个oneshot通道,一旦任意一个通道先完成,等待在这两个通道上的select!语句就会将任务的返回值绑定到val上。当tx1tx2中的任意一个完成,其关联的代码块就会被执行。

而没完成的那个分支就会被销毁。在例子中,我们的计算(select!)在每一个通道的oneshot::Receiver上等待,而尚未完成的那个通道的oneshot::Channel就会被销毁。

取消

在异步Rust中,取消操作通过销毁一个future来执行。回顾深入async一节,异步Rust操作使用future实现,而future是惰性的,操作仅当future被轮询时才会执行。如果future被销毁,那么由于所有关联的状态都已经销毁,所以操作无法执行。

不过话说回来,一个异步操作偶尔有可能会生成一个后台任务,或者启动一个运行在后台的其它操作。比如,在上面的示例中,一个任务被生成出来以发回一个消息。通常,一个任务将执行一些计算来生成一个值。

future或其它类型可以实现Drop以清理后台资源,Tokio的oneshot::Receiver实现了Drop以向Sender端发送一个关闭通知。sender端可以接收这个通知并通过销毁正在执行的操作来关闭它。

async fn some_operation() -> String {
    // 在这里计算值
    String::from("hello")
}

#[tokio::main]
async fn main() {
    let (mut tx1, rx1) = tokio::sync::oneshot::channel();
    let (tx2, rx2) = tokio::sync::oneshot::channel();

    tokio::spawn(async {
        // 在操作以及oneshot的`closed()`通知上select
        tokio::select! {
            val = some_operation() => {
                tx1.send(format!("msg from sender 1: {:?}", val));
            }
            _ = tx1.closed() => {
                // `some_operation()`已经被取消,
                // 任务完成,`tx1`被销毁
                println!("tx1 is closed, some operation is cancelled!");
            }
        }
    });
    tokio::spawn(async {
        tx2.send("msg from sender 2");
    });

    tokio::select! {
        val = rx1 => {
            println!("rx1 completed first with : {:?}", val);
        }
        val = rx2 => {
            println!("rx2 completed first with : {:?}", val);
        }
    }

}

Future的实现

为了更好的理解select!如何工作,我们来看一个假的Future实现应该是什么样的,这是一个简单版本,在实践中,select!包含了一些类似随机选择要先行轮询的分支的功能。

use tokio::sync::oneshot;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct MySelect {
    rx1: oneshot::Receiver<&'static str>,
    rx2: oneshot::Receiver<&'static str>,
}

impl Future for MySelect {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        if let Poll::Ready(val) = Pin::new(&mut self.rx1).poll(cx) {
            println!("rx1 completed first with {:?}", val);
            return Poll::Ready(());
        }

        if let Poll::Ready(val) = Pin::new(&mut self.rx2).poll(cx) {
            println!("rx2 completed first with {:?}", val);
            return Poll::Ready(());
        }

        Poll::Pending
    }
}

#[tokio::main]
async fn main() {
    let (tx1, rx1) = oneshot::channel();
    let (tx2, rx2) = oneshot::channel();

    // use tx1 and tx2

    MySelect {
        rx1,
        rx2,
    }.await;
}

MySelectfuture中包含了每一个分支中的future。当MySelect被轮询,第一个分支就被轮询,如果它准备好了,它的值就会被使用,并且MySelect已经完成了。在.await接收到一个future的输出后,这个future就会被销毁,这将导致两个分支的future都将被销毁,因为其中一个分支还没有完成,操作将被有效的取消。

记住前一个部分中我们讲过的:

当future返回Poll::Pending,它必须确保waker在未来的某一个时间点被通知。忘记做这件事将会导致任务永久挂起。

MySelect实现中没有Context参数的显式使用,反之,waker规约通过被传递到内部future中来满足。通过当我们从其它的内部future中接收到Poll::Pending时也返回Poll::Pending,我们的MySelect也符合了waker规约。

select语法

<pattern> = <async expression> => <handler>,

使用select来accept TCP连接的例子

use tokio::net::TcpListener;
use tokio::sync::oneshot;
use std::io;

#[tokio::main]
async fn main() -> io::Result<()> {
    let (tx, rx) = oneshot::channel();

    tokio::spawn(async move {
        tx.send(()).unwrap();
    });

    let mut listener = TcpListener::bind("localhost:3465").await?;

    tokio::select! {
        _ = async {
            loop {
                let (socket, _) = listener.accept().await?;
                tokio::spawn(async move { process(socket) });
            }

            // Help the rust type inferencer out
            Ok::<_, io::Error>(())
        } => {}
        _ = rx => {
            println!("terminating accept loop");
        }
    }

    Ok(())
}

accept循环一直运行,直到一个错误出现或rx接收到一个值。_模式意味着我们不关心异步计算的返回值。

返回值

tokio::select!宏返回<handler>表达式的执行结果。

async fn computation1() -> String {
    // .. computation
}

async fn computation2() -> String {
    // .. computation
}

#[tokio::main]
async fn main() {
    let out = tokio::select! {
        res1 = computation1() => res1,
        res2 = computation2() => res2,
    };

    println!("Got = {}", out);
}

因为这一点,每一个分支的<handler>表达式必须返回相同类型,如果不需要select!表达式的输出,让表达式返回()是一个很好的实践。

错误

使用?操作符会将表达式中的错误传播出去,具体如何工作取决于?是在异步表达式中使用还是在handler中使用。在异步表达式中使用?将会把错误传播到异步表达式外,这将导致异步表达式的输出是一个Result,在handler中使用?将立即将错误传递出select!表达式,让我们再来看看那个accept循环示例:

use tokio::net::TcpListener;
use tokio::sync::oneshot;
use std::io;

#[tokio::main]
async fn main() -> io::Result<()> {
    // [setup `rx` oneshot channel]

    let listener = TcpListener::bind("localhost:3465").await?;

    tokio::select! {
        res = async {
            loop {
                let (socket, _) = listener.accept().await?;
                tokio::spawn(async move { process(socket) });
            }

            // Help the rust type inferencer out
            Ok::<_, io::Error>(())
        } => {
            res?;
        }
        _ = rx => {
            println!("terminating accept loop");
        }
    }

    Ok(())
}

注意,listener.accept().await??操作符将错误从这个表达式中传播出去,传到resbinding上。在错误发生时,res将被设置成Err(_),然后再handler中,?操作符又被使用到。res?语句将错误传播出main函数。

模式匹配

任何的Rust模式都可以用在<pattern>上,比如我们正从一个MPSC通道上接收消息,我们可能向下面这样做:

tokio::select! {
    Some(v) = rx1.recv() => {
        println!("Got {:?} from rx1", v);
    }
    Some(v) = rx2.recv() => {
        println!("Got {:?} from rx2", v);
    }
    else => {
        println!("Both channels closed");
    }
}

在这个例子中,select!表达式等待从rx1rx2中接收值。如果通道关闭,recv()返回None,这不会匹配到模式,并且分支会被禁用。select!表达式将继续在其余分支上等待。

注意,select!表达式包含else分支。select!表达式必须返回一个值,如果使用模式匹配,有可能并没有关联的分支匹配到,如果这发生了,else分支将被返回。

借用

当产生一个任务时,被产生的异步表达式必须拥有它所有的数据。select!宏则没有这个限制,每一个分支的异步表达式可以借用数据并且并发操作。根据Rust的借用规则,多个async表达式可以不可变的借用同一部分数据或者一个异步表达式可以可变的借用一部分数据。

举个例子,我们模拟向两个不同的TCP目的地发送相同的数据:

use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use std::io;
use std::net::SocketAddr;

async fn race(
    data: &[u8],
    addr1: SocketAddr,
    addr2: SocketAddr
) -> io::Result<()> {
    tokio::select! {
        Ok(_) = async {
            let mut socket = TcpStream::connect(addr1).await?;
            socket.write_all(data).await?;
            Ok::<_, io::Error>(())
        } => {}
        Ok(_) = async {
            let mut socket = TcpStream::connect(addr2).await?;
            socket.write_all(data).await?;
            Ok::<_, io::Error>(())
        } => {}
        else => {}
    };

    Ok(())
}

data变量被两个异步表达式不可变的借用,当其中一个操作成功完成,另一个就被销毁。因为我们在Ok(_)上模式匹配,如果一个表达式失败,另一个将会继续执行。

当到达了每一个分支的<handler>select!保证只有一个<handler>会被执行,因为这一点,每一个<handler>可以可变的借用相同数据。

比如在每一个handler中修改out

use tokio::sync::oneshot;

#[tokio::main]
async fn main() {
    let (tx1, rx1) = oneshot::channel();
    let (tx2, rx2) = oneshot::channel();

    let mut out = String::new();

    tokio::spawn(async move {
        // Send values on `tx1` and `tx2`.
    });

    tokio::select! {
        _ = rx1 => {
            out.push_str("rx1 completed");
        }
        _ = rx2 => {
            out.push_str("rx2 completed");
        }
    }

    println!("{}", out);
}

循环

loop {
    let msg = tokio::select! {
        Some(msg) = rx1.recv() => msg,
        Some(msg) = rx2.recv() => msg,
        Some(msg) = rx3.recv() => msg,
        else => { break }
    };

    println!("Got {:?}", msg);
}

select!被求值,如果此时多个通道具有待处理消息,只有一个通道(随机的)的值将被弹出,所有其它通道都将保持不受影响,它们的消息保存在同导致直到下一个loop迭代,不会产生消息丢失。

如果select!不是随机的选择先进行检查的分支,那么在每一次循环迭代中,rx1都将被先检查。如果rx1总是含有新消息,剩下的通道将永远不会被检查。

继续一个异步操作

#[tokio::main]
async fn main() {
    let (mut tx, mut rx) = tokio::sync::mpsc::channel(128);    
    
    let operation = action();
    tokio::pin!(operation);
    
    loop {
        tokio::select! {
            // 恢复异步操作
            _ = &mut operation => break,
            Some(v) = rx.recv() => {
                if v % 2 == 0 {
                    break;
                }
            }
        }
    }
}

select!循环中,我们没有直接传入operation,我们使用了&mut operationoperation变量正在跟踪执行中的异步操作,每一个循环迭代使用相同的操作而非提交一个新的action()调用。

这是我们第一次使用tokio::pin!,我们还不会深入它的细节,你只需要注意,当你.await一个引用,其值必须被固定或实现Unpin

修改分支

看一个稍有点复杂的循环,我们有:

  1. 一个i32值的通道
  2. 一个在i32值上执行的异步操作

我们想要实现的逻辑是:

  1. 等待通道上的一个偶数
  2. 开启异步操作,使用这个偶数作为输入
  3. 等待操作,同时监听通道上更多的偶数
  4. 如果一个新的偶数在现有的操作完成前被接受到,中断现有的操作并且使用一个新的偶数开启它
async fn action(input: Option<i32>) -> Option<String> {
    let i = match input {
        Some(input) => input,
        None => return None
    };

    // 这里是异步逻辑
    Some(i.to_string())
}

#[tokio::main]
async fn main() {
    let (tx, mut rx) = tokio::sync::mpsc::channel(128);

    let operation = action(None);
    tokio::pin!(operation);

    tokio::spawn(async move {
        let _ = tx.send(1).await;
        let _ = tx.send(2).await;
        let _ = tx.send(3).await;
    });

    let mut done = false;
    loop {
        tokio::select! {
            res = &mut operation, if !done => {
                done = true;
                if let Some(v) = res {
                    println!("Got = {}", v);
                    return;
                }
            }
            Some(num) = rx.recv() => {
                if num % 2 == 0 {
                    operation.set(action(Some(num)));
                    done = false;
                }
            }
        }
    }
}

注意action如何接受一个Option<i32>作为参数。在我们接收第一个偶数之前,我们需要将operation实例化,我们让action接收Option返回Option,如果None被传入,那么返回值也是None。第一个循环迭代,operation携带None立即完成。

这个示例使用了一些新语法。第一个分支包含, if !done,这是一个分支前提条件,在我们解释它如何工作之前,让我们看看如果省略了它会发生什么,我们拿出, if !done,运行示例,将导致如下输出:

thread 'main' panicked at '`async fn` resumed after completion', src/main.rs:1:55
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

这个错误在你尝试在operation完成后使用它时发生。通常,当使用.await时,被await的值就会被消费,在这个例子中,我们在一个引用上await,这意味着operation在它完成后依然存在。

为了避免这个panic,我们必须在operation完成后小心的禁用这一分支,done变量用于跟踪operation是否完成。一个select!分支可能包含一个前提条件,这个前提条件会在select!在该分支值上await前被检查,如果条件为false,那么分支就会被禁用。done变量被初始化成false,当operation完成,done被设置成true,下一个循环迭代中将会禁用operation分支,当一个偶数消息从通道中被接收到,operation将会被重置,done将被设置成false

译者:

  • operation分支中的, if !done和其handler中的done = true可以保证同一个operation只被尝试一次
  • 通道分支中的operation.setdone=false共同完成了两件事
    1. operation换成新的
    2. 开启done开关,让新的operation不被禁用

译者:当前的实现只能从通道中接收一个值,然后就会从main中返回,试试让它接收所有通道中的偶数,直到select!中的这两个分支都被禁用。

你可能会遇到些麻烦,但是仔细看看错误中的提示

posted @ 2022-12-22 17:59  yudoge  阅读(794)  评论(0编辑  收藏  举报