2012-12-10 47 views
27

私はawaitキーワードがどのように機能するかについて脆​​弱な理解を持っています。再帰とawait/asyncキーワード

私の頭が回転する問題は、再帰の使用です。ここに例があります:

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); 
     } 
    } 
} 

この1つはStackOverflowExceptionをスローしません。私はsortofを参照してください、しかし、私はそれが腸の感情(それはおそらくスタックのビルドを避けるためにコールバックを使用するように配置されているコードを扱うだろうが、私はその腸の感情を説明)

だから、私は2つの質問があります:コードの第二のバッチは、StackOverflowExceptionを避けるどのよう

  • を?
  • コードの2番目のバッチは他のリソースを浪費しますか? (例えば、ヒープ上に不当に多数のタスクオブジェクトを割り当てていますか?)

ありがとう!

答えて

16

どの関数でも最初の部分が同期して実行されます。最初のケースでは、スタックオーバーフローが発生するため、スタックオーバーフローが発生します。

最初の待ち時間(直ちに完了しない - これは高い確率で起こります)は、関数が戻り値を返すようにします(スタック領域をあきらめます)。残りの部分を継続としてキューに入れます。 TPLは継続が深く深く巣立つことを保証しません。スタックオーバーフローのリスクがある場合、継続はスレッドプールにキューイングされ、スタックがリセットされます(これはいっぱいになりました)。

2番目の例はまだオーバーフローする可能性があります。Task.Runタスクがいつもすぐに完了したらどうなりますか? (これはそうではありませんが、正しいOSスレッドスケジューリングでは可能です。そうすれば、非同期関数は中断されることはありません(すべてのスタック領域を解放して解放させます)。ケース1と同じ動作が発生します。

+0

もし 'Task.Run()'を直ちに完了した 'Task'オブジェクトを返す関数に置き換えると、スタックオーバーフロー例外が再導入されますか? (あるいは、私はできないものを提案しただけですか?) – riwalk

+0

@ Stargazer712それは可能であるだけでなく、あなたが描いていることを確かにするでしょう。単に 'await Task.FromResult (null);を試してみてください。これは短い時間内に' await'をたくさん呼び出す単一の関数を避けなければならない理由です。待機しているタスクがかなり長い時間稼働しているか、同期して実行しても問題ないような限られた数のタスクがあることを確認する必要があります。 – Servy

+5

@ Stargazer712はい! Task.Yield()(これは継続をポストする必要があることが保証されています)で置き換えると、(パフォーマンスコストで)スタックのオーバーフローを防ぐことができます。 'Yield'は' Task'以外の待ち時間を返します。待機中の機能によって照会されたときに完了しないようにする必要があるため、この保証をタスクから取得することははるかに難しくなります。 – usr

0

最初の例と2番目の例では、TestAsyncは呼び出し自体が返るのを待っています。違いは、2番目の方法でスレッドを印刷して他の作業に戻す再帰です。したがって、再帰はスタックオーバーフローになるほど速くはありません。しかし、最初のタスクはまだ待機しており、最終的にカウントが最大整数サイズに達するか、スタックオーバーフローが再びスローされます。要点は、呼び出しスレッドが返されるが、実際の非同期メソッドは同じスレッドでスケジュールされるということです。基本的には、待機が完了するまでTestAsyncメソッドは忘れてしまいますが、まだメモリに保持されています。スレッドは完了を待つまで他のことを行うことが許され、そのスレッドは記憶され、中断されたところで終了する。追加待ちの呼び出しはスレッドを格納し、再び待機が完了するまでそれを忘れる。すべての待機が完了し、その方法が完了するまでTaskAsyncはまだメモリに残っています。それで、ここに事があります。方法を教えて何かをしてから、仕事を待っていれば、残りのコードは他の場所でも動作し続けます。待機が完了すると、コードはそこに戻ってピックアップし、その直前にその時点で行っていたことに戻ります。あなたの例では、TaskAsyncは、最後の呼び出しが完了して呼び出しをチェーンに戻すまで、(つまり話すために)トゥーンステーン状態になっています。

EDIT:私はスレッドまたはそのスレッドを格納していると私はルーチンを意味し続けた。彼らは皆あなたの例のメインスレッドである同じスレッド上にあります。私はあなたを混乱させて申し訳ありません。

+1

しかし、ここではスタックは使用されていません。タスクに割り当てられたヒープの鎖だけが互いにポイントし、永遠に待機します。 –