< 返回版块

LongRiver 发表于 2023-10-21 18:55

我最近打算学习下rust的async runtime。我先是看了些对一些概念,比如Future,之类的介绍,然后就是一些runtime的示例代码。然后我打算自己写一个简单的runtime,加深下印象。

我平时用C语言,也用epoll封装过事件驱动的lib,所以打算先用C语言的epoll写个简单的echo服务。然后移植到rust的mio库上。再把程序中的 运行时 和 业务逻辑 区分开,尽量抽象出Future的概念,类似runtime的原型。然后再慢慢向标准的runtime靠拢。

为了简单,我开始是定义了自己的Future定义:

enum MiniPoll {
    Pending,
    Ready,
}
trait MiniFuture {
    fn poll(&mut self) -> MiniPoll;
}

跟标准库里的Future对比,有3个区别:1.没有用Pin;2.没有Context(暂时全部用全局static变量);3.没有返回值Output。

前2个区别只是为了简化实现,影响不大。问题出在第3个区别上,也就是Output。最开始简单的echo服务程序里,两个Future(一个是accept,一个是连接上执行echo功能)都没有返回值,所以无所谓。简单的runtime原型也跑起来了。现在是想给Future加上返回值。问题就出在这里。

现在没有Output,所以程序在维护一个task列表的时候,是这么定义的:

Slab<Box<dyn MiniFuture>>

Vec<Box<dyn MiniFuture>>

如果要加上trait关联类型Output,变成 MiniFuture<Output=T>,那么上述的两个列表就不能这么定义了。就是不能把不同类型返回值的Future放到一个Slab或者Vec中!!!但是runtime总是要管理全部的Future的。到这里,我就不知道该怎么办了。

====

我看了一篇对 async-task 的源码介绍的文章。里面有句话:“提供了 JoinHandle,这样spawn函数对Future没有 Output=()的限制,极大方便用户使用”。是不是说 Output类型,本来就是个难缠的问题?

另外,看了这篇文章里贴的Task的定义的源码,发现都是unsafe代码,手动定义vtable,手动用Output覆盖future内存。感觉又回到了C语言的风格,直接操作内存。

====

我的问题:

  1. 如果要管理带不同关联类型Output的Future,要怎么做?是不是一定要unsafe才行?

  2. rust的async runtime跟C里的libev这种库,难度不是一个层次的?要难得多。另外,对于普通程序员即便不了解runtime,也并不影响对async/await的使用?

====

这个介绍runtime的文章里,也有一个非常小的例子。这个例子里task的定义里,Future的Output也是写死了是 (),而不是泛型T

struct Task {
    future: Pin<Box<dyn Future<Output = ()> + 'static>>,
    waker: Waker,
}

pub struct BasicExecutor {
    tasks: Vec<Task>,
}

这个就是我上面遇到的问题:如何定义全部(不同Output返回类型的Future)的tasks列表: Box<dyn Future<Output=T>> ?

评论区

写评论
作者 LongRiver 2023-10-22 10:41

好的。我改下思路。先去学习下smol的代码。据说很短,比较容易学习。

aj3n 2023-10-21 22:10

首先,重新定义Future是没有任何意义的,劝你尽早停止浪费你的时间; 其次,我也说过了,task和future是两种东西,Future有output但是task没有,一开始混为一谈就是错,你根本在和不存在的问题搏斗;而且这从来也是一个很细微的问题,并不存在什么原则性的困难😓,教程为了只是为了简化问题才限制Output=(),而async-std是一个生产级的开源库,它不需要回避这种问题; 贴上我之前实现的玩具executor的Task定义,省略实现仅供参考;

trait Task {
	fn poll(self: Pin<&mut Self>) -> Poll<()>;
	fn canceled(self: Pin<&Self>) -> bool;
}
#[pin_project::pin_project]
struct TaskImpl<F: Future> {
	ctx: Rc<JoinHandleCtx<F::Output>>,
	waker: Arc<waker::Waker>,
	#[pin]
	fut: F,
}
impl<T> Future for JoinHandle<T> {
	type Output = T;
        ...
}
pub fn spawn_local<F>(fut: F) -> JoinHandle<F::Output>
where
	F: Future + 'static,
	F::Output: 'static,{...}

impl<F: Future> Task for TaskImpl<F> {...}
type TaskWrapper = Pin<Box<dyn Task + 'static>>;
struct Context {
        // XXX: 这就是你要的Tasks的定义;
	tasks: RefCell<Slab<Option<TaskWrapper>>>,
	waker: Arc<CachedWaker>,
}

作者 LongRiver 2023-10-21 20:59

有没有推荐的文章?

直接看代码太痛苦了。本来想的是能自己先写一个简单的runtime(不需要多线程,也最好不用unsafe),有助于后续阅读现有的库。现在发现连最基本的流程都跑不通。

--
👇
Ryan-Git: 建议先了解现有库的实现逻辑,最好和历史。 基本上,要 work stealing 还要性能比较好的话,unsafe + vtable 是目前唯一的选择。

作者 LongRiver 2023-10-21 20:57

重新定义Future的目的是:我想从最简单的定义开始,按需去加特性。后续按需加上Pin,Context等,慢慢向标准库靠拢。这样可以更好的理解每个特征的真正需求。

当然这是我自认为的。不一定真的对理解有帮助。

--
👇
aj3n: 1. 如果能作出适当取舍,unsafe不是必须的;Task和Future不是对等的,你不应该这么定义Task集合;我也不太明白你重新定义Future的意义,你如果只是想重新实现runtime,只需要设计你的Task结构就好了; 2. 不好评价难度,只针对后面的提问,写rust异步代码完全可以先上车后补票,不了解原理并不会影响使用(大部分情况?),但是肯定会有一些细微的坑🤦

Ryan-Git 2023-10-21 20:52

建议先了解现有库的实现逻辑,最好和历史。 基本上,要 work stealing 还要性能比较好的话,unsafe + vtable 是目前唯一的选择。

aj3n 2023-10-21 20:22
  1. 如果能作出适当取舍,unsafe不是必须的;Task和Future不是对等的,你不应该这么定义Task集合;我也不太明白你重新定义Future的意义,你如果只是想重新实现runtime,只需要设计你的Task结构就好了;
  2. 不好评价难度,只针对后面的提问,写rust异步代码完全可以先上车后补票,不了解原理并不会影响使用(大部分情况?),但是肯定会有一些细微的坑🤦
1 共 6 条评论, 1 页