【发布时间】: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