c# - 局部变量清理的异步等待问题

标签 c# .net garbage-collection async-await

我遇到了一个问题,如果资源在 async-await 方法中,则在垃圾回收期间可能不会清理本地资源。

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

简单类

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

程序

这是包含三个测试的主程序

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

结果

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

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

这有充分的理由吗?我的猜测是,在所有相关的异步完成之前,异步方法本身仍然是“事件的”,因此它的变量因此保持事件状态。

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

  1. 如果是这样,如何证明这一点。
  2. 否则,我担心非常大的对象可能会因为使用异步等待模式而导致内存不足异常。

如有任何答复/意见,我们将不胜感激。

最佳答案

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

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

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

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

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

这是一个可视化:

Visualization

关于c# - 局部变量清理的异步等待问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23934352/

相关文章:

c# - 使用 Unity 代替 ASP.Net Core DI IServiceCollection

c# - Autofac:批量注册开放通用类型

java - 为什么 ArrayList.clear() 方法不释放内存?

java - 当我们有字符串池时为什么要进行字符串重复数据删除

c# - 单点触控 : Can't create DbParameterCollection?

c# - RefreshSection 未按预期工作

c# - .NET Standard 1.6 中的 System.Threading.Tasks.Parallel 在哪里?

c# - 无法从 Microsoft.Extensions.Logging 解析 ILogger

c# - 在已连接的客户端上处理套接字异常10060

c# - 当所有对象引用被删除时,异步方法会发生什么?