简体   繁体   English

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

[英]C++ force stack unwinding inside function

I'm in the process of learning C++ and currently I'm fiddling with the following code:我正在学习 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);
}

The main idea is that I know that at every site result.Continue is called, the function Foo will return too.主要思想是我知道在每个站点调用result.Continue时,function Foo也会返回。 Therefore the stack can be unwound before calling the continuation.因此,可以在调用延续之前展开堆栈。

As the user code will use this in a recursive way, I'm worried that this code may lead into stackoverflows.由于用户代码将以递归方式使用它,我担心这段代码可能会导致计算器溢出。 As far as my understanding goes, the parameters _x and result are kept on the stack when result.Continue is executed, because the stack is unwound only when Foo returns.据我了解,执行result.Continue时,参数_xresult保留在堆栈中,因为堆栈仅在Foo返回时展开。

Edit : The Continue function may (and probably will) call the Foo method: resulting in recursion.编辑Continue function 可能(并且可能会)调用Foo方法:导致递归。 Simply tail-call optimizing Continue and not Foo can lead to a stackoverflow.简单地尾调用优化Continue而不是Foo会导致计算器溢出。

What can I do to force unwinding of the stack, before the return of Foo , keeping result in a temporary ( register ?) variable and then execute that continuation?Foo返回之前,我该怎么做才能强制展开堆栈,将result保存在临时( register ?)变量中,然后执行该继续?

You can use a design I have found that solves this problem.您可以使用我发现的可以解决此问题的设计。 The design assumes an event-driven program (but you can create a fake event loop otherwise).该设计假定一个事件驱动程序(但您可以创建一个假的事件循环)。

For clarity, let's forget about your particular problem and instead focus on the problem of an interface between two objects: a sender object sending data packets to a receiver object. The sender always has to wait for the receiver to finish processing any data packet before sending another.为了清楚起见,让我们忘记您的特定问题,而是关注两个对象之间的接口问题:发送方object 向接收方 object 发送数据包。发送方始终必须等待接收方在发送之前完成任何数据包的处理其他。 The interface is defined by two calls:该接口由两个调用定义:

  • Send() - called by sender to start sending a data packet, implemented by receiver Send() - 由发送方调用以开始发送数据包,由接收方实现
  • Done() - called by receiver to inform the sender that the send operation is complete and it is possible to send more packets Done() - 由接收方调用以通知发送方发送操作已完成并且可以发送更多数据包

None of these calls return anything.这些调用都没有返回任何内容。 The receiver always reports completion of the operation by calling Done().接收方总是通过调用 Done() 来报告操作的完成。 As you can see, this interface is conceptually similar to what you have presented, and suffers from the same problem of recursion between Send() and Done(), possibly resulting in a stack overflow.如您所见,此接口在概念上与您所呈现的类似,并且在 Send() 和 Done() 之间存在相同的递归问题,可能导致堆栈溢出。

My solution was the introduction of a job queue into the event loop.我的解决方案是在事件循环中引入作业队列 The job queue is a LIFO queue (stack) of events waiting to be dispatched.作业队列是等待分派的事件的LIFO 队列(堆栈)。 The event loop treats the job on top of the queue as a maximum-priority event.事件循环将队列顶部的作业视为最高优先级事件。 In other words, when the event loop has to decide which event to dispatch, it will always dispatch the top job in the job queue if the queue is not empty, and not any other event.换句话说,当事件循环必须决定调度哪个事件时,如果队列不为空,它将始终调度作业队列中的顶部作业,而不是任何其他事件。

The interface described above is then modified to make both the Send() and Done() calls queued .然后修改上述接口,使 Send() 和 Done() 调用都排队 This means that when the sender calls Send(), all that happens is that a job is pushed to the job queue, and this job, when dispatched by the event loop, will call the receiver's real implementation of Send().这意味着当发送方调用 Send() 时,所发生的只是将一个作业推送到作业队列,而这个作业在被事件循环调度时,将调用接收方对 Send() 的实际实现。 Done() works the same way - called by the receiver, it just pushes a job which, when dispatched, calls the sender's implementation of Done(). Done() 的工作方式相同 - 由接收方调用,它只是推送一个作业,该作业在分派时调用发送方的 Done() 实现。

See how the queue design provides three major benefits.了解队列设计如何提供三大优势。

  1. It avoids stack overflow because there is no explicit recursion between Send() and Done().它避免了堆栈溢出,因为在 Send() 和 Done() 之间没有明确的递归。 But the sender can still call Send() again right from its Done() callback, and the receiver can call Done() right from its Send() callback.但是发送方仍然可以直接从其 Done() 回调中再次调用 Send(),并且接收方可以直接从其 Send() 回调中调用 Done()。

  2. It blurs the difference between (I/O) operations that have completed immediately and those that take some time, ie the receiver has to wait for some system-level event.它模糊了立即完成的(I/O)操作和需要一些时间的操作之间的区别,即接收方必须等待某些系统级事件。 For example, when using non-blocking sockets, the implementation of Send() in the receiver calls the send() syscall, which either manages to send something, or returns EAGAIN/EWOULDBLOCK, in which case the receiver asks the event loop to inform it when the socket is writable.例如,当使用非阻塞 sockets 时,接收器中 Send() 的实现调用 send() 系统调用,它要么设法发送一些东西,要么返回 EAGAIN/EWOULDBLOCK,在这种情况下,接收器要求事件循环通知当套接字可写时。 It retries the send() syscall when it's informed by the event loop that the socket is writable, which likely succeeds, in which case it informs the sender that the operation is complete by calling Done() from this event handler.当事件循环通知套接字可写时,它会重试 send() 系统调用,这很可能会成功,在这种情况下,它会通过从此事件处理程序调用 Done() 来通知发送方操作已完成。 Whichever happens, it's the same from the perspective of the sender - its Done() function is called when the send operation is complete, immediately or after some time.无论发生哪种情况,从发送方的角度来看都是一样的 - 它的 Done() function 在发送操作完成时立即或在一段时间后被调用。

  3. It makes error handling orthogonal to the actual I/O.它使错误处理与实际 I/O 正交。 Error handling can be implemented by having the receiver call an Error() callback which handles the error somehow.错误处理可以通过让接收者调用以某种方式处理错误的 Error() 回调来实现。 See how the sender and receiver can be independent reusable modules that don't know anything about errors .了解发送方和接收方如何成为独立的、对错误一无所知可重用模块 In case of error (eg send() syscall fails with a real error code, not EAGAIN/EWOULDBLOCK), the sender and receiver can simply be destroyed from the Error() callback, which is likely part of the same code that created the sender and receiver.如果出现错误(例如 send() 系统调用失败并返回真正的错误代码,而不是 EAGAIN/EWOULDBLOCK),发送方和接收方可以简单地从 Error() 回调中销毁,这可能是创建发送方的相同代码的一部分和接收器。

Together, these features enable elegant flow-based programming in event-driven programs.这些功能共同支持在事件驱动程序中进行优雅的基于流的编程 I have implemented the queue design and flow-based programming in my BadVPN software project, with great success.我在我的BadVPN软件项目中实现了队列设计和基于流的编程,并取得了巨大成功。

Finally, some clarification on why the job queue should be a LIFO.最后,澄清一下为什么作业队列应该是后进先出。 The LIFO scheduling policy provides coarse-grained control over the order of dispatching of jobs. LIFO 调度策略提供了对作业调度顺序的粗粒度控制。 For instance, suppose you are calling some method of some object, and want to do something after this method has executed, and after all the jobs it pushed have been dispatched, recursively.例如,假设您正在调用某个 object 的某个方法,并希望该方法执行后以及它推送的所有作业都被递归分派后做一些事情。 All you have to do is push a job of your own right before calling this method, and do your work from the event handler of this job.您所要做的就是在调用此方法之前推送您自己的作业,并从该作业的事件处理程序中完成您的工作。

There is also the nice property that you can always cancel this postponed work by dequeuing the job.还有一个很好的特性,就是您可以随时通过使作业出队来取消这个推迟的工作。 For instance, if something this function did (including the jobs it pushed) resulted in an error and consequent destruction of our own object, our destructor can dequeue the job that we pushed, avoiding a crash that would happen if the job executed and accessed data that no longer exists.例如,如果这个 function 所做的事情(包括它推送的作业)导致错误并因此破坏我们自己的 object,我们的析构函数可以使我们推送的作业出列,避免作业执行和访问数据时发生的崩溃不再存在。

You can't explicitly force stack unwinding as you call it (destruction of _x and result in the code sample) before the function ends.在 function 结束之前,您不能在调用它时显式强制堆栈展开(破坏_xresult代码示例)。 If your recursion (you didn't show it) is amenable to tail call optimisation then good compilers will be able to handle the recursion without creating a new stack frame.如果您的递归(您没有显示)适合尾部调用优化,那么好的编译器将能够处理递归而无需创建新的堆栈框架。

Unless I misunderstood, why not something like this (a single function causing a stackoverflow is a design flaw imo, but if there are lots of locals in your original Foo() then calling DoFoo() may alleviate the problem):除非我误解了,否则为什么不这样(单个 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);
}

I've found another way, but this is specific to Windows and Visual C++:我找到了另一种方法,但这是特定于 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]
    }
}

The continuation void (*ct)(void*) will have access to the void* p; continuation void (*ct)(void*)将可以访问void* p; stack-allocated memory. Whenever the continuation returns, the memory is deallocated by restoring the stack pointer esp to the usual level.堆栈分配 memory。每当继续返回时,通过将堆栈指针esp恢复到通常级别来释放 memory。

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

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