33

キーワードのしくみ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);
        }
    }
}

This one does not throw a StackOverflowException. I can sortof see why it works, but I would call it more of a gut feeling (it probably deals with how the code is arranged to use callbacks to avoid building the stack, but I can't translate that gut feeling into an explanation)

So I have two questions:

  • How does the second batch of code avoid a StackOverflowException?
  • Does the second batch of code waste other resources? (for example does it allocate an absurdly large number of Task objects on the heap?)

Thanks!

4

2 に答える 2

17

関数の最初の待機までの部分は同期して実行されます。最初のケースでは、そのためにスタックオーバーフローが発生します-それ自体を呼び出す関数を中断するものは何もありません。

最初の待機(すぐには完了しません-これは可能性が高い場合です)により、関数が戻ります(そしてスタックスペースを放棄します!)。残りの部分を継続としてキューに入れます。TPLは、継続が深くネストしすぎないようにします。スタックオーバーフローのリスクがある場合、継続はスレッドプールのキューに入れられ、スタックがリセットされます(スタックがいっぱいになり始めていました)。

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

于 2012-12-10T19:57:37.497 に答える
0

In your first and second example the TestAsync is still waiting for the call to itself to return. The difference is the recursion is printing and returning the thread to other work in the second method. Therefore the recursion isn't fast enough to be a stack overflow. However, the first task is still waiting and eventually count will reach it's max integer size or stack overflow will be thrown again. The point is the calling thread is returned to but the actual async method is scheduled on the same thread. Basically, the TestAsync method is forgotten until await is complete but it is still held in memory. The thread is allowed to do other things until await completes and then that thread is remembered and finishes where await left off. Additional await calls store the thread and forget it again until await is again complete. Until all awaits are completed and the method therefore completes the TaskAsync is still in memory. So, here's the thing. If I tell a method to do something and then call await for a task. The rest of my codes elsewhere continues running. When await is complete the code picks back up there and finishes and then goes back to what it was doing at that time right before it. In your examples your TaskAsync is always in a tombstoned state (so to speak) until the last call complete and returns the calls back up the chain.

EDIT: I kept saying store the thread or that thread and I meant routine. They are all on the same thread which is the main thread in your example. Sorry if I confused you.

于 2014-08-07T05:12:20.490 に答える