为什么我引用GC时会收集它?

斯里拉姆·萨克提维

让我们看一下显示问题的以下代码片段。

class Program
{
    static void Main(string[] args)
    {
        var task = Start();
        Task.Run(() =>
        {
            Thread.Sleep(500);
            Console.WriteLine("Starting GC");
            GC.Collect();
            GC.WaitForPendingFinalizers();
            Console.WriteLine("GC Done");
        });

        task.Wait();

        Console.Read();
    }

    private static async Task Start()
    {
        Console.WriteLine("Start");
        Synchronizer sync = new Synchronizer();
        var task = sync.SynchronizeAsync();
        await task;

        GC.KeepAlive(sync);//Keep alive or any method call doesn't help
        sync.Dispose();//I need it here, But GC eats it :(
    }
}

public class Synchronizer : IDisposable
{
    private TaskCompletionSource<object> tcs;

    public Synchronizer()
    {
        tcs = new TaskCompletionSource<object>(this);
    }

    ~Synchronizer()
    {
        Console.WriteLine("~Synchronizer");
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose");
    }

    public Task SynchronizeAsync()
    {
        return tcs.Task;
    }
}

输出产生:

Start
Starting GC
~Synchronizer
GC Done

如您所见,sync获取了Gc'd(更具体地说,已完成,我们不知道是否回收了内存)。但为什么?引用我的对象后,GC为什么会收集它?

研究:我花了一些时间调查幕后发生的事情,似乎由C#编译器生成的状态机被保留为局部变量,并且在第一次await命中之后,状态机本身似乎超出了范围。

因此,这GC.KeepAlive(sync);sync.Dispose();没有帮助,因为它们生活在状态机中,而状态机本身不在范围之内。

C#编译器不应该生成代码,以使我的sync实例在仍然需要时会超出范围。这是C#编译器中的错误吗?还是我缺少基本的东西?

PS:我不是在寻找解决方法,而是在解释为什么编译器会这样做?我用谷歌搜索,但没有找到任何相关问题,如果对此重复感到抱歉。

Update1:我已经修改了TaskCompletionSource创建内容以保存该Synchronizer实例,但这仍然无济于事。

鼻塞

什么GC.KeepAlive(sync)-本身空白的-此处只是向编译器发出的将指令添加sync到为struct生成的状态机中的指令Start正如@usr所指出的,由其调用方返回外部任务包含对该内部状态机的引用Start

另一方面,内部使用TaskCompletionSource'tcs.Task任务Start确实包含这样的引用(因为它持有对await继续回调的引用,因此对整个状态机都有引用;该回调tcs.Taskawait里面注册了Start,在tcs.Task之间创建了循环引用。状态机)。但是,它们都tcs没有tcs.Task露在外面 Start(可能已经被强引用了),因此状态机的对象图被隔离并进行了GC处理。

您可以通过创建明确的强引用来避免过早的GC tcs

public Task SynchronizeAsync()
{
    var gch = GCHandle.Alloc(tcs);
    return tcs.Task.ContinueWith(
        t => { gch.Free(); return t; },
        TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}

或者,使用以下更具可读性的版本async

public async Task SynchronizeAsync()
{
    var gch = GCHandle.Alloc(tcs);
    try
    {
        await tcs.Task;
    }
    finally
    {
        gch.Free();
    }
}

要远一点借此研究,考虑以下变化不大,注意Task.Delay(Timeout.Infinite)和我回到事实和使用sync作为ResultTask<object>它并没有变得更好:

    private static async Task<object> Start()
    {
        Console.WriteLine("Start");
        Synchronizer sync = new Synchronizer();

        await Task.Delay(Timeout.Infinite); 

        // OR: await new Task<object>(() => sync);

        // OR: await sync.SynchronizeAsync();

        return sync;
    }

    static void Main(string[] args)
    {
        var task = Start();
        Task.Run(() =>
        {
            Thread.Sleep(500);
            Console.WriteLine("Starting GC");
            GC.Collect();
            GC.WaitForPendingFinalizers();
            Console.WriteLine("GC Done");
        });

        Console.WriteLine(task.Result);

        Console.Read();
    }

IMO,sync在我可以通过进行对象访问之前过早地对其进行GC处理是非常意外和不希望的task.Result

现在,更改Task.Delay(Timeout.Infinite)Task.Delay(Int32.MaxValue),一切按预期进行。

在内部,它归结为在await继续回调对象(委托本身)上的强引用,该引用应在导致该回调的操作仍处于挂起状态(在运行中)时保留。我在“异步/等待,自定义等待者和垃圾收集器”中对此进行了解释

IMO,此操作可能永无休止(如Task.Delay(Timeout.Infinite)或不完整TaskCompletionSource的事实不应影响此行为。对于大多数自然异步操作,此类强引用确实由进行低级OS调用的基础.NET代码持有(例如在with中Task.Delay(Int32.MaxValue),它将回调传递给非托管Win32计时器API并通过保持GCHandle.Alloc)。

如果没有待处理的任何级别的非托管调用(这可能是与情况Task.Delay(Timeout.Infinite)TaskCompletionSource是一种冷Task,定制awaiter),有一个地方没有明确的强引用,状态机的对象图纯粹是管理和隔离,所以确实发生了意外的GC。

我认为这是async/await基础架构中的一个小设计折衷,以避免standard创建通常多余的强引用ICriticalNotifyCompletion::UnsafeOnCompletedTaskAwaiter

无论如何,使用自定义的等待者(可能称为StrongAwaiter很容易实现通用的解决方案

private static async Task<object> Start()
{
    Console.WriteLine("Start");
    Synchronizer sync = new Synchronizer();

    await Task.Delay(Timeout.Infinite).WithStrongAwaiter();

    // OR: await sync.SynchronizeAsync().WithStrongAwaiter();

    return sync;
}

StrongAwaiter 本身(通用和非通用):

public static class TaskExt
{
    // Generic Task<TResult>

    public static StrongAwaiter<TResult> WithStrongAwaiter<TResult>(this Task<TResult> @task)
    {
        return new StrongAwaiter<TResult>(@task);
    }

    public class StrongAwaiter<TResult> :
        System.Runtime.CompilerServices.ICriticalNotifyCompletion
    {
        Task<TResult> _task;
        System.Runtime.CompilerServices.TaskAwaiter<TResult> _awaiter;
        System.Runtime.InteropServices.GCHandle _gcHandle;

        public StrongAwaiter(Task<TResult> task)
        {
            _task = task;
            _awaiter = _task.GetAwaiter();
        }

        // custom Awaiter methods
        public StrongAwaiter<TResult> GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted
        {
            get { return _task.IsCompleted; }
        }

        public TResult GetResult()
        {
            return _awaiter.GetResult();
        }

        // INotifyCompletion
        public void OnCompleted(Action continuation)
        {
            _awaiter.OnCompleted(WrapContinuation(continuation));
        }

        // ICriticalNotifyCompletion
        public void UnsafeOnCompleted(Action continuation)
        {
            _awaiter.UnsafeOnCompleted(WrapContinuation(continuation));
        }

        Action WrapContinuation(Action continuation)
        {
            Action wrapper = () =>
            {
                _gcHandle.Free();
                continuation();
            };

            _gcHandle = System.Runtime.InteropServices.GCHandle.Alloc(wrapper);
            return wrapper;
        }
    }

    // Non-generic Task

    public static StrongAwaiter WithStrongAwaiter(this Task @task)
    {
        return new StrongAwaiter(@task);
    }

    public class StrongAwaiter :
        System.Runtime.CompilerServices.ICriticalNotifyCompletion
    {
        Task _task;
        System.Runtime.CompilerServices.TaskAwaiter _awaiter;
        System.Runtime.InteropServices.GCHandle _gcHandle;

        public StrongAwaiter(Task task)
        {
            _task = task;
            _awaiter = _task.GetAwaiter();
        }

        // custom Awaiter methods
        public StrongAwaiter GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted
        {
            get { return _task.IsCompleted; }
        }

        public void GetResult()
        {
            _awaiter.GetResult();
        }

        // INotifyCompletion
        public void OnCompleted(Action continuation)
        {
            _awaiter.OnCompleted(WrapContinuation(continuation));
        }

        // ICriticalNotifyCompletion
        public void UnsafeOnCompleted(Action continuation)
        {
            _awaiter.UnsafeOnCompleted(WrapContinuation(continuation));
        }

        Action WrapContinuation(Action continuation)
        {
            Action wrapper = () =>
            {
                _gcHandle.Free();
                continuation();
            };

            _gcHandle = System.Runtime.InteropServices.GCHandle.Alloc(wrapper);
            return wrapper;
        }
    }
}


更新了 ,这是一个真实的Win32互操作示例,说明了保持 async 状态机存活的重要性 如果 GCHandle.Alloc(tcs) gch.Free() 行被注释掉,则发行版本将崩溃 无论是 callback tcs 已被固定为它正常工作。可替代地, await tcs.Task.WithStrongAwaiter() 可以利用以上内容来代替使用 StrongAwaiter

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    public class Program
    {
        static async Task TestAsync()
        {
            var tcs = new TaskCompletionSource<bool>();

            WaitOrTimerCallbackProc callback = (a, b) =>
                tcs.TrySetResult(true);

            //var gch = GCHandle.Alloc(tcs);
            try
            {
                IntPtr timerHandle;
                if (!CreateTimerQueueTimer(out timerHandle,
                        IntPtr.Zero,
                        callback,
                        IntPtr.Zero, 2000, 0, 0))
                    throw new System.ComponentModel.Win32Exception(
                        Marshal.GetLastWin32Error());

                await tcs.Task;
            }
            finally
            {
                //gch.Free();

                GC.KeepAlive(callback);
            }
        }

        public static void Main(string[] args)
        {
            var task = TestAsync();

            Task.Run(() =>
            {
                Thread.Sleep(500);
                Console.WriteLine("Starting GC");
                GC.Collect();
                GC.WaitForPendingFinalizers();
                Console.WriteLine("GC Done");
            });

            task.Wait();

            Console.WriteLine("completed!");
            Console.Read();
        }

        // p/invoke
        delegate void WaitOrTimerCallbackProc(IntPtr lpParameter, bool TimerOrWaitFired);

        [DllImport("kernel32.dll")]
        static extern bool CreateTimerQueueTimer(out IntPtr phNewTimer,
           IntPtr TimerQueue, WaitOrTimerCallbackProc Callback, IntPtr Parameter,
           uint DueTime, uint Period, uint Flags);
    }
}

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章

为什么我在尝试从网站收集价格时会收到 nonetype 错误?

为什么GC无法收集我的对象?

为什么我在使用 transduce 时会收到错误消息?

为什么我在使用 pytube 时会收到这些错误?

EXCEL VBA 用户定义函数 - 为什么我在定义它的工作簿中使用它时会收到错误“函数未定义”?

为什么我在开始任务时会收到成功代码,但它什么也没做?

Mapbox GL-JS:为什么我在工作时会收到图像加载错误?

为什么我的CSS Grid的祖先显示为flex时会收缩?

为什么我在运行此代码时会收到 AttributeError?

为什么我在使用相同的密钥时会收到密码学 fernet InvalidToken

为什么我在获取视频数据时会收到错误 404?

为什么我会收到这个错误,我该如何修复它

如果 ViewModel 持有这个 Activity 实现的接口引用,GC 会收集 Activity 引用吗?

有人可以帮助我,为什么我在创建新的撰写项目时会收到此错误?

为什么当我什么都不输入并按回车键时会收到 IndexError 错误?

为什么在为列表编制索引时会收到 IndexError?

为什么在创建/更新通知时会收到RemoteServiceException?

为什么在使用SAXParser时会收到“ MalformedURLException:无协议”?

为什么在使用ID的if比较时会收到错误声明

为什么初始化Spring时会收到NullPointerException

为什么在解析时会收到此错误?

为什么在进行本地搜索时会收到MKErrorDomain错误?

为什么在使用 Brightspace API 时会收到 403 响应?

为什么在尝试执行备份时会收到“代理错误”?

为什么在尝试 SELECT 时会收到错误消息?

为什么我会收到一个空对象引用?

为什么我会收到对“getche”的未定义引用错误?

c#:为什么GC无法在我的代码中收集Weakreference的目标?

为什么我在函数之外不需要某个值时会收到“借用的值寿命不够长”错误?