2010-09-28 5 views
7

Con la lógica de negocio encapsulada detrás sincrónica llamadas de servicio ej .:Estrategias para llamar servicio síncrono llama de forma asincrónica en C#

interface IFooService 
{ 
    Foo GetFooById(int id); 
    int SaveFoo(Foo foo); 
} 

¿Cuál es la mejor manera de extender/uso de estas llamadas de servicio en un moda asíncrona?

En la actualidad he creado una clase simple AsyncUtils:

public static class AsyncUtils 
{ 
    public static void Execute<T>(Func<T> asyncFunc) 
    { 
     Execute(asyncFunc, null, null); 
    } 

    public static void Execute<T>(Func<T> asyncFunc, Action<T> successCallback) 
    { 
     Execute(asyncFunc, successCallback, null); 
    } 

    public static void Execute<T>(Func<T> asyncFunc, Action<T> successCallback, Action<Exception> failureCallback) 
    { 
     ThreadPool.UnsafeQueueUserWorkItem(state => ExecuteAndHandleError(asyncFunc, successCallback, failureCallback), null); 
    } 

    private static void ExecuteAndHandleError<T>(Func<T> asyncFunc, Action<T> successCallback, Action<Exception> failureCallback) 
    { 
     try 
     { 
      T result = asyncFunc(); 
      if (successCallback != null) 
      { 
       successCallback(result); 
      } 
     } 
     catch (Exception e) 
     { 
      if (failureCallback != null) 
      { 
       failureCallback(e); 
      } 
     } 
    } 
} 

que me permite llamar a cualquier cosa de forma asíncrona:

AsyncUtils(
    () => _fooService.SaveFoo(foo), 
    id => HandleFooSavedSuccessfully(id), 
    ex => HandleFooSaveError(ex)); 

Mientras que esto funciona en casos de uso sencillos que rápidamente se complica si otros procesos Necesito coordinar sobre los resultados, por ejemplo, si necesito guardar tres objetos de forma asincrónica antes de que el hilo actual pueda continuar, entonces me gustaría una manera de esperar/unir los hilos de trabajo.

Opciones He pensado hasta ahora incluyen:

  • tengan AsyncUtils vuelven a WaitHandle
  • tengan AsyncUtils utilizan un AsyncMethodCaller y devuelven un IAsyncResult
  • reescribir la API para incluir Begin, terminar llamadas asíncronas

por ejemplo algo parecido a:

interface IFooService 
{ 
    Foo GetFooById(int id); 
    IAsyncResult BeginGetFooById(int id); 
    Foo EndGetFooById(IAsyncResult result); 
    int SaveFoo(Foo foo); 
    IAsyncResult BeginSaveFoo(Foo foo); 
    int EndSaveFoo(IAsyncResult result); 
} 

¿Hay otros enfoques que debería considerar? ¿Cuáles son los beneficios y las posibles dificultades de cada uno?

Idealmente me gustaría mantener la capa de servicio simple/sincrónica y proporcionar algunos métodos de utilidad fáciles de usar para llamarlos de forma asíncrona. Me interesaría conocer las soluciones e ideas aplicables a C# 3.5 y C# 4 (no hemos actualizado aún pero lo haremos en un futuro relativamente próximo).

Esperamos sus ideas.

+3

¿Por qué no utiliza la Biblioteca de tareas paralelas? Fue hecho para esto. http://msdn.microsoft.com/en-us/library/dd460717.aspx –

+0

Lo que dijo John, pero en específico, prueba 'Lazy '. –

+1

@Steven: debe ser la tarea , para este tipo de operación. Lazy es para inicialización. –

Respuesta

3

Dado su requisito para permanecer .NET 2.0 solamente, y no trabajar en 3.5 o 4.0, esta es probablemente la mejor opción.

Tengo tres comentarios sobre su implementación actual.

  1. ¿Hay algún motivo específico por el que esté utilizando ThreadPool.UnsafeQueueUserWorkItem? A menos que exista una razón específica para esto, recomendaría utilizar ThreadPool.QueueUserWorkItem en su lugar, especialmente si está en un equipo grande de desarrollo. La versión insegura puede potencialmente permitir que aparezcan fallas de seguridad a medida que pierde la pila de llamadas y, como resultado, la capacidad de controlar los permisos tan de cerca.

  2. El diseño actual de su manejo de excepciones, usando el failureCallback, tragará todas las excepciones y no proporcionará comentarios, a menos que se defina una devolución de llamada. Puede ser mejor propocionar la excepción y dejarla burbujear si no la va a manejar correctamente. Alternativamente, puede volver a insertar esto en el hilo de llamada, aunque esto requeriría usar algo más como IAsyncResult.

  3. Actualmente no tiene forma de saber si se completa una llamada asincrónica. Esta sería la otra ventaja de usar IAsyncResult en su diseño (aunque agrega cierta complejidad a la implementación).


Una vez que actualiza a .NET 4, sin embargo, recomendaría sólo poner esto en un Task o Task<T>, ya que fue diseñado para manejar esto de forma muy limpia. En lugar de:

AsyncUtils(
    () => _fooService.SaveFoo(foo), 
    id => HandleFooSavedSuccessfully(id), 
    ex => HandleFooSaveError(ex)); 

Puede utilizar las herramientas integradas y acaba de escribir:

var task = Task.Factory.StartNew( 
       () => return _fooService.SaveFoo(foo)); 
task.ContinueWith( 
       t => HandleFooSavedSuccessfully(t.Result), 
        TaskContinuationOptions.NotOnFaulted); 
task.ContinueWith( 
       t => try { t.Wait(); } catch(Exception e) { HandleFooSaveError(e); }, 
        TaskContinuationOptions.OnlyOnFaulted); 

Por supuesto, la última línea no es un poco extraño, pero eso es principalmente porque traté de mantener su actual API. Si lo volvió a trabajar un poco, puede simplificarlo ...

+0

Gracias Reed, actualmente estamos usando 3.5 pero esperamos actualizar a 4. ¿Diferirían sus consejos sabiendo esto? – chillitom

+1

@Chillitom: Si puede hacerlo, obtendré el Rx framework (para .NET 3.5), ya que incluye un backport de la Task Parallel Lib de .NET 4. Luego podría usar Task/Task y obtener todo de las ventajas que proporcionan. Es una descarga gratuita en: http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx –

+0

@Chillitom: puse una versión TPL de tu código, no es tan breve (aunque podrías hacer fácilmente un método para hacer lo que haces), pero está usando todas las herramientas de framework, además de que te ofrece todas las ventajas de poder esperar (task.Wait()), consultar para completar y dejar de lado las partes que no necesitas ... –

2

La interfaz asincrónica (basada en IAsyncResult) es útil solo cuando tiene alguna llamada no bloqueada debajo de la cubierta. El punto principal de la interfaz es hacer posible la llamada sin bloquear el hilo de la persona que llama.

  • Esto es útil en situaciones cuando se puede hacer algo de llamada al sistema y el sistema le notificará atrás cuando pasa algo (por ejemplo, cuando se recibe una respuesta HTTP o cuando ocurre un evento).

  • El precio por usar la interfaz basada en IAsyncResult es que tiene que escribir el código de una manera un tanto incómoda (haciendo cada llamada usando la devolución de llamada). Peor aún, la API asíncrona hace que sea imposible utilizar construcciones de lenguaje estándar como while, for o try .. catch.

Realmente no veo el punto de envolver sincrónica API en asíncrono interfaz, porque no va a obtener el beneficio (siempre habrá un poco de hilo bloqueado) y vas a tener forma más incómoda de llamarlo.

Por supuesto, tiene un sentido perfecto para ejecutar el código síncrono en un hilo de fondo de alguna manera (para evitar el bloqueo del hilo de la aplicación principal). Ya sea usando Task<T> en .NET 4.0 o usando QueueUserWorkItem en .NET 2.0. Sin embargo, no estoy seguro de si esto debería hacerse automáticamente en el servicio; parece que hacer esto por el lado de la persona que llama sería más fácil, ya que es posible que necesite realizar varias llamadas al servicio. El uso de la API asíncrona, que tendría que escribir algo como:

svc.BeginGetFooId(ar1 => { 
    var foo = ar1.Result; 
    foo.Prop = 123; 
    svc.BeginSaveFoo(foo, ar2 => { 
    // etc... 
    } 
}); 

Cuando se utiliza la API síncrona, que iba a escribir algo como:

ThreadPool.QueueUserWorkItem(() => { 
    var foo = svc.GetFooId(); 
    foo.Prop = 123; 
    svc.SaveFoo(foo); 
}); 
+0

"Realmente no veo el punto de ajustar la API síncrona a la interfaz asíncrona", si su envoltorio empuja el trabajo hacia un hilo de fondo, usar IAsyncResult y el patrón asíncrono estándar puede darle una forma de verificar si la operación completado sin bloquear Hay razones para hacer esto, ya que efectivamente está haciendo que la API sea asíncrona. –

+1

@Reed: tiene razón al comprobar si la operación se completó es una ventaja de usar 'IAsyncresult', pero creo que las desventajas del patrón (programación basada en la devolución de llamada) son aún más significativas. –

+0

A menos que pueda usar .NET 4, sin embargo, a menudo no hay mejores alternativas. IAsyncResult es realmente la única forma en .NET 2.0 de proporcionarle una forma de saber si se completó una operación asíncrona, y también de proponer errores al hilo de llamada ... –

1

La siguiente es una respuesta a la pregunta de seguimiento de Reed . No estoy sugiriendo que sea la forma correcta de hacerlo.

public static int PerformSlowly(int id) 
    { 
     // Addition isn't so hard, but let's pretend. 
     Thread.Sleep(10000); 
     return 42 + id; 
    } 

    public static Task<int> PerformTask(int id) 
    { 
     // Here's the straightforward approach. 
     return Task.Factory.StartNew(() => PerformSlowly(id)); 
    } 

    public static Lazy<int> PerformLazily(int id) 
    { 
     // Start performing it now, but don't block. 
     var task = PerformTask(id); 

     // JIT for the value being checked, block and retrieve. 
     return new Lazy<int>(() => task.Result); 
    } 

    static void Main(string[] args) 
    { 
     int i; 

     // Start calculating the result, using a Lazy<int> as the future value. 
     var result = PerformLazily(7); 

     // Do assorted work, then get result. 
     i = result.Value; 

     // The alternative is to use the Task as the future value. 
     var task = PerformTask(7); 

     // Do assorted work, then get result. 
     i = task.Result; 
    } 
+0

Discutible, El uso de Lazy podría tener más sentido si la implementación se realizó a través de la cola del grupo de subprocesos, no de la tarea. La tarea ya está demasiado limpia como para beneficiarse de una nueva envoltura. –

+1

@Steven: El problema con Lazy es que bloquea tan pronto como intentas obtener el valor. En ese punto, la Tarea hace exactamente lo mismo (más proporciona otros beneficios) ... Lazy está realmente allí para la creación de instancias perezosas seguras en un entorno de subprocesos múltiples. –

+0

Pero gracias por dejarme ver lo que estabas pensando;) –

Cuestiones relacionadas