operador method espaƱol ejemplo create await async c# .net garbage-collection task-parallel-library async-await

c# - method - Async/await, aula personalizada y recolector de basura



operador await c# (1)

Estoy lidiando con una situación donde un objeto administrado se finaliza prematuramente en el medio del método async .

Este es un proyecto hogareño de automatización del hogar (Windows 8.1, .NET 4.5.1), donde proporciono una devolución de llamada C # a una DLL de terceros no administrada. La devolución de llamada se invoca sobre un determinado evento del sensor.

Para manejar el evento, uso async/await await y un simple awaiter personalizado (en lugar de TaskCompletionSource ). Lo hago de esta manera, en parte para reducir el número de asignaciones innecesarias, pero principalmente por curiosidad como ejercicio de aprendizaje.

A continuación se muestra una versión muy depurada de lo que tengo, utilizando un temporizador de cola de temporizador Win32 para simular el origen de evento no administrado. Comencemos con la salida:

Press Enter to exit... Awaiter() tick: 0 tick: 1 ~Awaiter() tick: 2 tick: 3 tick: 4

Observe cómo mi awaiter se finaliza después del segundo tic. Esto es inesperado.

El código (una aplicación de consola):

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 suprimir la colección de awaiter con esta línea:

var hold = GCHandle.Alloc(awaiter);

Sin embargo, no entiendo completamente por qué tengo que crear una referencia fuerte como esta. El awaiter se hace referencia dentro de un bucle sin fin. AFAICT, no saldrá del alcance hasta que la tarea devuelta por TestAsync se complete (cancelada / en fallo). Y la tarea en sí está referenciada dentro de Main para siempre.

Eventualmente, reduje TestAsync a solo esto:

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 colección todavía tiene lugar. Sospecho que todo el objeto de máquina de estado generado por el compilador se está recopilando. ¿Puede alguien explicar por qué sucede esto?

Ahora, con la siguiente modificación menor, el awaiter ya no recibe basura:

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++); } }

Actualizado , este violín muestra cómo el objeto de awaiter se recoge basura sin ningún código p / invoke. Creo que la razón podría ser que no hay referencias externas a awaiter fuera del estado inicial del objeto de máquina de estado generado. Necesito estudiar el código generado por el compilador.

Actualizado , aquí está el código generado por el compilador (para este violín , VS2012). Aparentemente, la Task devuelta por stateMachine.t__builder.Task no mantiene una referencia (o más bien, una copia de) la propia máquina de estado ( stateMachine ). ¿Me estoy perdiendo de algo?

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); } }


Eliminé todo lo relacionado con p / invoke y volví a crear una versión simplificada de la lógica de máquina de estado generada por el compilador. Exhibe el mismo comportamiento: el awaiter obtiene un garabage recolectado después de la primera invocación del método MoveNext la máquina de estado.

Microsoft ha realizado recientemente un excelente trabajo al proporcionar la interfaz de usuario web a sus fuentes de referencia .NET , que ha sido muy útil. Después de estudiar la implementación de AsyncTaskMethodBuilder y, lo más importante, AsyncMethodBuilderCore.GetCompletionAction , ahora creo que el comportamiento del GC que estoy viendo tiene mucho sentido . Trataré de explicar eso a continuación.

El código:

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 máquina de estados generada por el compilador es una estructura mutable, que pasa por ref . Aparentemente, esta es una optimización para evitar asignaciones adicionales.

La parte principal de esto está teniendo lugar dentro de AsyncMethodBuilderCore.GetCompletionAction , donde la estructura de la máquina de estado actual queda encuadrada, y la referencia a la copia INotifyCompletion.OnCompleted se mantiene mediante la devolución de llamada de continuación pasada a INotifyCompletion.OnCompleted .

Esta es la única referencia a la máquina de estado que tiene la oportunidad de soportar el GC y sobrevivir después de await . El objeto Task devuelto por TestAsync no contiene una referencia a él, solo lo hace la devolución de llamada de continuación de await . Creo que esto se hace a propósito, para preservar el comportamiento eficiente del GC.

Tenga en cuenta la línea comentada:

// s_strongRef = stateMachine;

Si no lo comento, la copia encuadrada de la máquina de estado no recibe GC, y awaiter mantiene vivo como parte de ella. Por supuesto, esto no es una solución, pero ilustra el problema.

Entonces, he llegado a la siguiente conclusión. Mientras que una operación asíncrona está en "vuelo" y ninguno de los estados de la máquina de estado ( MoveNext ) se está ejecutando actualmente, es responsabilidad del "guardián" de la devolución de llamada de continuación mantener la retrollamada, hacer asegúrese de que la copia en caja de la máquina de estado no se recoja basura.

Por ejemplo, en el caso de YieldAwaitable (devuelto por Task.Yield ), el ThreadPool tareas ThreadPool mantiene la referencia externa a la devolución de llamada de continuación, como resultado de la llamada ThreadPool.QueueUserWorkItem . En el caso de Task.GetAwaiter , el objeto de tarea hace referencia indirectamente a él.

En mi caso, el "guardián" de la devolución de llamada de continuación es el propio Awaiter .

Por lo tanto, siempre que no haya referencias externas a la devolución de llamada de continuación que CLR tenga conocimiento (fuera del objeto de máquina de estado), el awaiter personalizado debe tomar medidas para mantener vivo el objeto de devolución de llamada. Esto, a su vez, mantendría viva toda la máquina de estado. Los siguientes pasos serían necesarios en este caso:

  1. Llame al GCHandle.Alloc en la devolución de llamada en INotifyCompletion.OnCompleted .
  2. Llame a GCHandle.Free cuando el evento asincrónico haya sucedido realmente, antes de invocar la devolución de llamada de continuación.
  3. Implementar IDispose llamar a GCHandle.Free si el evento nunca sucedió.

Dado que, a continuación, hay una versión del código de devolución de llamada del temporizador original, que funciona correctamente. Tenga en cuenta que no es necesario poner un fuerte control sobre el delegado de devolución de llamada del temporizador ( WaitOrTimerCallbackProc callback ). Se mantiene vivo como parte de la máquina de estado. Actualizado : como lo señala @svick, esta declaración puede ser específica de la implementación actual de la máquina de estado (C # 5.0). He agregado GC.KeepAlive(callback) para eliminar cualquier dependencia de este comportamiento, en caso de que cambie en las futuras versiones del compilador.

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); } }