Stejpang 是 Rust 异步生态的重要贡献者。2020.3 他写了一篇文章:Why I'm building a new async runtime 。最近搜索的时候,发现已经被删除了。Stejpang 在异步领域,参与或者主导了 crossbeam、rayon、async-std、Tokio 和 smol 。是不是蛮惊叹的,他几乎参与了 Rust 所有核心的异步框架,然后自己还要实现一个独特的 smol

我之前写过一篇:并行迭代器 Rayon 。里面有对 Stejpang 的介绍

这篇对 Stejpang 的采访文章:Async Interview 。他这样表达:

It feels like this is finally it - it’s the big leap I was longing for the whole time! As a writer of async runtimes, I’m excited because this runtime may finally make my job obsolete and allow me to move onto whatever comes next.

也就是说,Stejpang 终于对 smol 满意了。那就让我们看看 smol 到底解决了什么问题。

在上面的文章里,这么介绍:

smol is an async runtime, similar to tokio or async-std, but with a distinctly different philosophy. It aims to be much simpler and smaller. Whereas async-std offers a kind of “mirror” of the libstd API surface, but made asynchronous, smol tries to get asynchrony by wrapping and adapting synchronous components. There are two main ways to do this:
  • One option is to delegate to a thread-pool. As we’ll see, stjepang argues that this option is can be much more efficient than people realize, and that it makes sense for things like accesses to the local file system. smol offers the blocking! macro as well as adapters like the reader function, which converts impl Read values into impl AsyncRead values.
  • The other option is to use the Async wrapper to convert blocking I/O sockets into non-blocking ones. This works for any I/O type T that is compatible with epoll (or its equivalent; on Mac, smol uses kqueue, and on Windows, smol uses wepoll).

smol 不同于 tokio 和 async-std,它更简单和小巧。它致力于通过包装和适配同步组件,将其异步化。有两个主要特征,采用线程池和 Async 装饰器。

在 async-std 里面,有一篇文章 The new async-std runtime。吸取了 Go 的经验,根据任务执行的时间,自动化的启用新线程来接管这个任务。这个策略,可以不用区分异步还是 blocking 调用

Stejpang 认为这样做也有一些问题。频繁的启动新线程会造成程序运营的不平滑,产生锯齿。所以 Smol 还是显式的采用 blocking! 来处理阻塞任务,让操作系统来调度任务

在对同步任务的异步包装上,smol 有一个独特的做法。通过 Async<T> 的写法,为 T 类型的操作提供了 read_with 和 write_with 的函数。这个异步实现的机制还是具有 smol 的创新的。当用 Async<T> 包装了一个读写操作,这个返回的对象就具有了 read_with 和 write_with 函数,如下:

let listener = Async::<TcpListener>::bind("127.0.0.1:0")?;
// Accept a new client asynchronously.
let (stream, addr) = listener.read_with(|l| l.accept()).await?;

在 read_with 里面,先使用 l.accept 调用。然后 kernel 发现 accept 是一个阻塞调用,返回 io:ErrorKind::WouldBlock。read_with 会去注册一个文件描述符(通过 AsRawFd ),然后 yield(挂起) 当前任务。smol 的异步 reactor 会调用 epoll 等待这个文件描述符完成就绪,再重新启动这个任务。这个做法和 Future 的做法很类似。不同点是 poll 文件操作符的时候,smol 返回 WouldBlock ,而 Future 返回 Poll:Pending

7efc3aa88915fa3205b77284eaf34ef4.png
rust future 机制

上面这个图是关于 Rust Future 机制的一个描述。async/.await 把代码转换到一个“状态机”,包装起来的代码只有在 spawned 或者 polled 的时候才会开始执行。在通过 spawn 或者 poll 执行程序之后,如果发现 poll 是 Pending 状态。设置一个 wake 然后任务挂起。当任务执行完成,wake 返回,然后继续后续程序执行

这个细节可以看我上一篇文章:深入了解 Rust 异步开发模式。这个是基于 Tokio 分析的,我们来看看 smol 是怎么做的。

#[derive(Debug)]
pub struct Async<T> {
    /// A source registered in the reactor.
    source: Arc<Source>,

    /// The inner I/O handle.
    io: Option<T>,
}

Async 是一个 Struct,source 是一个管理 waker 的 impl 。Async 针对 T 做了包装之后,执行了如下操作:

  1. 获取 io 的文件操作符(as_raw_fd)
  2. 设置文件操作符为非阻塞状态
  3. 构建Async 返回

其中第二步代码如下:

let mut res = libc::fcntl(fd, libc::F_GETFL);
if res != -1 {
  res = libc::fcntl(fd, libc::F_SETFL, res | libc::O_NONBLOCK);
}

在调用 Async 的 read_with 的时候,先判断返回是不是 WouldBlock

pub async fn read_with<R>(&self, op: impl FnMut(&T) -> io::Result<R>) -> io::Result<R> {
  let mut op = op;
  loop {
    match op(self.get_ref()) {
      Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
      res => return res,
    }
    optimistic(self.readable()).await?;
  }
}

optimistic 会 poll 一次,如果 poll 成功,就继续执行。get_ref 调用 T 的 as_ref

pub fn get_ref(&self) -> &T {
  self.io.as_ref().unwrap()
}

在对 T 类型执行 as_ref 的时候,获得对 T 类型的引用。WouldBlock 的返回代表,需要引用的对象这个操作需要阻塞才能完成。文档如下:

WouldBlock : The operation needs to block to complete, but the blocking operation was requested to not occur.

smol 还包含很多内容,这里简单总结一下这篇的内容:

smol 相对其他异步框架,更轻量级、采用线程池、采用 Async 来包装阻塞操作。也显式提供 blocking! 宏启动线程执行阻塞任务

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐