Async / wait, awaiter personnalisé et collecteur d'ordures
je suis face à une situation où un objet géré est prématurément finalisé au milieu de async
méthode.
il s'agit d'un hobby home automation project (Windows 8.1, .NET 4.5.1), où je fournis Un C# callback à un tiers non géré DLL. Le rappel est invoqué lors d'un certain événement de détection.
Pour gérer l'événement, j'utilise async/await
et une simple coutume awaiter (plutôt que TaskCompletionSource
). Je le fais de cette façon, en partie pour réduire le nombre de inutiles les allocations, mais surtout par curiosité comme un exercice d'apprentissage.
ci-dessous est une version très dépouillée de ce que j'ai, en utilisant un timer-file d'attente Win32 pour simuler la source d'événement non gérée. Commençons par la sortie:
Press Enter to exit... Awaiter() tick: 0 tick: 1 ~Awaiter() tick: 2 tick: 3 tick: 4
notez comment mon serveur est finalisé après la seconde tique. C'est inattendu.
Le code (une application console):
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);
}
}
j'ai réussi à supprimer la collection de awaiter
avec cette ligne:
var hold = GCHandle.Alloc(awaiter);
cependant je ne comprends pas parfaitement pourquoi je dois créer une référence forte comme celle-ci. awaiter
est référencé à l'intérieur d'une boucle sans fin. AFAICT, il n'est pas hors de portée jusqu'à ce que la tâche retournée par TestAsync
est terminé (annulé/défectueux). Et la tâche elle-même est référencé à l'intérieur de pour toujours.
eventuellement, j'ai Reduit TestAsync
simplement ceci:
static async Task TestAsync()
{
var awaiter = new Awaiter();
//var hold = GCHandle.Alloc(awaiter);
var i = 0;
while (true)
{
await awaiter;
Console.WriteLine("tick: " + i++);
}
}
la collecte a toujours lieu. Je soupçonne que le tout l'objet machine d'état généré par le compilateur est collecté. quelqu'un peut-il expliquer pourquoi cela se produit?
Maintenant, avec la modification mineure, le awaiter
ne se fait plus ramasser les ordures:
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++);
}
}
mise à Jour,ce violon montre comment l' awaiter
l'objet est collecté sans aucun code P / invoke. Je pense, la raison pourrait être que il n'y a pas de externe références awaiter
en dehors de l'état initial de l'objet machine d'état généré. Je dois étudier le code généré par le compilateur.
mise à Jour voici le code généré par le compilateur (pour ce violon, VS2012). Apparemment, le Task
retournée par stateMachine.t__builder.Task
ne conserve pas de référence (ou plutôt une copie) à la machine d'état elle-même (stateMachine
). Ai-je raté quelque chose?
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 réponses
j'ai supprimé tous les éléments P/invoke et recréé une version simplifiée de la logique de la machine d'état générée par le compilateur. Il présente le même comportement:awaiter
obtient garabage recueillies après la première invocation de l'état de la machine MoveNext
méthode.
Microsoft a récemment fait un excellent travail sur la fourniture de L'interface web à leur . net sources de référence, qui a été très utile. Après avoir étudié la mise en œuvre de AsyncTaskMethodBuilder
et, la plupart important, AsyncMethodBuilderCore.GetCompletionAction
, je le crois maintenant le comportement du GC, je vais voir a le sens parfait. Je vais essayer d'expliquer que ci-dessous.
le code:
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);
}
}
}
}
la machine d'état générée par le compilateur est une structure mutable, étant dépassée par ref
. Apparemment, c'est une optimisation pour éviter les affectations supplémentaires.
La partie centrale de ce qui est de prendre place à l'intérieur de AsyncMethodBuilderCore.GetCompletionAction
, où la structure de la machine de l'état actuel est la référence à la copie boxée est conservée par le rappel de continuation passé à INotifyCompletion.OnCompleted
.
C'est la seule référence à la machine d'état qui a une chance de supporter le GC et de survivre après await
. Task
objet renvoyé par TestAsync
contenir une référence à elle, uniquement de la await
poursuite de rappel. Je crois que c'est fait exprès, pour préserver le comportement efficace du GC.
noter le commentaire ligne:
// s_strongRef = stateMachine;
si je le dés-commente, la copie boxée de la machine d'état n'est pas GC'ED, et awaiter
reste en vie comme une partie d'elle. Bien sûr, ce n'est pas une solution, mais cela illustre le problème.
donc, j'en suis arrivé à la conclusion suivante. Alors qu'une opération async est en "vol" et qu'aucun des états de la machine d'état (MoveNext
) est actuellement en cours d'exécution, c'est la responsabilité de la "gardien" de la poursuite de rappel pour mettre un forte emprise sur le rappel lui-même, pour s'assurer que la copie en boite de la machine d'état ne reçoit pas les ordures.
par exemple, en cas de YieldAwaitable
(retournée par Task.Yield
), la référence externe au rappel de continuation est conservée par le ThreadPool
planificateur de tâches, résultat de ThreadPool.QueueUserWorkItem
appel. Dans le cas où Task.GetAwaiter
, c'est indirectement référencés par l'objet tâche.
Dans mon cas, le "gardien" de la poursuite le rappel est le Awaiter
lui-même.
ainsi, tant qu'il n'y a pas de référence externe à la continuation de l'objet callback que le CLR connaît (en dehors de l'état de l'objet machine), l'awaiter personnalisé devrait prendre des mesures pour garder l'objet callback en vie. Cela, à son tour, maintiendrait en vie toute la machine d'état. Les étapes suivantes seront nécessaires dans ce cas:
- appelez le
GCHandle.Alloc
sur le rappel surINotifyCompletion.OnCompleted
. - Appel
GCHandle.Free
quand l'async l'événement s'est réellement produit, avant d'invoquer le rappel de continuation. - implémenter
IDispose
appelerGCHandle.Free
si l'événement n'est jamais arrivé.
étant donné que, ci-dessous est une version du code de rappel de minuterie original, qui fonctionne correctement. notez qu'il n'est pas nécessaire de mettre un hold-up important sur le délégué de callback timer (mise à Jour: comme le souligne @svick, ce déclaration peut être spécifique à la mise en place de la machine d'état (C# 5.0). J'ai ajouté WaitOrTimerCallbackProc callback
). Il est maintenu en vie comme une partie de la machine d'état.GC.KeepAlive(callback)
pour éliminer toute dépendance à ce comportement, au cas où il changerait dans les futures versions du compilateur.
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);
}
}