Я имею дело с ситуацией, когда управляемый объект преждевременно завершается в середине метода async
.
Это проект домашней автоматизации хобби (Windows 8.1,.NET 4.5.1), где я предоставляю обратный вызов С# неуправляемой сторонней DLL. Обратный вызов активируется при определенном событии датчика.
Чтобы обработать событие, я использую async/await
и простой пользовательский awaiter (а не TaskCompletionSource
). Я делаю это таким образом частично, чтобы уменьшить количество ненужных ассигнований, но в основном из любопытства в качестве учебного упражнения.
Ниже приведена очень лишенная версии того, что у меня есть, с использованием таймера очереди таймера Win32 для имитации неуправляемого источника событий. Начните с вывода:
Press Enter to exit... Awaiter() tick: 0 tick: 1 ~Awaiter() tick: 2 tick: 3 tick: 4
Обратите внимание на то, как мой awaiter завершается после второго тика. Это неожиданно.
Код (консольное приложение):
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++);
}
}
Обновлено, эта скрипка показывает, как объект awaiter
получает сбор мусора без кода p/invoke. Я думаю, причина может заключаться в том, что нет внешних ссылок на awaiter
вне исходного состояния созданного объекта конечного автомата. Мне нужно изучить код, сгенерированный компилятором.
Обновлен, здесь код, сгенерированный компилятором (для эта скрипка, 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);
}
}