I have a fragile grasp of how the await
keyword works, and I want to extend my understanding of it a bit.
The issue that still makes my head spin is the use of recursion. Here's an example:
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);
}
}
}
This one obviously throws a StackOverflowException
.
My understanding is because the code actually runs synchronously until the first asynchronous action, after which it returns a Task
object that contains information about the asynchronous operation. In this case, there is no asynchronous operation, thus it just keeps recursing under the false promise that it will eventually get a Task
returned.
Now changing it just a tiny bit:
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!
See Question&Answers more detail:
os 与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…