C++中的锈式期货

2020-08-28 22:04:36

所有联网应用程序基本上归结为以正确的方式将多个异步调用串在一起。传统上,对于用C编写的程序,这将通过注册回调来完成,其中被调用者要么自己处理事件以通过状态机进行分派。然而,在这样的实现中,关于内存安全的推理可能是危险的,有时它需要全程序知识。未来,或者也被称为承诺,在这方面通过允许异步程序以直接风格编写,保持控制流线性,从而简化了这一点。

综上所述,我确实认为期货在合适的情况下可以很好地适合C语言编程,我也希望这篇文章能帮助人们理解锈蚀期货,作为一个单独的参考,只触及基本面。

Rust Futures的故事特别有趣,因为它从根本上不同于FutureFunctional Language或JavaScript语言的通常工作方式。其他实现是基于推送的-这意味着您给出一个函数要推送到具有未来解析结果的地方-Rust期货是基于轮询的。让我们看看这在C语言中是什么样子,我们将自己限制在一个单一的任务,即一个运行在一个线程上的顶级未来。这在嵌入式编程中很常见,而且在没有安全性的情况下仍然相当容易管理。这在嵌入式编程中很常见,而且在没有安全性的情况下仍然相当容易管理。这在C语言中是很常见的,也就是说,在没有安全性的情况下,它仍然相当容易管理。Libuv用于事件循环。不需要分配堆-从这里开始都是下坡路(明白了吗?因为堆栈向下增长。)-除了由libuv接口强加的那些。

枚举轮询{POLL_PENDING,POLL_READY};struct Future{enum Poll(*poll)(struct Future*self,struct context*ctx);//暂时让我们跳过此方法//void(*drop)(struct Future*self,struct context*ctx);};

举个例子,让我们考虑最简单的情况:一个直接用数字4来解析的未来,

枚举Poll simpleFuturePoll(struct Future*self,struct context*ctx){struct SimpleFuture*state=(struct SimpleFuture*)self;self->;result=4;return Poll_Ready;}struct SimpleFuture{struct Future;int result;}simpleFuture={。未来={.。Poll=simpleFuturePoll,}};//...。在事件循环simpleFuture中。Poll(&;simpleFuture,ctx);//=>;POLL_READY//现在我们可以在这里使用结果simpleFuture。结果//=>;4。

为了尝试解决未来,我们轮询它;它返回POLL_READY,因此我们完成了。对于在轮询时返回POLL_PENDING的期货,我们只需确保稍后再次轮询它们-期货是懒惰的,除非积极地被告知这样做,否则不会取得进展。没有人比未来本身更清楚它何时应该被再次轮询-唤醒-因此,赋予所有期货的上下文允许他们唤醒自己的任务。对于许多并行任务,额外的复杂性在这里会很明显,但在我们的情况下,如下所示。

Struct context{struct Future*mainFuture;uv_loop_t loop;};void wakeTask(struct context*ctx){if(ctx->;mainFuture->;polt(ctx->;mainFuture,ctx)==poll_ady){exit(Exit_Uccess);//完成!}}

枚举TimerStatus{TIMER_NOT_STARTED,TIMER_WAITING,TIMER_FINISHED};struct TimerFuture{struct Future;enum TimerStatus status;Union{uint64_t Timer_t*Handle;};};static void uvCloseFree(UV_Handle_t*Handle){free(Handle);}static void timerCb(UV_Timer_t*Handle){struct TimerFuture*state=Handle->;data;struct。Data;uv_close((uv_Handle_t*)Handle,uvCloseFree);state->;status=Timer_Finish;wakeTask(CTX);}静态枚举轮询计时器FuturePoll(struct Future*self,struct context*ctx){struct TimerFuture*state=(struct TimerFuture*)self;Switch(state->;status){case Timer_not_start:uint64_t timeout=state->。HANDLE=malloc(sizeof*state->;Handle);UV_TIMER_INIT(CTX.。循环,&;状态->;句柄);STATE-&>;HANDLE->;DATA=STATE;UV_TIMER_START(&;STATE-&>;HANDLE,timerCb,TIMEOUT,/*无重复*/0);STATE->;STATUS=TIMER_WAITING;/*FLOTHROUG*/CASE TIMER_WAITING:RETURN POLL_PENDING;CASE TIMER_FINISHED:RETURN POLL_READY。}struct TimerFuture timerFutureNew(uint64_t超时){return(Struct TimerFuture){。未来={.。Poll=timerFuturePoll,},。状态=TIMER_NOT_STARTED,。超时=超时,};}。

定时器句柄用于保存对其用户数据字段中的未来的引用,从而回调知道切换状态的是哪个未来。然而,这需要将未来的对象固定在存储器中,移动它将使引用悬挂。使用Pin构造来处理这种不安全性,该Pin构造包装指针类型P,并且只允许不能移动被指针对象的操作(对于这样做可能不总是安全的情况,例如,P:!Unpin),并确保其存储器保持有效,直到它被丢弃为止,但是使用Pin构造,包装指针类型P,并且只允许不能移动被指针对象的操作(对于这样做可能不总是安全的情况,例如,P:!Unpin),并且确保其存储器保持有效,直到它被删除,最接近你的是文档中埋着一个红色的段落,这意味着小心行事,只为主要的未来分配一次存储空间,永远不要复制它,并且只引用带有指向其在内存中静态位置的指针的未来。

注意,通过认识到每当主未来被唤醒时:(I)必须具有最小超时的定时器被触发;或者(Ii)所有定时器需要丢弃并重置,因为期货形成树,因此可以仅使用一个全局UV_Timer_t,正如我们将看到的那样。

顺序运行多个期货就是构建一个新的未来,它轮询每个未来,直到一个接一个地完成。外部未来的Poll方法必须在每个中间步骤之后返回POLL_PENDING,然后继续它停止的地方-就像协奏一样。Rust将每个未来变成一个状态机,在C语言中做同样的事情意味着扮演Rust编译器的角色。正如Simon Tatham所描述的那样,对Duff的设备的改编可以帮助减少沸腾的模板。想法是用Rust将每个未来变成一个状态机,这意味着扮演Rust编译器的角色。正如Simon Tatham所描述的那样,对Duff的设备进行改编可以帮助减少沸腾的模板。想法是使用。您可以使用__line__宏创建唯一标签,其中执行将在重新进入时开始,并按此方式设置开关表达式,然后返回。

Tyfinf unsign协程;#定义COR_START(S)开关(*(S)){案例0:;#定义COR_YIELD(s,r)do{*(S)=__line__;return(R);case__line__:;}WHILE(0)#定义COR_END}。

其中s是指向协程状态的指针。必须非常小心,因为当返回时,所有本地变量都是无效的-只要有一种语言可以静态地检查这样的错误。

为了说明这一点,这里是这样一个未来,它以标准输出打印四次,第一次以一秒的间隔打印三次,然后在两秒后再次打印:

Struct TestFuture{struct Future Future;协程c;Union{struct{int i;struct TimerFuture Timera;};struct TimerFuture timerB;};};struct TestFuture testFutureNew(){return(Struct TestFuture){。未来={.。Poll=testFuturePoll,},。C=0,};}

枚举轮询testFuturePoll(struct Future*self,struct context*ctx){struct TestFuture*state=(struct TestFuture*)self;COR_START(&;STATE->;c)for(STATE->I=0;STATE->;3;++STATE->;I){STATE-&>;Timera=timerFutureNew(1000);等待(&;将来);printf(";一秒已过!";);}state->;timerB=timerFutureNew(2000);等待(&;state->;c,ctx,&;state->;timerB。将来);printf(";又过了两秒!";);COR_END RETURN POLL_READY;}。

枚举轮询testFuturePoll(struct Future*self,struct context*ctx){struct TestFuture*state=(struct TestFuture*)self;Switch(state->;c){case 0:;for(state-gt;i=0;state-gt;i<;3;++state->;i){state->;Timera=timerFutureNew(1000);While(state->;i=0;state->;3;++state->;i){state->;Timera=timerFutureNew(1000);While(state->。未来。投票(&;州->;时间。未来,ctx)==POLL_PENDING){STATE-&>;c=1;RETURN POLL_PENDING;CASE 1:;}printf(";一秒已过!";);}STATE-&gT;timerB=timerFutureNew(2000);While(STATE-&>;timerB。未来。轮询(&;州->;计时器B。未来,ctx)==POLL_PENDING){STATE-&>;c=2;RETURN POLL_PENDING;CASE 2:;}printf(";又过了两秒!";);}RETURN POLL_READY;}。

请注意,必须将局部i溢出到未来结构,才能跨屈服点持久存在,并且使用联合来显示在每一步中哪些变量是活动的,并挤出最后的性能点滴,即使面对来自各个方向的不确定的行为威胁也是如此。

类似地,使用未来组合器可以使多个期货并行运行,该组合器的轮询方法轮询其所有子代,然后等待所有子代都完成-加入它们,或者选择第一个准备就绪。后者稍微困难一些,所以让我们关注一下这一点。原因是,在第一个未来解决之后,其余的可能还在运行。它们的内存可能在其他地方引用。这就是我们略过的drop()方法的用武之地。拖放固定的对象应该会放松其内存保持有效的约束。例如,上面TimerFuture的Drop实现可以调用UV_TIMER_Stop(),这样回调就不会触发或用NULL覆盖对未来的悬空引用。对于其他类型,由于它们的Drop实现不是自动生成的,就像在Rust中发生的那样,因此定义futureDropp会很有用。

下面的实现优化了希望具有异构静态大小的期货竞赛列表的情况,方法是允许在某个对象中静态分配该列表,只需给出它们与offsetof()的偏移量:

Struct Race{struct Future Future;bool已完成;Union{struct{struct Future*base;/<;基指针。Size_t count;/<;竞赛中的期货数量。Size_t*Offsets;/<;从每个将来的基址到内存的偏移量。};size_t victor;/<;赢得比赛的未来索引。};};#定义race_get_nth(state,n)((struct Future*)((char*)(State)->;base+(State)->;offsets[n]))enum Poll futureRacePoll(struct Future*self,struct context*ctx){struct Race*state=(struct Race*)self;for(size_t i=0;i<;state->;numFutures;++i){struct Future*Future=race_get_nth(state,i);if(Future->;pol(Future,ctx)==poll_ady){//丢弃(size_t j=0;j<;state->;numFutures;++j){if(i==j)继续;struct Future*Future=race_get_nth(state,j);Future->;DROP(未来,CTX);}//设置竞争状态的结果->;Finish=true;state->;victor=i;return polReady;}}return Poll_Pending;}。

要记住的一件事是,如果您的主要未来包括循环中的竞赛,例如10微秒计时器和总是在第一次轮询时打开新套接字的未来之间的竞争,那么套接字将在每次循环迭代时被拆卸并重新打开。要解决这个问题,要么重新考虑单个任务是否是正确的工具,要么在程序启动时打开套接字一次-在不等待未来的情况下删除或排队接收的数据。

我们描述的用于实现未来状态机的协程是无堆栈的,而不是堆栈的:当构建未来时,分配的存储空间正好足以容纳在任何一点都处于活动状态的所有数据,而不是一般大小的组

为完整起见,所描述的内容与Rust中的一些不同之处在于,在Rust生态系统中,事件循环的不同部分已被抽象,以允许选择和匹配:

执行器负责调度一组线程上的任务。它会将上下文中的自定义Waker传递给每个将来。如果将来关注的是I/O,它会将唤醒程序交给。

此外,Rust编译器将插入的很多对drop()的调用都被省略了,这些调用会变成无操作(例如,在解析的未来上的调用)。

感谢您的阅读!不用说,这一切都提出了一个问题:为什么人们不会只使用铁锈,但作为一个考虑到这一点的人,我相信您肯定有一个非常明确的答案;)。为什么要在中途停下来?让他们脚部向西移动后,合乎逻辑的下一步是添加一个完整的效果系统,正如本技术报告中所描述的那样。然而,在这变得司空见惯之前,我将回顾未来,将其作为一个有用的模式。