簡體   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