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

时间:2014-05-29 13:20:19

标签: c# .net 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;
                }
            }
        }
    }
}

程序

这是主程序,有三个测试

  1. 初始化类的标准调用,然后变量将超出范围
  2. 初始化类的异步调用,然后变量将超出范围
  3. 初始化类的异步调用,然后在变量超出范围之前将变量设置为null
  4. 在每种情况下,在调用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对象中的析构函数不会被垃圾收集调用(即使它超出范围),直到从main函数调用垃圾收集。

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

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

    1. 如果是这样,如何证明这一点。
    2. 如果没有,我担心非常大的对象可能会因使用async-await模式而导致内存不足异常。
    3. 任何答案/评论都将不胜感激。

2 个答案:

答案 0 :(得分:7)

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

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

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

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

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

这是一个可视化:

Visualization

答案 1 :(得分:3)

Async只是一种使用状态机的便捷方式。

简单来说,当你写

void async MyMethod()
{
    int k = await Some1();
    await Some2();
}

实际上你有一个像这样的结构(简化)

struct MyMethodState
{
    int k;
    int stage;
    Task currentTaskToWaitFor;
}

并且编译器重写该方法以在各阶段之间移动(阶段由您使用的地点async定义)

一个特别好的方法是使用Ildasm查看内部。

所以是的,你保留了对象的引用。