【问题标题】:Async/await, custom awaiter and garbage collector异步/等待,自定义等待者和垃圾收集器
【发布时间】:2014-04-11 16:35:10
【问题描述】:

我正在处理一个托管对象在async 方法中过早完成的情况。

这是一个爱好家庭自动化项目(Windows 8.1、.NET 4.5.1),我在其中向非托管的第 3 方 DLL 提供 C# 回调。回调在某个传感器事件时被调用。

为了处理事件,我使用async/await 和一个简单的自定义等待者(而不是TaskCompletionSource)。我这样做部分是为了减少不必要的分配数量,但主要是出于学习练习的好奇心。

下面是我所拥有的一个非常精简的版本,使用 Win32 计时器队列计时器来模拟非托管事件源。让我们从输出开始:

按 Enter 退出... 服务员() 勾号:0 勾号:1 〜服务员() 勾号:2 勾号:3 勾号:4

请注意我的等待者是如何在第二次滴答后完成的。 这是意料之外的。

代码(控制台应用):

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

namespace ConsoleApplication
{
    class Program
    {
        static async Task TestAsync()
        {
            var awaiter = new Awaiter();
            //var hold = GCHandle.Alloc(awaiter);

            WaitOrTimerCallbackProc callback = (a, b) =>
                awaiter.Continue();

            IntPtr timerHandle;
            if (!CreateTimerQueueTimer(out timerHandle, 
                    IntPtr.Zero, 
                    callback, 
                    IntPtr.Zero, 500, 500, 0))
                throw new System.ComponentModel.Win32Exception(
                    Marshal.GetLastWin32Error());

            var i = 0;
            while (true)
            {
                await awaiter;
                Console.WriteLine("tick: " + i++);
            }
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Press Enter to exit...");
            var task = TestAsync();
            Thread.Sleep(1000);
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            Console.ReadLine();
        }

        // custom awaiter
        public class Awaiter : 
            System.Runtime.CompilerServices.INotifyCompletion
        {
            Action _continuation;

            public Awaiter()
            {
                Console.WriteLine("Awaiter()");
            }

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

            // resume after await, called upon external event
            public void Continue()
            {
                var continuation = Interlocked.Exchange(ref _continuation, null);
                if (continuation != null)
                    continuation();
            }

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

            public bool IsCompleted
            {
                get { return false; }
            }

            public void GetResult()
            {
            }

            // INotifyCompletion
            public void OnCompleted(Action continuation)
            {
                Volatile.Write(ref _continuation, continuation);
            }
        }

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

我设法用这一行禁止收集awaiter

var hold = GCHandle.Alloc(awaiter);

但是我不完全理解为什么我必须创建这样的强引用。 awaiter 在无限循环中被引用。 AFAICT,在TestAsync 返回的任务完成(取消/故障)之前,它不会超出范围。并且任务本身在 Main 中永远被引用。

最终,我将TestAsync 简化为:

static async Task TestAsync()
{
    var awaiter = new Awaiter();
    //var hold = GCHandle.Alloc(awaiter);

    var i = 0;
    while (true)
    {
        await awaiter;
        Console.WriteLine("tick: " + i++);
    }
}

收集仍在进行。我怀疑整个编译器生成的状态机对象正在被收集。 有人能解释一下为什么会这样吗?

现在,通过以下小的修改,awaiter 不再被垃圾收集:

static async Task TestAsync()
{
    var awaiter = new Awaiter();
    //var hold = GCHandle.Alloc(awaiter);

    var i = 0;
    while (true)
    {
        //await awaiter;
        await Task.Delay(500);
        Console.WriteLine("tick: " + i++);
    }
}

更新this fiddle 展示了 awaiter 对象如何在没有任何 p/invoke 代码的情况下进行垃圾收集。我认为,原因可能是没有外部引用awaiter在生成的状态机对象的初始状态之外。我需要研究编译器生成的代码。


更新,这是编译器生成的代码(针对this fiddle,VS2012)。显然,stateMachine.t__builder.Task 返回的 Task 没有保留对状态机本身 (stateMachine) 的引用(或者更确切地说,是其副本)。我错过了什么吗?

    private static Task TestAsync()
    {
      Program.TestAsyncd__0 stateMachine;
      stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
      stateMachine.1__state = -1;
      stateMachine.t__builder.Start<Program.TestAsyncd__0>(ref stateMachine);
      return stateMachine.t__builder.Task;
    }

    [CompilerGenerated]
    [StructLayout(LayoutKind.Auto)]
    private struct TestAsyncd__0 : IAsyncStateMachine
    {
      public int 1__state;
      public AsyncTaskMethodBuilder t__builder;
      public Program.Awaiter awaiter5__1;
      public int i5__2;
      private object u__awaiter3;
      private object t__stack;

      void IAsyncStateMachine.MoveNext()
      {
        try
        {
          bool flag = true;
          Program.Awaiter awaiter;
          switch (this.1__state)
          {
            case -3:
              goto label_7;
            case 0:
              awaiter = (Program.Awaiter) this.u__awaiter3;
              this.u__awaiter3 = (object) null;
              this.1__state = -1;
              break;
            default:
              this.awaiter5__1 = new Program.Awaiter();
              this.i5__2 = 0;
              goto label_5;
          }
label_4:
          awaiter.GetResult();
          Console.WriteLine("tick: " + (object) this.i5__2++);
label_5:
          awaiter = this.awaiter5__1.GetAwaiter();
          if (!awaiter.IsCompleted)
          {
            this.1__state = 0;
            this.u__awaiter3 = (object) awaiter;
            this.t__builder.AwaitOnCompleted<Program.Awaiter, Program.TestAsyncd__0>(ref awaiter, ref this);
            flag = false;
            return;
          }
          else
            goto label_4;
        }
        catch (Exception ex)
        {
          this.1__state = -2;
          this.t__builder.SetException(ex);
          return;
        }
label_7:
        this.1__state = -2;
        this.t__builder.SetResult();
      }

      [DebuggerHidden]
      void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
      {
        this.t__builder.SetStateMachine(param0);
      }
    }

【问题讨论】:

  • 你试过固定对象吗?
  • 我的意思是在你明确指定的地方使用重载:msdn.microsoft.com/fr-fr/library/1246yz8f(v=vs.110).aspx 你使用的那个是正常的,而不是固定的。原因很抱歉,但我真的说不出来。
  • 我在下面的回答似乎是一个可靠的理论......但我的实验现在真的开始让我失望了。我给了Awaiter 一个Guid 属性,我在每次调用Continue 时都将它打印到控制台......它完美地触发了......即使在终结器应该被触发之后。我真的很茫然……这似乎不可能……除非整个事情都被 JIT 编译器内联了。
  • GCHandle.Alloc() 在这里是必需的。但不是在 awaiter 上,您必须 保持 callback 委托对象处于活动状态。这反过来又确保等待者也保持活力。硬性要求,以及对非托管代码回调的非常普遍的需求,GC 不知道本机代码对其有隐式引用。
  • 仅供参考,我已将此问题提交给 The Bug Guys。希望 Eric Lippert 对这个问题有一些有趣的意见!

标签: c# .net garbage-collection task-parallel-library async-await


【解决方案1】:

我已删除所有 p/invoke 内容并重新创建了编译器生成的状态机逻辑的简化版本。它表现出相同的行为:在第一次调用状态机的 MoveNext 方法后,awaiter 被回收。

Microsoft 最近在为他们的.NET reference sources 提供 Web UI 方面做得非常出色,这非常有帮助。在研究了AsyncTaskMethodBuilder 以及最重要的AsyncMethodBuilderCore.GetCompletionAction 的实现之后,我现在相信我看到的GC 行为非常合理。我将在下面尝试解释。

代码:

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

namespace ConsoleApplication
{
    public class Program
    {
        // Original version with async/await

        /*
        static async Task TestAsync()
        {
            Console.WriteLine("Enter TestAsync");
            var awaiter = new Awaiter();
            //var hold = GCHandle.Alloc(awaiter);

            var i = 0;
            while (true)
            {
                await awaiter;
                Console.WriteLine("tick: " + i++);
            }
            Console.WriteLine("Exit TestAsync");
        }
        */

        // Manually coded state machine version

        struct StateMachine: IAsyncStateMachine
        {
            public int _state;
            public Awaiter _awaiter;
            public AsyncTaskMethodBuilder _builder;

            public void MoveNext()
            {
                Console.WriteLine("StateMachine.MoveNext, state: " + this._state);
                switch (this._state)
                {
                    case -1:
                        {
                            this._awaiter = new Awaiter();
                            goto case 0;
                        };
                    case 0:
                        {
                            this._state = 0;
                            var awaiter = this._awaiter;
                            this._builder.AwaitOnCompleted(ref awaiter, ref this);
                            return;
                        };

                    default:
                        throw new InvalidOperationException();
                }
            }

            public void SetStateMachine(IAsyncStateMachine stateMachine)
            {
                Console.WriteLine("StateMachine.SetStateMachine, state: " + this._state);
                this._builder.SetStateMachine(stateMachine);
                // s_strongRef = stateMachine;
            }

            static object s_strongRef = null;
        }

        static Task TestAsync()
        {
            StateMachine stateMachine = new StateMachine();
            stateMachine._state = -1;

            stateMachine._builder = AsyncTaskMethodBuilder.Create();
            stateMachine._builder.Start(ref stateMachine);

            return stateMachine._builder.Task;
        }

        public static void Main(string[] args)
        {
            var task = TestAsync();
            Thread.Sleep(1000);
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            Console.WriteLine("Press Enter to exit...");
            Console.ReadLine();
        }

        // custom awaiter
        public class Awaiter :
            System.Runtime.CompilerServices.INotifyCompletion
        {
            Action _continuation;

            public Awaiter()
            {
                Console.WriteLine("Awaiter()");
            }

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

            // resume after await, called upon external event
            public void Continue()
            {
                var continuation = Interlocked.Exchange(ref _continuation, null);
                if (continuation != null)
                    continuation();
            }

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

            public bool IsCompleted
            {
                get { return false; }
            }

            public void GetResult()
            {
            }

            // INotifyCompletion
            public void OnCompleted(Action continuation)
            {
                Console.WriteLine("Awaiter.OnCompleted");
                Volatile.Write(ref _continuation, continuation);
            }
        }
    }
}

编译器生成的状态机是一个可变结构,由ref 传递。显然,这是一种避免额外分配的优化。

其中的核心部分发生在 AsyncMethodBuilderCore.GetCompletionAction 内部,当前状态机结构被装箱,对装箱副本的引用由传递给 INotifyCompletion.OnCompleted 的延续回调保留。

这是对状态机的唯一引用,它有机会在await 之后经受住 GC 并存活下来。 TestAsync 返回的 Task 对象 持有对它的引用,只有 await 延续回调持有。我相信这是故意这样做的,以保持高效的 GC 行为。

注意注释行:

// s_strongRef = stateMachine;

如果我取消注释,状态机的盒装副本不会被 GC'ed,awaiter 作为其中的一部分保持活跃。当然,这不是一个解决方案,但它说明了问题。

所以,我得出以下结论。当异步操作处于“进行中”并且当前没有任何状态机状态 (MoveNext) 正在执行时,继续回调的“守卫者”有责任放置一个强烈保留回调本身,以确保状态机的盒装副本不会被垃圾回收。

例如,对于YieldAwaitable(由Task.Yield 返回),对延续回调的外部引用由ThreadPool 任务调度程序保留,这是ThreadPool.QueueUserWorkItem 调用的结果。如果是Task.GetAwaiter,则任务对象为indirectly referenced

就我而言,延续回调的“守护者”是Awaiter 本身。

因此,只要没有对 CLR 知道的延续回调的外部引用(在状态机对象之外),自定义等待程序就应该采取措施使回调对象保持活动状态。反过来,这将使整个状态机保持活力。在这种情况下,需要执行以下步骤:

  1. INotifyCompletion.OnCompleted 的回调上调用GCHandle.Alloc
  2. 在异步事件实际发生时调用GCHandle.Free,然后再调用延续回调。
  3. 如果事件从未发生过,则实现 IDispose 以调用 GCHandle.Free

鉴于此,下面是原始计时器回调代码的一个版本,它可以正常工作。 注意,没有必要对计时器回调委托 (WaitOrTimerCallbackProc callback) 进行严格控制。它作为状态机的一部分保持活动状态。 更新:正如@svick 所指出的,此语句可能特定于 当前 的实现状态机 (C# 5.0)。我添加了GC.KeepAlive(callback) 以消除对这种行为的任何依赖,以防它在未来的编译器版本中发生变化。

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

namespace ConsoleApplication
{
    class Program
    {
        // Test task
        static async Task TestAsync(CancellationToken token)
        {
            using (var awaiter = new Awaiter())
            {
                WaitOrTimerCallbackProc callback = (a, b) =>
                    awaiter.Continue();
                try
                {
                    IntPtr timerHandle;
                    if (!CreateTimerQueueTimer(out timerHandle,
                            IntPtr.Zero,
                            callback,
                            IntPtr.Zero, 500, 500, 0))
                        throw new System.ComponentModel.Win32Exception(
                            Marshal.GetLastWin32Error());
                    try
                    {
                        var i = 0;
                        while (true)
                        {
                            token.ThrowIfCancellationRequested();
                            await awaiter;
                            Console.WriteLine("tick: " + i++);
                        }
                    }
                    finally
                    {
                        DeleteTimerQueueTimer(IntPtr.Zero, timerHandle, IntPtr.Zero);
                    }
                }
                finally
                {
                    // reference the callback at the end
                    // to avoid a chance for it to be GC'ed
                    GC.KeepAlive(callback);
                }
            }
        }

        // Entry point
        static void Main(string[] args)
        {
            // cancel in 3s
            var testTask = TestAsync(new CancellationTokenSource(10 * 1000).Token);

            Thread.Sleep(1000);
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);

            Thread.Sleep(2000);
            Console.WriteLine("Press Enter to GC...");
            Console.ReadLine();

            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            Console.WriteLine("Press Enter to exit...");
            Console.ReadLine();
        }

        // Custom awaiter
        public class Awaiter :
            System.Runtime.CompilerServices.INotifyCompletion,
            IDisposable
        {
            Action _continuation;
            GCHandle _hold = new GCHandle();

            public Awaiter()
            {
                Console.WriteLine("Awaiter()");
            }

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

            void ReleaseHold()
            {
                if (_hold.IsAllocated)
                    _hold.Free();
            }

            // resume after await, called upon external event
            public void Continue()
            {
                Action continuation;

                // it's OK to use lock (this)
                // the C# compiler would never do this,
                // because it's slated to work with struct awaiters
                lock (this)
                {
                    continuation = _continuation;
                    _continuation = null;
                    ReleaseHold();
                }

                if (continuation != null)
                    continuation();
            }

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

            public bool IsCompleted
            {
                get { return false; }
            }

            public void GetResult()
            {
            }

            // INotifyCompletion
            public void OnCompleted(Action continuation)
            {
                lock (this)
                {
                    ReleaseHold();
                    _continuation = continuation;
                    _hold = GCHandle.Alloc(_continuation);
                }
            }

            // IDispose
            public void Dispose()
            {
                lock (this)
                {
                    _continuation = null;
                    ReleaseHold();
                }
            }
        }

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

        [DllImport("kernel32.dll")]
        static extern bool DeleteTimerQueueTimer(IntPtr TimerQueue, IntPtr Timer,
            IntPtr CompletionEvent);
    }
}

【讨论】:

  • 非常好!我确实注意到状态机中的unbox.any 操作码,但并没有想太多。因为它是由 C# 编译器为所有内容输出的。不错的工作伙伴。
  • “注意,没有必要对定时器回调委托进行严格的控制” 我认为你不应该依赖这个。状态机当然不能保证为你做这件事。
  • @Noseratio 是的,但是您依赖于编译器如何准确地生成状态机,并且编译器的未来/替代版本可能会以不同的方式进行。而且我认为规范不要求该对象保持活动状态(就像在某个点之后未使用的任何局部变量一样)。所以,我认为你根本不应该依赖这种行为。
  • @Noseratio 不保证被 GCed。你是对的,它不会与当前的 C# 编译器一起使用,因为本地将被转换为状态机类型上的一个字段。但编译器不必这样做。
  • @Noseratio 第二次阅读这个问题和答案,这绝对是惊人的。一个为async-await pantheon!
猜你喜欢
  • 2016-07-07
  • 2016-03-25
  • 2017-10-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多