簡體   English   中英

遞歸和等待/異步關鍵字

[英]Recursion and the await / async Keywords

我對await關鍵字的工作方式有一個脆弱的把握,我想稍微擴展一下我對它的理解。

仍然讓我頭疼的問題是使用遞歸。 這是一個例子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestingAwaitOverflow
{
    class Program
    {
        static void Main(string[] args)
        {
            var task = TestAsync(0);
            System.Threading.Thread.Sleep(100000);
        }

        static async Task TestAsync(int count)
        {
            Console.WriteLine(count);
            await TestAsync(count + 1);
        }
    }
}

這個顯然拋出了StackOverflowException

我的理解是因為代碼實際上是同步運行的,直到第一個異步操作,之后它返回一個包含異步操作信息的Task對象。 在這種情況下,沒有異步操作,因此它只是在錯誤的承諾下繼續遞歸,它最終會返回一個Task

現在改變它只是一點點:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestingAwaitOverflow
{
    class Program
    {
        static void Main(string[] args)
        {
            var task = TestAsync(0);
            System.Threading.Thread.Sleep(100000);
        }

        static async Task TestAsync(int count)
        {
            await Task.Run(() => Console.WriteLine(count));
            await TestAsync(count + 1);
        }
    }
}

這個不會拋出StackOverflowException 我可以理解為什么它有效,但我會更多地稱之為直覺(它可能涉及代碼如何安排使用回調以避免構建堆棧,但我無法將這種直覺轉化為解釋)

所以我有兩個問題:

  • 第二批代碼如何避免StackOverflowException
  • 第二批代碼是否浪費了其他資源? (例如,它是否在堆上分配了大量的荒謬的Task對象?)

謝謝!

任何函數中第一個等待的部分同步運行。 在第一種情況下,由於這種情況,它會遇到堆棧溢出 - 沒有任何事情會中斷調用自身的函數。

第一個await(它沒有立即完成 - 對於你有很高可能性的情況)導致函數返回(並放棄它的堆棧空間!)。 它將其余部分排成一個延續。 TPL確保延續從不嵌套太深。 如果存在堆棧溢出的風險,則繼續將排隊到線程池,重置堆棧(它開始填滿)。

第二個例子仍然可以溢出! 如果Task.Run任務總是立即完成怎么辦? (這不太可能,但可以使用正確的OS線程調度)。 然后,async函數永遠不會被中斷(導致它返回並釋放所有堆棧空間),並且會產生與情況1相同的行為。

在您的第一個和第二個示例中,TestAsync仍在等待對自身的調用返回。 區別在於遞歸是打印並將線程返回到第二種方法中的其他工作。 因此,遞歸速度不足以成為堆棧溢出。 但是,第一個任務仍在等待,最終計數將達到它的最大整數大小,否則將再次拋出堆棧溢出。 重點是返回調用線程,但實際的異步方法是在同一個線程上調度的。 基本上,在等待完成之前忘記了TestAsync方法,但它仍然保存在內存中。 允許該線程執行其他操作,直到等待完成,然后該線程被記住並完成等待中斷的位置。 額外的等待調用存儲線程並再次忘記它,直到等待再次完成。 直到所有等待完成並且該方法因此完成TaskAsync仍然在內存中。 所以,這就是事情。 如果我告訴方法做某事然后調用等待任務。 我在其他地方的其他代碼繼續運行。 等待完成后,代碼會在那里重新啟動並完成,然后回到它之前的那個時間。 在您的示例中,您的TaskAsync始終處於邏輯刪除狀態(可以這么說),直到最后一次調用完成並將調用返回到鏈中。

編輯:我一直說存儲線程或那個線程,我的意思是例程。 它們都在同一個線程中,這是您示例中的主線程。 對不起,如果我困惑你。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM