2010-05-06 14 views
12

Hace poco tiempo me encontré con la necesidad de un mecanismo seguro de "apagar y olvidar" para ejecutar el código de forma asincrónica.Tipo seguro de invocación de delegado asíncrono en C#

Idealmente, lo que yo quiero hacer es algo como:

var myAction = (Action)(() => Console.WriteLine("yada yada")); 
myAction.FireAndForget(); // async invocation 

Por desgracia, la elección obvia de llamar BeginInvoke() sin la correspondiente EndInvoke() no funciona - el resultado es una pérdida de recursos lenta (ya que la el estado de ejecución es retenido por el tiempo de ejecución y nunca se lanzó ... espera una llamada eventual al EndInvoke(). Tampoco puedo ejecutar el código en el grupo de subprocesos de .NET porque puede llevar mucho tiempo completarlo (se recomienda solo ejecuta un código relativamente efímero en el grupo de subprocesos): esto hace que sea imposible usar el ThreadPool.QueueUserWorkItem().

Inicialmente, solo necesitaba este comportamiento para los métodos cuya firma coincide con Action, Action<...> o Func<...>. Así que armé un conjunto de métodos de extensión (consulte la lista a continuación) que me permiten hacer esto sin tener que encontrar la fuga de recursos. Hay sobrecargas para cada versión de Action/Func.

Lamentablemente, ahora deseo transferir este código a .NET 4, donde el número de parámetros genéricos en Action y Func se ha incrementado sustancialmente. Antes de escribir un script T4 para generar estos, también esperaba encontrar una manera más simple y más elegante para hacer esto. Cualquier idea es bienvenida.

public static class AsyncExt 
{ 
    public static void FireAndForget(this Action action) 
    { 
     action.BeginInvoke(OnActionCompleted, action); 
    } 

    public static void FireAndForget<T1>(this Action<T1> action, T1 arg1) 
    { 
     action.BeginInvoke(arg1, OnActionCompleted<T1>, action); 
    } 

    public static void FireAndForget<T1,T2>(this Action<T1,T2> action, T1 arg1, T2 arg2) 
    { 
     action.BeginInvoke(arg1, arg2, OnActionCompleted<T1, T2>, action); 
    } 

    public static void FireAndForget<TResult>(this Func<TResult> func, TResult arg1) 
    { 
     func.BeginInvoke(OnFuncCompleted<TResult>, func); 
    } 

    public static void FireAndForget<T1,TResult>(this Func<T1, TResult> action, T1 arg1) 
    { 
     action.BeginInvoke(arg1, OnFuncCompleted<T1,TResult>, action); 
    } 

    // more overloads of FireAndForget<..>() for Action<..> and Func<..> 

    private static void OnActionCompleted(IAsyncResult result) 
    { 
     var action = (Action)result.AsyncState; 
     action.EndInvoke(result); 
    } 

    private static void OnActionCompleted<T1>(IAsyncResult result) 
    { 
     var action = (Action<T1>)result.AsyncState; 
     action.EndInvoke(result); 
    } 

    private static void OnActionCompleted<T1,T2>(IAsyncResult result) 
    { 
     var action = (Action<T1,T2>)result.AsyncState; 
     action.EndInvoke(result); 
    } 

    private static void OnFuncCompleted<TResult>(IAsyncResult result) 
    { 
     var func = (Func<TResult>)result.AsyncState; 
     func.EndInvoke(result); 
    } 

    private static void OnFuncCompleted<T1,TResult>(IAsyncResult result) 
    { 
     var func = (Func<T1, TResult>)result.AsyncState; 
     func.EndInvoke(result); 
    } 

    // more overloads of OnActionCompleted<> and OnFuncCompleted<> 

} 
+0

pregunta fresca. Código doloroso! Lo único que tienen en común es que todos ellos son MulticastDelegate. No estoy seguro de que vaya a haber una forma segura de hacer dieta con el código. ¿Cómo genera MS todo en primer lugar? – spender

+1

@spender: el compilador genera automáticamente los métodos 'BeginInvoke' /' EndInvoke' en cada tipo de delegado fuertemente tipado a los argumentos del delegado. – LBushkin

+0

Seguro. Me parece que fomenta un código repetitivo considerable, y me sorprende que .net4 no haya introducido una forma más genérica (o supergenérica) de declarar este tipo de construcciones ... aunque estaría de acuerdo con eso en la mayoría de los casos. código, las listas de parámetros no excederían de 5 o más de longitud. – spender

Respuesta

7

Puede pasar EndInvoke como AsyncCallback para BeginInvoke:

Action<byte[], int, int> action = // ... 

action.BeginInvoke(buffer, 0, buffer.Length, action.EndInvoke, null); 

ayuda eso?

+0

Esto fue algo que consideré ... la preocupación de otros desarrolladores con los que trabajo es que sería fácil olvidarse de pasar 'action.EndInvoke' - con el resultado de que habría fugas silenciosas dentro del código. Incluso pensamos en escribir una regla de FxCop personalizada para esto, pero tardaba demasiado y existía el riesgo de que el código aún pudiera convertirse en producción. Sin embargo, estoy considerando los méritos de esta opción nuevamente. – LBushkin

+0

Al menos puede simplificar un poco su implementación de FireAndForget. – dtb

+0

La versión real del código en realidad tiene un poco más de lógica en los métodos completados de OnAction/OnFunc que no quería saturar en la pregunta (principalmente algunos registros de cuánto tiempo llevó completar la llamada asincrónica) - pero, en principio, sí. – LBushkin

0

Ese inteligente tipo Skeet se acerca a este tema here.

Hay un enfoque diferente para "disparar y olvidar" a mitad de camino.

+0

Sí, he visto ese enfoque antes. El problema principal es que pierde la seguridad de tipo (y, por lo tanto, el soporte de intellisense y refactorización), pero admite más firmas de delegado. – LBushkin

3

También se llama al método BeginInvoke generado por el compilador en el grupo de subprocesos (reference). Así que creo que ThreadPool.QueueUserWorkItem estaría bien, excepto que estás siendo un poco más explícito al respecto, supongo (y supongo que un futuro CLR podría elegir ejecutar los métodos BeginInvoke 'ed en un conjunto de hilos diferente).

4

¿Qué tal algo como:

public static class FireAndForgetMethods 
{ 
    public static void FireAndForget<T>(this Action<T> act,T arg1) 
    { 
     var tsk = Task.Factory.StartNew(()=> act(arg1), 
             TaskCreationOptions.LongRunning); 
    } 
} 

usarlo como:

Action<int> foo = (t) => { Thread.Sleep(t); }; 
foo.FireAndForget(100); 

Para añadir la seguridad de tipos, simplemente ampliar los métodos de ayuda. T4 es probablemente el mejor aquí.

+0

¡Parece muy interesante! –

+0

ContinueWith (...) devuelve un segundo objeto Tarea, por lo que aún le queda una Tarea que no se elimina. De hecho, no es necesario deshacerse del objeto Tarea original en este caso, ya que la eliminación solo es necesaria si se requiere que la Tarea realice una espera de bloqueo. Ver [esta pregunta] (http://stackoverflow.com/questions/3734280/) –

+0

buen punto - corregirá –

4

noto nadie respondió a esta:

Asimismo, no puede ejecutar el código en el grupo de subprocesos .NET, ya que puede tomar un tiempo muy largo para completar (se aconseja que sólo se ejecutan relativamente corto código vivido en el grupo de subprocesos): esto hace que sea imposible utilizar ThreadPool.QueueUserWorkItem().

no estoy seguro de si eres consciente de ello, pero los delegados asincrónicos en realidad hacen exactamente esto - que hacen cola el trabajo en un subproceso de trabajo en el ThreadPool, exactamente igual que si lo hiciera QueueUserWorkItem.

El único momento en que los delegados asincrónicos se comportan de manera diferente es cuando son delegados especiales del marco como Stream.BeginRead o Socket.BeginSend. En su lugar, usan puertos de finalización de E/S.

A menos que esté haciendo girar cientos de estas tareas en un entorno ASP.NET, recomendaría simplemente usar el grupo de subprocesos.

ThreadPool.QueueUserWorkItem(s => action()); 

O, en .NET 4, puede utilizar la fábrica de tareas:

Task.Factory.StartNew(action); 

(Tenga en cuenta que lo anterior también utilizará el grupo de subprocesos!)

+0

De hecho, esto es un problema. Tendré que volver a examinar el enfoque dado este hecho. ¿Existe alguna documentación clara en MSDN donde se indique esto? – LBushkin

+0

@LBushkin: Sabes, esa es una muy buena pregunta, y no creo que * haya * ninguna documentación clara, se supone que es un detalle de implementación tal vez. Me enteré con certeza cuando hice algunos experimentos para la pregunta de otra persona sobre el hambre en ASP.NET ThreadPool; Entré en una devolución de llamada asincrónica y vi que efectivamente estaba ocupando un hilo de trabajo en el grupo. Si encuentras algún documento, avísame, sería bueno tener algo para vincular. – Aaronaught

+0

Sí, intenté encontrar una referencia para esto también y dibujé un espacio en blanco. – spender

0

Se puede escribir su propio implementación de threadpool. Probablemente suena como si llevara trabajo de lo que realmente sería. Entonces no tiene que cumplir con la recomendación "solo ejecutar código relativamente de corta duración".

0

dar a este método de extensión de un disparo (por C# Is action.BeginInvoke(action.EndInvoke,null) a good idea?) para asegurar que no hay fugas de memoria:

public static void FireAndForget(this Action action) 
{ 
    action.BeginInvoke(action.EndInvoke, null); 
} 

y se podía usar con parámetros genéricos como:

T1 param1 = someValue; 
T2 param2 = otherValue; 
(() => myFunc<T1,T2>(param1,param2)).FireAndForget(); 
Cuestiones relacionadas