我正在处理一种情况,即托管对象在async
方法的中间过早完成。
这是一个业余爱好家庭自动化项目(Windows 8.1,.NET 4.5.1),在该项目中,我向非托管的第三方DLL提供了C#回调。回调在某个传感器事件时被调用。
为了处理事件,我使用了async/await
和一个简单的自定义等待器(而不是TaskCompletionSource
)。我这样做是部分地减少了不必要的分配,但是出于好奇,这主要是出于学习的目的。
下面是我所拥有的非常剥离的版本,使用Win32计时器队列计时器模拟非托管事件源。让我们从输出开始:
Press Enter to exit... Awaiter() tick: 0 tick: 1 ~Awaiter() tick: 2 tick: 3 tick: 4
Note how my awaiter gets finalized after the second tick. This is unexpected.
The code (a console app):
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显示了如何在没有任何p/调用代码的情况下对
awaiter
对象进行垃圾回收。我认为,原因可能是在生成的状态机对象的初始状态之外没有对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);
}
}
最佳答案
我删除了所有p/invoke内容,并重新创建了编译器生成的状态机逻辑的简化版本。它表现出相同的行为:第一次调用状态机的awaiter
方法后,就会对MoveNext
进行垃圾回收。
微软最近在为.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
内部,将当前状态机struct装箱,并通过传递给INotifyCompletion.OnCompleted
的继续回调保留对装箱副本的引用。这是对状态机的唯一引用,它有机会经受住GC并在
await
之后存活。 Task
返回的TestAsync
对象不会对进行引用,而会保留对它的引用,只有await
延续回调可以。我相信这样做是有意的,以保持有效的GC行为。注意注释行:
// s_strongRef = stateMachine;
如果我取消注释,则状态机的盒装副本不会被GC处理,并且
awaiter
作为其一部分仍然有效。当然,这不是解决方案,但可以说明问题。因此,我得出以下结论。当异步操作处于“进行中”状态且当前未执行任何状态机状态(
MoveNext
)时,继续回调的“守护者”的职责是来对回调本身进行严格控制,确保装箱的状态机副本不会被垃圾收集。例如,对于
YieldAwaitable
(由Task.Yield
返回),由于ThreadPool
调用,对继续回调的外部引用由ThreadPool.QueueUserWorkItem
任务调度程序保留。如果使用Task.GetAwaiter
,则任务对象为indirectly referenced。就我而言,继续回调的“守护者”就是
Awaiter
本身。因此,只要没有外部引用到CLR知道的连续回调(在状态机对象之外),定制等待者就应采取措施使回调对象保持 Activity 状态。反过来,这将使整个状态机保持 Activity 状态。在这种情况下,必须执行以下步骤:
GCHandle.Alloc
在回调函数上调用INotifyCompletion.OnCompleted
。 GCHandle.Free
。 IDispose
来调用GCHandle.Free
。 鉴于此,下面是原始计时器回调代码的一个版本,该版本可以正常工作。请注意,无需牢记计时器回调委托(delegate)(
WaitOrTimerCallbackProc callback
)。它作为状态机的一部分保持 Activity 状态。 更新了:如@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);
}
}