2012-12-10 76 views
27

我的await關鍵字是如何工作的脆弱把握,我想延長我對它的理解有點。遞歸和的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。我可以sortof明白爲什麼它的工作原理,但我把它更多的是直覺的(它可能與代碼是如何安排使用回調以避免建立堆棧交易,但我不能把這一預感到解說)

所以我有兩個問題:

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

謝謝!

回答

16

的部分最多在任何函數的第一個AWAIT同步運行。在第一種情況下,它會因爲這種情況而陷入堆棧溢出 - 沒有任何事情會中斷自己調用的函數。

第一的await(不立即完成 - 這是具有高的情形產生對你的情況)導致返回功能(並放棄它的堆棧空間!)。它將其餘的部分作爲延續。 TPL確保延續永遠不會太深。如果存在堆棧溢出的風險,則繼續排隊到線程池,重新設置堆棧(堆棧開始填充)。

第二個例子仍然可以溢出!如果Task.Run任務總是立即完成,該怎麼辦? (這是不太可能的,但可以通過正確的OS線程調度來實現)。然後,異步函數永遠不會被中斷(導致它返回並釋放所有堆棧空間),並且會導致與情況1相同的行爲。

+0

因此,如果我用一個立即返回完成的Task對象的函數替換Task.Run(),它會重新引入堆棧溢出異常? (或者我剛剛提出的東西是不可能的?) – riwalk

+0

@ Stargazer712不僅是可能的,它確實會做你正在描述的東西。只需嘗試'等待Task.FromResult (null);'這就是爲什麼你應該避免在短時間內大量調用'await'的單個函數;你應該確保等待的任務要麼運行時間相當長,要麼有少量的有限的任務,以便同步運行它們是可以的。 – Servy

+5

@ Stargazer712是的!如果你用'Task.Yield()'替換它(保證需要發佈一個延續),你將得到保證免於堆棧溢出(以性能成本)。請注意,'Yield'返回的是除Task之外的其他候選項。從任務獲得此保證要困難得多,因爲您必須確保在等待功能查詢時未完成此任務。 – usr

0

在你的第一和第二個例子TestAsync還在等待調用本身返回。不同之處在於遞歸是在第二種方法中打印並將線程返回到其他工作。因此遞歸速度不夠快是堆棧溢出。但是,第一個任務仍在等待,最終計數將達到其最大整數大小,否則堆棧溢出將再次拋出。重點是調用線程返回,但實際的異步方法調度在同一個線程。基本上,TestAsync方法遺忘了,直到AWAIT已完成,但它仍然保存在內存中。該線程被允許做其他事情,直到等待完成,然後該線程被記住並完成在等待中斷的地方。額外的等待電話存儲線程並再次忘記,直到等待再次完成。在所有等待完成並且方法完成之前,TaskAsync仍在內存中。所以,這是事情。如果我告訴某種方法去做某件事,然後打電話等待任務。我的其他代碼繼續運行。當等待完成時,代碼會在那裏回到那裏並完成,然後返回到當前正在執行的操作。在您的示例中,您的TaskAsync始終處於邏輯狀態(可以這麼說),直到最後一次調用完成並將調用返回到鏈中。

編輯:我一直說存儲線程或線程,我的意思是例行公事。他們都在同一個線程,這是你的例子中的主線程。對不起,如果我困惑你。

+1

但是這裏沒有使用堆棧,只有堆分配了任務的鏈,指向彼此,並永遠等待。 –