其他分享
首页 > 其他分享> > C力栈展开内部功能

C力栈展开内部功能

作者:互联网

我正在学习C的过程中,目前正在摆弄以下代码:

class Bar;
struct Callback {
    virtual void Continue(Bar&) = 0;
};

// ...

void Foo(Bar& _x, Callback& result)
{
    // Do stuff with _x

    if(/* some condition */) {
        // TODO: Force unwind of stack
        result.Continue(_x);
        return;
    }

    // Do more stuff with _x

    if(/* some other condition */) {
        // TODO: Force unwind of stack
        result.Continue(_x);
        return;
    }

    // TODO: Force unwind of stack
    Bar y; // allocate something on the stack
    result.Continue(y);
}

主要思想是我知道在每个站点结果.Continue被调用时,函数Foo也将返回.因此,可以在调用继续操作之前将堆栈展开.

由于用户代码将以递归方式使用它,因此我担心此代码可能会导致stackoverflows.据我所知,执行result.Continue时,参数_x和result保留在堆栈中,因为仅当Foo返回时才取消堆栈的堆栈.

编辑:Continue函数可能(并且可能会)调用Foo方法:导致递归.只是简单地进行尾部调用优化,而不是Foo会导致堆栈溢出.

我该怎么做才能在Foo返回之前强制展开堆栈,将结果保存在一个临时(寄存器?)变量中,然后执行该连续操作?

解决方法:

您可以使用我发现的可解决此问题的设计.该设计假定使用事件驱动程序(但是您可以创建假事件循环).

为了清楚起见,让我们忘记您的特定问题,而将注意力放在两个对象之间的接口问题上:发送者对象将数据包发送到接收者对象.发送方总是必须等待接收方完成对任何数据包的处理,然后再发送另一个.该接口由两个调用定义:

> Send()-由发送方调用以开始发送数据包,由接收方实现
> Done()-接收方调用以通知发送方发送操作已完成,并且可以发送更多数据包

这些调用均未返回任何内容.接收者总是通过调用Done()来报告操作完成.如您所见,此接口在概念上与您介绍的接口相似,并且遭受相同的Send()和Done()之间的递归问题,可能导致堆栈溢出.

我的解决方案是将作业队列引入事件循环.作业队列是等待分派的事件的LIFO队列(堆栈).事件循环将队列顶部的作业视为最大优先级事件.换句话说,当事件循环必须决定要分派哪个事件时,如果队列不为空,并且没有任何其他事件,它将始终分派作业队列中的最高作业.

然后修改上述接口,以使Send()和Done()调用都排队.这意味着,当发送方调用Send()时,所有发生的事情是将作业推送到作业队列,并且该作业在由事件循环调度时,将调用接收方的Send()的实际实现. Done()以相同的方式工作-由接收方调用,它只是推送一个作业,该作业在分派时调用发送方的Done()实现.

了解队列设计如何提供三个主要好处.

>避免了堆栈溢出,因为Send()和Done()之间没有显式递归.但是发送者仍然可以直接从其Done()回调中再次调用Send(),接收者可以直接从其Send()回调中调用Done().
>它模糊了立即完成的(I / O)操作与需要一些时间的操作之间的差异,即接收器必须等待某些系统级事件.例如,当使用非阻塞套接字时,接收器中Send()的实现将调用send()syscall,后者可以设法发送某些内容,或者返回EAGAIN / EWOULDBLOCK,在这种情况下,接收器会要求事件循环通知当套接字可写时使用.当事件循环通知套接字可写时,它会重试send()系统调用,这很可能成功,在这种情况下,它将通过从此事件处理程序中调用Done()来通知发送方操作已完成.无论发生哪种情况,从发送者的角度来看都是相同的-在发送操作完成后,立即或一段时间后调用其Done()函数.
>它使错误处理与实际I / O正交.可以通过让接收方调用以某种方式处理错误的Error()回调来实现错误处理.了解发送方和接收方如何成为独立的可重用模块,这些模块对错误一无所知.如果发生错误(例如send()syscall失败,并显示真正的错误代码,而不是EAGAIN / EWOULDBLOCK),则可以简单地从Error()回调中破坏发送方和接收方,这很可能是创建发送方的同一代码的一部分和接收器.

这些功能一起在事件驱动程序中实现了优雅的flow-based programming.我在BadVPN软件项目中实现了队列设计和基于流的编程,并取得了巨大的成功.

最后,澄清为什么作业队列应该是LIFO. LIFO调度策略可对作业的分配顺序进行粗粒度控制.例如,假设您正在调用某个对象的某个方法,并且想要在该方法执行后以及递归分配了它推送的所有作业之后执行某些操作.您要做的就是在调用此方法之前先完成自己的工作,然后从该工作的事件处理程序中进行工作.

还有一个不错的属性,您始终可以通过使作业出队来取消此推迟的工作.例如,如果此函数执行的某些操作(包括其推送的作业)导致错误并因此破坏了我们自己的对象,则析构函数可以使我们推送的作业出队,从而避免了在作业执行并访问数据时发生崩溃的情况.不再存在.

标签:c,stack-overflow,stack,continuations
来源: https://codeday.me/bug/20191009/1877778.html