异步/等待,自定义awaiter和垃圾收集器

时间:2014-03-08 11:17:14

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

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

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

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

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

Press Enter to exit...
Awaiter()
tick: 0
tick: 1
~Awaiter()
tick: 2
tick: 3
tick: 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)。显然,Task返回的stateMachine.t__builder.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);
      }
    }

1 个答案:

答案 0 :(得分:14)

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

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的延续回调保存。

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

注意注释行:

// s_strongRef = stateMachine;

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

所以,我得出以下结论。虽然异步操作处于“正在进行中”并且状态机的状态(MoveNext)当前都没有被执行,但是继续回调“守护者”的责任强烈控制回调本身,以确保状态机的盒装副本不会被垃圾收集。

例如,在YieldAwaitable(由Task.Yield返回)的情况下,ThreadPool任务调度程序保留对延续回调的外部引用,作为{{1的结果调用。对于ThreadPool.QueueUserWorkItem,任务对象为indirectly referenced

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

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

  1. Awaiter上回拨GCHandle.Alloc
  2. 在异步事件实际发生之前调用INotifyCompletion.OnCompleted,然后再调用延续回调。
  3. 如果事件从未发生过,请实施GCHandle.Free以致电IDispose
  4. 鉴于此,下面是原始计时器回调代码的一个版本,它可以正常工作。 注意,没有必要强烈保留计时器回调委托(GCHandle.Free)。它作为状态机的一部分保持活动状态。 更新:正如@svick所指出的,此声明可能特定于当前实现的状态机(C#5.0)。我已添加WaitOrTimerCallbackProc callback以消除对此行为的任何依赖,以防它在将来的编译器版本中发生更改。

    GC.KeepAlive(callback)