Ответ 1
Я удалил все материалы p/invoke и заново создал упрощенную версию логики конечного автомата, созданного компилятором. Он демонстрирует такое же поведение: awaiter
получает сбор гарабеков после первого вызова метода конечного автомата MoveNext
.
Недавно Microsoft сделала отличную работу по предоставлению веб-интерфейса своим .NET справочным источникам, что было очень полезно. Изучив реализацию 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'ed, а awaiter
останется в живых как часть этого. Конечно, это не решение, но оно иллюстрирует проблему.
Итак, я пришел к следующему выводу. В то время как асинхронная операция находится в режиме "в полете", и ни одно из состояний состояния машины (MoveNext
) в настоящее время не выполняется, ответственность "хранителя" продолжения обратного вызова, чтобы поставить сильная фиксация самого обратного вызова, чтобы убедиться, что копия конечного автомата в коробке не получает сбор мусора.
Например, в случае YieldAwaitable
(возвращается Task.Yield
), внешняя ссылка на обратный вызов продолжения сохраняется ThreadPool
планировщик задач, в результате вызова ThreadPool.QueueUserWorkItem
. В случае Task.GetAwaiter
это объект косвенно ссылается на объект задачи.
В моем случае "хранителем" обратного вызова продолжения является сам awaiter
.
Таким образом, до тех пор, пока нет внешних ссылок на обратный вызов продолжения, который CLR знает (вне объекта конечного автомата), пользовательский awaiter должен предпринять шаги для сохранения объекта обратного вызова. Это, в свою очередь, будет поддерживать всю государственную машину. В этом случае необходимы следующие шаги:
- Вызовите
GCHandle.Alloc
в обратном вызовеINotifyCompletion.OnCompleted
. - Вызов
GCHandle.Free
, когда событие async действительно произошло, перед вызовом обратного вызова продолжения. - Внесите
IDispose
, чтобы вызватьGCHandle.Free
, если событие никогда не происходило.
Учитывая, что ниже приведена версия исходного кода обратного вызова таймера, который работает правильно. Обратите внимание: нет необходимости сильно удерживать делегата обратного вызова таймера ( Обновлено: как указано в @svick, этот оператор может быть специфичен для текущей реализации конечного автомата (С# 5.0). Я добавил WaitOrTimerCallbackProc callback
). Он поддерживается как часть конечного автомата.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 OK to use lock (this)
// the C# compiler would never do this,
// because it 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);
}
}