Async-Await issue with local variable cleanup
.net, async-await, c#, garbage-collection
Solution
`async/await` is a bit tricky. Let's have a closer look at your method:
private static async Task RunTest2Async()
{
Console.WriteLine("Test 2\n======");
await TestCreationAsync();
DisplayCounts();
}
The method prints something on the console. Then it calls `TestCreationAsync()` and a `Task` handle is returned. The method registers itself as the task's successor and returns a task handle itself. The compiler converts the method to a state machine to keep track of the entry points.
Then when the task returned by `TestCreationAsync()` has finished, it calls `RunTest2Async()` again (with the specified entry point). You can see this in the call stack when you're in debug mode. So the method is still alive, thus the created `simple` is still in scope. That's why it is not collected.
If you're in Release mode, `simple` is already collected in the `await` continuation. Probably because the compiler found out that it is not used any more. So in practice, this should not be a problem.
Here is a visualization:
Problem
I have encountered an issue where it appears that local resources may not be cleaned up during garbage collection if the resources are in an async-await method. I have created some sample code to illustrate the issue. SimpleClass The SimpleClass uses a static counter to record number of active instances by incrementing a static _count field during construction and decrementing the same field during destruction. ``` using System; namespace AsyncGarbageCollector { public class SimpleClass { private static readonly object CountLock = new object(); private static int _count; public SimpleClass() { Console.WriteLine("Constructor is called"); lock (CountLock) { _count++; } } ~SimpleClass() { Console.WriteLine("Destructor is called"); lock (CountLock) { _count--; } } public static int Count { get { lock (CountLock) { return _count; } } } } } ``` Program Here is the main program which has three tests - Standard call which initializes the class and then the variable is left to go out of scope - Async call which initializes the class and then the variable is left to go out of scope - Async call which initializes the class and then sets the variable to null before it goes out of scope In each case, the variable will be out of scope before GC.Collect is called. I would therefore expect the destructor to be called during garbage collection. ``` using System; using System.Threading.Tasks; namespace AsyncGarbageCollector { class Program { static void Main(string[] args) { Console.WriteLine("Press 1, 2 or 3 to start.\n\n"); var code = Console.ReadKey(true); if (code.Key == ConsoleKey.D1) RunTest1(); else if (code.Key == ConsoleKey.D2) RunTest2Async().Wait(); else if (code.Key == ConsoleKey.D3) RunTest3Async().Wait(); Console.WriteLine("\n\nPress any key to close."); Console.ReadKey(); } private static void RunTest1() { Console.WriteLine("Test 1\n======"); TestCreation(); DisplayCounts(); } private static async Task RunTest2Async() { Console.WriteLine("Test 2\n======"); await TestCreationAsync(); DisplayCounts(); } private static async Task RunTest3Async() { Console.WriteLine("Test 3\n======"); await TestCreationNullAsync(); DisplayCounts(); } private static void TestCreation() { var simple = new SimpleClass(); } private static async Task TestCreationAsync() { var simple = new SimpleClass(); await Task.Delay(50); } private static async Task TestCreationNullAsync() { var simple = new SimpleClass(); await Task.Delay(50); Console.WriteLine("Setting Null"); simple = null; } private static void DisplayCounts() { Console.WriteLine("Running GC.Collect()"); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Console.WriteLine("Count: " + SimpleClass.Count); } } } ``` Results ``` Test 1 ====== Constructor is called Running GC.Collect() Destructor is called Count: 0 Returned to Main Running GC.Collect() Count: 0 Test 2 ====== Constructor is called Running GC.Collect() Count: 1 Returned to Main Running GC.Collect() Destructor is called Count: 0 Test 3 ====== Constructor is called Setting Null Running GC.Collect() Destructor is called Count: 0 Returned to Main Running GC.Collect() Count: 0 ``` In Test 2, the destructor in the SimpleClass object is not called by garbage collection (even when it is out of scope) until garbage collection is called from the main function. Is there a good reason for this? My guess is that it is that the async method itself is still 'alive' until all relevant asyncs have completed and hence its variables are kept alive as a result. Question - Will local objects ever be collected during the lifetime of a async call? - If so, how can this be demonstrated. - If not, I am concerned that very large objects could cause out-of-memory exceptions as a result of using the async-await pattern. Any answers/comments would be greatly appreciated.