2015-09-05 13 views
9

StackOverflowExceptionで次の非同期再帰が失敗し、カウンタがゼロになる最後のステップで正確に何が起こっているのですか?予期しないスタックオーバーフローが発生したにもかかわらず

static async Task<int> TestAsync(int c) 
{ 
    if (c < 0) 
     return c; 

    Console.WriteLine(new { c, where = "before", Environment.CurrentManagedThreadId }); 

    await Task.Yield(); 

    Console.WriteLine(new { c, where = "after", Environment.CurrentManagedThreadId }); 

    return await TestAsync(c-1); 
} 

static void Main(string[] args) 
{ 
    Task.Run(() => TestAsync(5000)).GetAwaiter().GetResult(); 
} 

出力:

 
... 
{ c = 10, where = before, CurrentManagedThreadId = 4 } 
{ c = 10, where = after, CurrentManagedThreadId = 4 } 
{ c = 9, where = before, CurrentManagedThreadId = 4 } 
{ c = 9, where = after, CurrentManagedThreadId = 5 } 
{ c = 8, where = before, CurrentManagedThreadId = 5 } 
{ c = 8, where = after, CurrentManagedThreadId = 5 } 
{ c = 7, where = before, CurrentManagedThreadId = 5 } 
{ c = 7, where = after, CurrentManagedThreadId = 5 } 
{ c = 6, where = before, CurrentManagedThreadId = 5 } 
{ c = 6, where = after, CurrentManagedThreadId = 5 } 
{ c = 5, where = before, CurrentManagedThreadId = 5 } 
{ c = 5, where = after, CurrentManagedThreadId = 5 } 
{ c = 4, where = before, CurrentManagedThreadId = 5 } 
{ c = 4, where = after, CurrentManagedThreadId = 5 } 
{ c = 3, where = before, CurrentManagedThreadId = 5 } 
{ c = 3, where = after, CurrentManagedThreadId = 5 } 
{ c = 2, where = before, CurrentManagedThreadId = 5 } 
{ c = 2, where = after, CurrentManagedThreadId = 5 } 
{ c = 1, where = before, CurrentManagedThreadId = 5 } 
{ c = 1, where = after, CurrentManagedThreadId = 5 } 
{ c = 0, where = before, CurrentManagedThreadId = 5 } 
{ c = 0, where = after, CurrentManagedThreadId = 5 } 

Process is terminated due to StackOverflowException. 

私がインストールされている.NET 4.6でこれを見ています。このプロジェクトは、.NET 4.5を対象としたコンソールアプリケーションです。

私は、スレッドが既にプールにリリースされている場合にはTask.Yieldための継続は、(上記の#5のように)同じスレッド上ThreadPool.QueueUserWorkItemによってスケジュールを受けることを理解 - 右await Task.Yield()後、しかしQueueUserWorkItemコールバックがされている前に、実際に予定されている。

なぜスタックがまだ深刻になっているのか理解できません。たとえ同じスレッドで呼び出されたとしても、ここでは同じスタックフレームで継続が起こるべきではありません。スレッドがそれぞれを反転され、TaskExt.Yieldを使用して代わりにTask.Yieldながら、

public static class TaskExt 
{ 
    public static YieldAwaiter Yield() { return new YieldAwaiter(); } 

    public struct YieldAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion 
    { 
     public YieldAwaiter GetAwaiter() { return this; } 

     public bool IsCompleted { get { return false; } } 

     public void GetResult() { } 

     public void UnsafeOnCompleted(Action continuation) 
     { 
      using (var mre = new ManualResetEvent(initialState: false)) 
      { 
       ThreadPool.UnsafeQueueUserWorkItem(_ => 
       { 
        mre.Set(); 
        continuation(); 
       }, null); 

       mre.WaitOne(); 
      } 
     } 

     public void OnCompleted(Action continuation) 
     { 
      throw new NotImplementedException(); 
     } 
    } 
} 

今:

は、私はさらに一歩を踏み出したと継続が同じスレッドでは発生しませんを確認します Yieldのカスタムバージョンを実装しました時間が、スタックオーバーフローがまだそこにある:再び

 
... 
{ c = 10, where = before, CurrentManagedThreadId = 3 } 
{ c = 10, where = after, CurrentManagedThreadId = 4 } 
{ c = 9, where = before, CurrentManagedThreadId = 4 } 
{ c = 9, where = after, CurrentManagedThreadId = 5 } 
{ c = 8, where = before, CurrentManagedThreadId = 5 } 
{ c = 8, where = after, CurrentManagedThreadId = 3 } 
{ c = 7, where = before, CurrentManagedThreadId = 3 } 
{ c = 7, where = after, CurrentManagedThreadId = 4 } 
{ c = 6, where = before, CurrentManagedThreadId = 4 } 
{ c = 6, where = after, CurrentManagedThreadId = 5 } 
{ c = 5, where = before, CurrentManagedThreadId = 5 } 
{ c = 5, where = after, CurrentManagedThreadId = 4 } 
{ c = 4, where = before, CurrentManagedThreadId = 4 } 
{ c = 4, where = after, CurrentManagedThreadId = 3 } 
{ c = 3, where = before, CurrentManagedThreadId = 3 } 
{ c = 3, where = after, CurrentManagedThreadId = 5 } 
{ c = 2, where = before, CurrentManagedThreadId = 5 } 
{ c = 2, where = after, CurrentManagedThreadId = 3 } 
{ c = 1, where = before, CurrentManagedThreadId = 3 } 
{ c = 1, where = after, CurrentManagedThreadId = 5 } 
{ c = 0, where = before, CurrentManagedThreadId = 5 } 
{ c = 0, where = after, CurrentManagedThreadId = 3 } 

Process is terminated due to StackOverflowException. 
+2

まだ匿名のオブジェクトを使用していることを確認してください。ToString trick :) – usr

+0

@usr、それは私があなたからそれを学んだ以来私のお気に入りの一つです:) – Noseratio

答えて

8

TPLの再入のストライキ:

スタックオーバーフローは、の最後に、すべての反復の完了がの後に発生することに注意してください。反復回数を増やしてもそれは変わりません。少量に下げると、スタックオーバーフローがなくなります。

メソッドTestAsyncの非同期状態マシンタスクを完了すると、スタックオーバーフローが発生します。それは "降下"で起こることはありません。これはバックアウトしてすべてのasyncメソッドタスクを完了するときに発生します。

最初にカウントを2000に減らして、デバッガの負荷を軽減しましょう。次に、コールスタックを見て:

enter image description here

確かに非常に反復的に長いです。これは見るべき正しい糸です。内側のタスクtが完了すると

 var t = await TestAsync(c - 1); 
     return t; 

は、それが外TestAsyncの残りの部分を実行させる:でクラッシュが発生します。これは単なるreturn文です。戻り値は、外部TestAsyncが生成したタスクを完了します。これにより、別のtなどの完了が再びトリガされます。

TPLはパフォーマンスの最適化としていくつかのタスク継続をインライン化します。この動作は、Stack Overflowの質問ですでに明らかなように、多くの悲嘆を引き起こしています。 It has been requested to remove it.問題はかなり古く、今までのところ応答がありませんでした。これは、最終的にTPLの再入荷問題を取り除く可能性があるという希望を喚起するものではありません。

TPLには、スタックの深さが深くなったときに継続のインライン展開をオフにするためのスタック深度チェックがあります。これは私には分かっていない(まだ)理由でここでは行われていない。スタック上のどこにもTaskCompletionSourceがあることに注意してください。 TaskAwaiterは、パフォーマンスを向上させるためにTPL内の内部機能を使用します。多分、最適化されたコードパスはスタック深度チェックを実行しないかもしれません。おそらくこれはその意味でのバグです。私はYieldを呼び出すとは思わない

は問題とは何かを持っていますが、それはTestAsyncの非同期完了を確実にするために、ここでそれを置くために良いことです。


のは、手動で非同期ステートマシンを書いてみましょう:我々はまた、継続インライン化が起こることを期待TaskContinuationOptions.ExecuteSynchronously

static Task<int> TestAsync(int c) 
{ 
    var tcs = new TaskCompletionSource<int>(); 

    if (c < 0) 
     tcs.SetResult(0); 
    else 
    { 
     Task.Run(() => 
     { 
      var t = TestAsync(c - 1); 
      t.ContinueWith(_ => tcs.SetResult(0), TaskContinuationOptions.ExecuteSynchronously); 
     }); 
    } 

    return tcs.Task; 
} 

static void Main(string[] args) 
{ 
    Task.Run(() => TestAsync(2000).ContinueWith(_ => 
    { 
      //breakpoint here - look at the stack 
    }, TaskContinuationOptions.ExecuteSynchronously)).GetAwaiter().GetResult(); 
} 

感謝。それはありませんが、それはスタックがオーバーフローしない:

enter image description here

TPLが深すぎになってからスタックを防ぐため、(上記で説明したように)だこと。このメカニズムは、asyncメソッドタスクを完了するときに存在しないようです。

ExecuteSynchronouslyが削除された場合、スタックは浅く、インライン化は発生しません。 await runs with ExecuteSynchronously enabled.

+0

素晴らしい答え。実際問題は、ここで説明したTPLの問題とまったく同じ問題を解決するために私が試している顧客の待ち時間(「AlwaysAsync」と呼ぶ)に触発されました。私は 'TestAsync'の中でそれを使用しますが、戻り行には使用しません。だから私は戻り値の行を 'TestAsync(c-1).AlwaysAsync()'を返すように変更しました。問題はなくなりました:) – Noseratio

+0

ここでスタックダイビングを取り除くもう一つの方法は 'Task.Run'です: '非同期タスク TestAsync(int c){if(c <0)return c;リターン待ちTask.Run(()=> TestAsync(c-1)); } '。 – Noseratio

+0

@Noseratioは実際にその作業をしていますか?私は完成がインラインで終わるかもしれないと思う。 Task.Runには、最適な最適化されたアンラッピングコードが内部にあります。多分ここでスタックオーバーフロー回避メカニズムが働いていて動作しています。 – usr

関連する問題