繁体   English   中英

C++ 强制堆栈在内部展开 function

[英]C++ force stack unwinding inside function

我正在学习 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);
}

主要思想是我知道在每个站点调用result.Continue时,function Foo也会返回。 因此,可以在调用延续之前展开堆栈。

由于用户代码将以递归方式使用它,我担心这段代码可能会导致计算器溢出。 据我了解,执行result.Continue时,参数_xresult保留在堆栈中,因为堆栈仅在Foo返回时展开。

编辑Continue function 可能(并且可能会)调用Foo方法:导致递归。 简单地尾调用优化Continue而不是Foo会导致计算器溢出。

Foo返回之前,我该怎么做才能强制展开堆栈,将result保存在临时( register ?)变量中,然后执行该继续?

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

为了清楚起见,让我们忘记您的特定问题,而是关注两个对象之间的接口问题:发送方object 向接收方 object 发送数据包。发送方始终必须等待接收方在发送之前完成任何数据包的处理其他。 该接口由两个调用定义:

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

这些调用都没有返回任何内容。 接收方总是通过调用 Done() 来报告操作的完成。 如您所见,此接口在概念上与您所呈现的类似,并且在 Send() 和 Done() 之间存在相同的递归问题,可能导致堆栈溢出。

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

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

了解队列设计如何提供三大优势。

  1. 它避免了堆栈溢出,因为在 Send() 和 Done() 之间没有明确的递归。 但是发送方仍然可以直接从其 Done() 回调中再次调用 Send(),并且接收方可以直接从其 Send() 回调中调用 Done()。

  2. 它模糊了立即完成的(I/O)操作和需要一些时间的操作之间的区别,即接收方必须等待某些系统级事件。 例如,当使用非阻塞 sockets 时,接收器中 Send() 的实现调用 send() 系统调用,它要么设法发送一些东西,要么返回 EAGAIN/EWOULDBLOCK,在这种情况下,接收器要求事件循环通知当套接字可写时。 当事件循环通知套接字可写时,它会重试 send() 系统调用,这很可能会成功,在这种情况下,它会通过从此事件处理程序调用 Done() 来通知发送方操作已完成。 无论发生哪种情况,从发送方的角度来看都是一样的 - 它的 Done() function 在发送操作完成时立即或在一段时间后被调用。

  3. 它使错误处理与实际 I/O 正交。 错误处理可以通过让接收者调用以某种方式处理错误的 Error() 回调来实现。 了解发送方和接收方如何成为独立的、对错误一无所知可重用模块 如果出现错误(例如 send() 系统调用失败并返回真正的错误代码,而不是 EAGAIN/EWOULDBLOCK),发送方和接收方可以简单地从 Error() 回调中销毁,这可能是创建发送方的相同代码的一部分和接收器。

这些功能共同支持在事件驱动程序中进行优雅的基于流的编程 我在我的BadVPN软件项目中实现了队列设计和基于流的编程,并取得了巨大成功。

最后,澄清一下为什么作业队列应该是后进先出。 LIFO 调度策略提供了对作业调度顺序的粗粒度控制。 例如,假设您正在调用某个 object 的某个方法,并希望该方法执行后以及它推送的所有作业都被递归分派后做一些事情。 您所要做的就是在调用此方法之前推送您自己的作业,并从该作业的事件处理程序中完成您的工作。

还有一个很好的特性,就是您可以随时通过使作业出队来取消这个推迟的工作。 例如,如果这个 function 所做的事情(包括它推送的作业)导致错误并因此破坏我们自己的 object,我们的析构函数可以使我们推送的作业出列,避免作业执行和访问数据时发生的崩溃不再存在。

在 function 结束之前,您不能在调用它时显式强制堆栈展开(破坏_xresult代码示例)。 如果您的递归(您没有显示)适合尾部调用优化,那么好的编译器将能够处理递归而无需创建新的堆栈框架。

除非我误解了,否则为什么不这样(单个 function 导致计算器溢出是 imo 的设计缺陷,但如果原始 Foo() 中有很多本地人,那么调用 DoFoo() 可能会缓解问题):

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

// ...

enum { use_x, use_y };

int DoFoo(Bar& _x)
{
    // Do stuff with _x

    if(/* some condition */) {
        return use_x;
    }

    // Do more stuff with _x

    if(/* some other condition */) {
        return use_x;
    }

    return use_y;
}

void Foo(Bar& _x, Callback& result)
{
    int result = DoFoo(_x);
    if (result == use_x)
    {
       result.Continue(_x);
       return;
    }

    Bar y; // allocate something on the stack
    result.Continue(y);
}

我找到了另一种方法,但这是特定于 Windows 和 Visual C++ 的:

void* growstk(size_t sz, void (*ct)(void*))
{
    void* p;
    __asm
    {
        sub esp, [sz]
        mov p, esp
    }
    ct(p);
    __asm
    {
        add esp, [sz]
    }
}

continuation void (*ct)(void*)将可以访问void* p; 堆栈分配 memory。每当继续返回时,通过将堆栈指针esp恢复到通常级别来释放 memory。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM