2017-06-26 17 views
11

我們有很多嵌套的異步方法,並查看我們不太瞭解的行爲。就拿這個簡單的C#控制檯應用程序StackOverflowExceptions堆棧展開時的嵌套異步方法

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

namespace AsyncStackSample 
{ 
    class Program 
    { 
    static void Main(string[] args) 
    { 
     try 
     { 
     var x = Test(index: 0, max: int.Parse(args[0]), throwException: bool.Parse(args[1])).GetAwaiter().GetResult(); 
     Console.WriteLine(x); 
     } 
     catch(Exception ex) 
     { 
     Console.WriteLine(ex); 
     } 
     Console.ReadKey(); 
    } 

    static async Task<string> Test(int index, int max, bool throwException) 
    { 
     await Task.Yield(); 

     if(index < max) 
     { 
     var nextIndex = index + 1; 
     try 
     { 
      Console.WriteLine($"b {nextIndex} of {max} (on threadId: {Thread.CurrentThread.ManagedThreadId})"); 

      return await Test(nextIndex, max, throwException).ConfigureAwait(false); 
     } 
     finally 
     { 
      Console.WriteLine($"e {nextIndex} of {max} (on threadId: {Thread.CurrentThread.ManagedThreadId})"); 
     } 
     } 

     if(throwException) 
     { 
     throw new Exception(""); 
     } 

     return "hello"; 
    } 
    } 
} 

當我們運行這個樣本具有下列參數:

AsyncStackSample.exe 2000 false 

我們得到一個StackOverflowException,這是我們在控制檯中看到的最後一條消息:

e 331 of 2000 (on threadId: 4) 

當我們改變參數到

AsyncStackSample.exe 2000 true 

我們結束這個消息

e 831 of 2000 (on threadId: 4) 

所以StackOverflowException堆棧(真的不知道我們是否應該稱呼它的平倉出現,但StackOverflowException我們的樣本中的遞歸調用發生後,在同步代碼,StackOverflowException將始終發生在嵌套方法調用中)。在我們拋出異常的情況下,StackOverflowException甚至更早發生。

我們知道我們可以在finally塊調用Task.Yield()解決這個問題,但是我們有幾個問題:

  1. 爲什麼堆棧生長平倉路徑(上相比於 方法,不會導致線程切換在等待)?
  2. 爲什麼StackOverflowException在異常情況下發生得比在我們不拋出異常的時候早?
+1

這可能是有用的https://stackoverflow.com/questions/13808166/recursion-and-the-await-async-keywords – Bit

+0

謝謝@Bit,雖然它觸及相同的主題,它不回答我們的具體問題。 –

+1

也相關,但我從來沒有給予回報的旅程很多考慮:https://stackoverflow.com/questions/35464468/async-recursion-where-is-my-memory-actually-going – spender

回答

8

爲什麼堆棧在展開路徑上增長(與不會導致線程在await上切換的方法相比)?

的核心原因是因爲await schedules its continuations with the TaskContinuationOptions.ExecuteSynchronously flag

因此,當執行「最內層」Yield時,最終會產生3000個不完整的任務,每個「內部」任務持有完成回調,完成下一個內部任務。這一切都在堆。

當最Yield簡歷(上一個線程池線程),繼續(同步)執行的Test方法,它完成其任務,該任務(同步)執行的Test方法的其餘部分,完成的其餘部分它的任務等等,幾千次。因此,每個任務完成後,該線程池線程上的調用堆棧實際上是增長

就我個人而言,我覺得這種行爲令人驚訝,並將其報告爲一個錯誤。但是,這個錯誤被微軟封爲「按設計」。有趣的是,JavaScript中的Promises規範(並且通過擴展,await的行爲)總是有承諾完成異步運行,並且從未同步。這讓一些JS開發人員感到困惑,但這是我期望的行爲。

通常情況下,它可以正常工作,並且ExecuteSynchronously可以作爲次要的性能改進。但是,正如你所指出的那樣,有一些像「異步遞歸」的場景可能導致StackOverflowException

There are some heuristics in the BCL to run continuations asynchronously if the stack is too full,但他們只是啓發式,並不總是工作。

爲什麼StackOverflowException在Exception的情況下比在我們不拋出異常的時候更早出現?

這是一個很好的問題。我不知道。 :)

+0

感謝Stephen的偉大答案。任何想法如何得到第二個問題的答案? –

+2

你可以把它作爲一個單獨的問題。這樣就非常具體 - 只有一個問題。 –