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 虽然可能做的还不够,但做的也不少了。

相关推荐
C雨后彩虹9 小时前
CAS与其他并发方案的对比及面试常见问题
java·面试·cas·同步·异步·
sunguang20189 小时前
“懂不懂管理,一看便知”:做管理就是3件事,抓大、放小、管细做管理,其实就是要做好三件事:抓大、放小、管细。
经验分享·职场和发展
哲伦贼稳妥9 小时前
职场发展-遇到以下情况请直接准备后手吧
运维·经验分享·其他·职场和发展
西瓜程序猿10 小时前
传统礼簿收礼小工具:记了么,解决纸质收礼记账痛点
经验分享·测试工具·程序人生·全文检索·交友
孞㐑¥13 小时前
算法—队列+宽搜(bfs)+堆
开发语言·c++·经验分享·笔记·算法
星夜泊客15 小时前
C# 基础:为什么类可以在静态方法中创建自己的实例?
开发语言·经验分享·笔记·unity·c#·游戏引擎
Hello.Reader20 小时前
Rocket 0.5 响应体系Responder、流式输出、WebSocket 与 uri! 类型安全 URI
websocket·网络协议·安全·rust·rocket
June bug20 小时前
【PMP】风险管理
经验分享·职场和发展·学习方法
易知微EasyV数据可视化21 小时前
数字孪生+AI:头部能源企业-监测光伏产品生命周期,驱动绿色智造零碳未来
人工智能·经验分享·能源·数字孪生
GEO科技21 小时前
氧气科技在AIIA签署《人工智能安全承诺:生成式引擎优化GEO 》
经验分享