Rust 异步中的 Waker

Rust语言圣经中关于 Waker 的描述是很不完整的,导致新手不可能从中看懂 Waker 在实际应用中真正的工作方式。本文旨在对这部分内容做一个补充,帮助新手了解 "Rust 异步生态是如何基于 Rust 编译器提供的能力建立的" 。

如果你读了圣经,还不清楚 Rust 中 async/.await 的工作原理,不知道编译器在其中做了什么,不知道执行器是用来 poll Future 的,可以去读读Rust 异步编程基础这篇。但这篇中没有提到 Waker 的作用。

还有我特别推荐一个开源仓库hexagonal-sun/trale: Tiny Rust Async Linux Executor,是一个难得的兼具简单易懂和符合实际情况两种特性的 Rust 异步执行器的实现。你对 Rust 异步底层有什么问题,都可以去看看,不费太大功夫,很可能能解答你的问题!

Waker 基础概念

因为 Rust 异步中和 Waker 相关相似的概念比较多,容易混淆,所以先罗列梳理一下

有一个 Trait 叫 Wake,它有一个方法叫 wake

rust 复制代码
impl Wake for TaskId {
    fn wake(self: Arc<TaskId>) {
        EXEC.with(|exec| {
            let mut exec = exec.borrow_mut();
            if let Some(task) = exec.waiting.try_remove(self.0.load(Ordering::Relaxed)) {
                exec.run_q.push(task);
            }
        });
    }
}

有一个结构体叫 Waker,Waker 有一个 from 方法,可以利用 Impl Wake 的对象生成 Waker。Waker 还有一个 wake 方法,wake 方法内部其实在调用用于生成 Waker 的 Impl Wake 对象的 wake 方法。

rust 复制代码
let waker = Waker::from(task.id.clone());

waker.wake();

有一个结构体叫 Context,Context 有一个 from_waker 方法,从 Waker 的引用中生成 Context。Context 有一个 waker() 方法,用于获取用于生成 Context 的 Waker 的引用。

rust 复制代码
let waker = Waker::from(task.id.clone());

let mut cx = Context::from_waker(&waker);

cx.waker();

是不是真挺绕的。

异步库、执行器和 Waker

回顾一下关于 async/.await、Future 和 执行器的知识。Future 是标准库定义的一个 trait,有一个 poll 方法。async 指定的代码块会被编译器编译成一个 Future,代码块执行的逻辑被编译器处理后(形成一个状态机)放到 poll 方法里。.await 只能在 async 中使用,通过 .await 调用其他 async 块或 Future,编译器把 .await 转化为对 Future 的 poll,所以编译器生成的 Future poll 中,外层 Future 的 poll 内部会 poll 内层的 Future,直至最内一层的 Future。最外层的 async 或者 Future 会被放入执行器,执行器负责不断 poll Future,直至其返回完成的状态

重点: poll 方法有一个参数 Context,执行器在 poll 最外层 Future 时会传入一个 Context。这个 Context 是编写执行器代码的人通过上面的 from_waker 之类的方法构造的 。最外层的 Future 的 poll 方法内部有编译器生成的 poll 内层 Future 的代码,poll 内层 Future 时,会把这个 Context 传下去,传到最底层的 Future。最底层的 Future 是编写执行器的人手工实现的(不是编译器通过 async/.await 生成的),其 poll 方法内部会利用 Context 找到 Waker。这个最底层 Future 一般用于调用阻塞或者异步的系统 IO,比如 send/recv,如果 IO 不能立即响应,此 Future 会让执行器等待 IO 响应(通过 OS 提供的功能比如 epoll,io_uring),并将 Waker 注册给执行器,再向外层返回 Pending,通过返回值,执行器得知 Future 没有完成 。执行器在没有 Future 可 poll 的时候就等待 OS(epoll, io_uring)的反馈,得到阻塞或异步的 IO 可以继续推进的响应后,就找到此 IO 对应的 Waker(由刚刚的底层 Future 注册),调用 Waker.wake() 将外层 Future 重新放回执行器的待 poll 队列中。**这里的 Waker.wake 也是编写执行器的人负责实现的。**到此,执行器才能异步地调度上层用户随意实现的 Future。
引用自 hexagonal-sun/trale: Tiny Rust Async Linux Executor

圣经只告诉我们 Waker.wake() 告知 执行器 Future 可以继续执行,没有告诉我们这是如何做到的。实际上,只有底层 Future,Waker,Executor 三者利用 OS 协同工作,才能做到上文中的:1. 底层 Future 告知 Executor 等待特定事件,并将 Waker 与事件绑定注册给 Executor;2. Executor 知道从哪里等待 IO 事件的更新,从哪里获取 Future 注册的 Waker;3. Waker.wake() 能将 Future 重新交给 Executor。之所以他们能完成这么复杂的协同,是因为底层 Future,Waker,Executor都是同一伙人编写的,(目前为止)并非协商好特定接口就能做到的。

这就引出一个额外的问题,我们使用的上层异步库,比如LinkBond in rtnetlink - Rust,这种顶层 async 或 Future 也是编 Executor 那帮人实现的吗?并非如此,第三方的人也可以自行实现上层异步库,不过你必须要选择一个 Executor 作为你的执行器,并用该 Executor 提供的底层异步 IO Future。反过来,如果你用了一个 Executor 的底层异步 IO Future,你就不能用其他 Executor 作为你的执行器了!

有一篇博客 Portable and interoperable async Rust证实了上述理论:

讨论

在最初使用 Rust 异步库时,我以为随意挑选一个执行器即可,但想不明白在这种情况下 Waker 是怎么工作的,异步库实现的 Future 怎么知道如何将 Waker 注册给执行器,在它甚至不知道用哪个执行器的情况下?在我了解到实际情况后,感叹道,原来 Rust 中一个执行器就代表一个异步生态,而执行器就是异步生态的核心。编写执行器需要负责的内容要比想象的多。 当时觉得是不是这种上层库和执行器的绑定太不灵活了,怪不得Portable and interoperable async Rust这篇博客呼吁搞一个统一的执行器让上下层分离。但转念一想,比如 C++ 的 Boost.asio 也是一个异步库,干脆同时实现了上层接口和底层执行器,根本没想把执行器这个概念单拎出来,所以 Rust 虽然可能做的还不够,但做的也不少了。

相关推荐
天意生信云1 小时前
生信分析自学攻略 | R软件和Rstudio的安装
经验分享·r语言
m0_4805026414 小时前
Rust 入门 KV存储HashMap (十七)
java·开发语言·rust
大阳12314 小时前
线程(基本概念和相关命令)
开发语言·数据结构·经验分享·算法·线程·学习经验
左直拳16 小时前
前端vue3+后端spring boot导出数据
超时·多线程·异步·连接超时·数据导出
Include everything17 小时前
Rust学习笔记(三)|所有权机制 Ownership
笔记·学习·rust
码码哈哈爱分享19 小时前
Tauri 框架介绍
css·rust·vue·html
yaya_1921 天前
想要PDF翻译保留格式?用对工具是关键
经验分享
草莓熊Lotso1 天前
《详解 C++ Date 类的设计与实现:从运算符重载到功能测试》
开发语言·c++·经验分享·笔记·其他
寻月隐君2 天前
硬核实战:从零到一,用 Rust 和 Axum 构建高性能聊天服务后端
后端·rust·github