Async-Await问题与局部变量清理

Sho*_*ome 4 .net c# garbage-collection async-await

我遇到了一个问题,如果资源是在async-await方法中,似乎在垃圾回收期间可能无法清除本地资源.

我已经创建了一些示例代码来说明问题.

SimpleClass

SimpleClass使用静态计数器通过在构造期间递增静态_count字段并在销毁期间递减相同字段来记录活动实例的数量.

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;
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

程序

这是主程序,有三个测试

  1. 初始化类的标准调用然后变量将超出范围
  2. 初始化类然后变量的异步调用将超出范围
  3. 异步调用初始化类,然后在变量超出范围之前将变量设置为null

在每种情况下,在调用GC.Collect之前,变量将超出范围.因此,我希望在垃圾收集期间调用析构函数.

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

结果

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
Run Code Online (Sandbox Code Playgroud)

在测试2中,SimpleClass对象中的析构函数不会被垃圾收集调用(即使它超出范围),直到从main函数调用垃圾收集.

有这么好的理由吗?我的猜测是,异步方法本身仍然是"活着的",直到所有相关的asyncs都已完成,因此其变量保持活着状态.

问题 - 在异步调用的生命周期内是否会收集本地对象?

  1. 如果是这样,如何证明这一点.
  2. 如果没有,我担心非常大的对象可能会因使用async-await模式而导致内存不足异常.

任何答案/评论将不胜感激.

Nic*_*ler 7

async/await有点棘手.让我们仔细看看你的方法:

private static async Task RunTest2Async()
{
    Console.WriteLine("Test 2\n======");
    await TestCreationAsync();
    DisplayCounts();
}
Run Code Online (Sandbox Code Playgroud)

该方法在控制台上打印一些东西.然后调用TestCreationAsync()Task返回句柄.该方法将自身注册为任务的后继者,并返回任务句柄本身.编译器将方法转换为状态机以跟踪入口点.

然后当返回的任务TestCreationAsync()完成时,它RunTest2Async()再次调用(使用指定的入口点).当您处于调试模式时,您可以在调用堆栈中看到这一点.因此该方法仍然存在,因此创建simple的仍然在范围内.这就是为什么它没有收集.

如果您处于发布模式,simple则已在await续集中收集.可能是因为编译器发现它不再被使用了.所以在实践中,这应该不是问题.

这是一个可视化:

可视化