2010-09-28 18 views
7

Con la logica di business incapsulato dietro sincrono servizio chiamate ad es .:Strategie per chiamare il servizio sincrono chiamate in modo asincrono in C#

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

Qual è il modo migliore per estendere/utilizzare queste chiamate di servizio in un asincrono moda?

Attualmente ho creato una semplice classe 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); 
      } 
     } 
    } 
} 

che mi permette di chiamare qualsiasi cosa in modo asincrono:

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

Mentre questo funziona in semplici casi di utilizzo diventa rapidamente difficile se altri processi è necessario coordinare i risultati, ad esempio se devo salvare tre oggetti in modo asincrono prima che il thread corrente possa continuare, quindi mi piacerebbe un modo per attendere/unire i thread worker.

Opzioni ho pensato finora includono:

  • hanno AsyncUtils restituire un WaitHandle
  • aventi AsyncUtils utilizzano un AsyncMethodCaller e restituiscono un IAsyncResult
  • riscrivere l'API per includere Begin, terminare le chiamate asincrone

es. qualcosa di simile:

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

Ci sono altri approcci che dovrei prendere in considerazione? Quali sono i vantaggi e le potenziali insidie ​​di ciascuno?

Idealmente mi piacerebbe mantenere il livello di servizio semplice/sincrono e fornire alcuni metodi di utilità facili da usare per chiamarli in modo asincrono. Sarei interessato a conoscere soluzioni e idee applicabili a C# 3.5 e C# 4 (non abbiamo ancora aggiornato, ma lo faremo nel prossimo futuro).

In attesa di vostre idee.

+3

Perché non si utilizza la Libreria parallela attività? E 'stato fatto per questo. http://msdn.microsoft.com/en-us/library/dd460717.aspx –

+0

Ciò che John ha detto, ma in particolare, prova "Lazy ". –

+1

@Steven: dovrebbe essere l'attività , per questo tipo di operazione. Lazy è per l'inizializzazione. –

risposta

3

Dato il tuo requisito per rimanere solo su .NET 2.0 e non funziona su 3.5 o 4.0, questa è probabilmente l'opzione migliore.

Ho tre osservazioni sulla vostra attuale implementazione.

  1. C'è un motivo specifico per cui si sta utilizzando ThreadPool.UnsafeQueueUserWorkItem? A meno che non sia necessario un motivo specifico, ti consigliamo di utilizzare ThreadPool.QueueUserWorkItem, soprattutto se sei in un grande team di sviluppo. La versione Unsafe può potenzialmente consentire la visualizzazione di falle di sicurezza man mano che si perde lo stack chiamante e, di conseguenza, la capacità di controllare le autorizzazioni più da vicino.

  2. Il disegno corrente della gestione delle eccezioni, utilizzando lo failureCallback, ingerisce tutte le eccezioni e non fornisce alcun feedback, a meno che non sia definita una richiamata. Potrebbe essere meglio propogare l'eccezione e lasciarla nascere se non la gestirai correttamente. In alternativa, è possibile reinserirlo nel thread chiamante in qualche modo, sebbene ciò richiederebbe l'utilizzo di qualcosa di più come IAsyncResult.

  3. Attualmente non è possibile stabilire se una chiamata asincrona è stata completata. Questo sarebbe l'altro vantaggio dell'uso di IAsyncResult nella progettazione (anche se aggiunge una certa complessità all'implementazione).


Una volta che l'aggiornamento a .NET 4, invece, consiglio solo mettendo questo in un Task o Task<T>, come è stato progettato per gestire questo molto pulita. Invece di:

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

È possibile utilizzare gli strumenti incorporati e solo scrivere:

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

Certo, l'ultima riga v'è un po 'strano, ma questo è soprattutto perché ho cercato di mantenere il vostro attuale API. Se lo hai rielaborato un po ', puoi semplificarlo ...

+0

Grazie Reed, stiamo attualmente utilizzando 3.5 ma sperando di passare a 4. Il tuo consiglio sarebbe diverso sapendo questo? – chillitom

+1

@Chillitom: Se è possibile farlo, otterrei il framework Rx (per .NET 3.5), poiché include un backport del Task Parallel Lib da .NET 4. Potresti quindi utilizzare Task/Task e ottenere tutto dei vantaggi che forniscono. Si tratta di un download gratuito all'indirizzo: http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx –

+0

@Chillitom: inserisco una versione TPL del tuo codice - non è abbastanza succinta (anche se potresti facilmente fare un metodo per fare ciò che fai), ma utilizza tutti gli strumenti del framework, in più ti offre tutti i vantaggi di poter aspettare (task.Wait()), eseguire una query per il completamento e tralasciare le parti che non ti servono ... –

2

L'interfaccia asincrona (basata su IAsyncResult) è utile solo quando hai qualche chiamata non bloccante sotto la copertina. Il punto principale dell'interfaccia è di rendere possibile effettuare la chiamata senza bloccare il thread del chiamante.

  • Questo è utile in scenari in cui si può fare qualche chiamata di sistema e il sistema avviserà indietro quando succede qualcosa (per esempio quando si riceve una risposta HTTP o quando si verifica un evento).

  • Il prezzo per l'utilizzo dell'interfaccia basata su IAsyncResult è che è necessario scrivere codice in un modo un po 'imbarazzante (effettuando ogni chiamata utilizzando la richiamata). Ancora peggio, l'API asincrona rende impossibile l'utilizzo di costrutti di linguaggio standard come while, for o try .. catch.

io non vedo proprio il punto di avvolgere sincrono API in asincrona interfaccia, in quanto non sarà possibile ottenere il beneficio (ci sarà sempre un po 'di filo bloccato) e devi semplicemente ottenere modo più imbarazzante di chiamarlo.

Ovviamente, ha perfettamente senso eseguire il codice sincrono su un thread in background in qualche modo (per evitare di bloccare il thread dell'applicazione principale). O usando Task<T> su .NET 4.0 o usando QueueUserWorkItem su .NET 2.0. Tuttavia, non sono sicuro se questo dovrebbe essere fatto automaticamente nel servizio - sembra che farlo dal lato del chiamante sarebbe più facile, perché potrebbe essere necessario eseguire più chiamate al servizio. Utilizzando API asincrona, si sarebbe dovuto scrivere qualcosa di simile:

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

Quando si utilizza API sincrono, devi scrivere qualcosa come:

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

"Non vedo il punto di avvolgere l'API sincrona in un'interfaccia asincrona," Se il tuo wrapper spinge il lavoro su un thread in background, utilizzando IAsyncResult e lo schema asincrono standard puoi darti un modo per verificare se l'operazione ha completato senza blocco. Ci sono dei motivi per farlo, dato che stai rendendo l'API asincrona. –

+1

@Reed: hai ragione che controllare se l'operazione è stata completata è un vantaggio dell'uso di 'IAsyncresult', ma penso che gli svantaggi del modello (programmazione basata su callback) siano ancora più significativi. –

+0

A meno che non si possa usare .NET 4, tuttavia, spesso non ci sono alternative migliori. IAsyncResult è davvero l'unico modo in .NET 2.0 per darti un modo per dire se un'operazione asincrona è completa e anche per propagare gli errori al thread chiamante ... –

1

La seguente è una risposta alla domanda di follow-up di Reed . Non sto suggerendo che sia la strada giusta da percorrere.

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

Probabilmente, l'utilizzo di Lazy potrebbe avere più senso se l'implementazione si trovava sulla coda del pool di thread, non su Task. Il compito è già troppo pulito per trarre vantaggio da ulteriori processi. –

+1

@Steven: il problema con Lazy è che si blocca non appena si tenta di ottenere il valore. A questo punto, l'attività fa esattamente la stessa cosa (oltre a fornire altri vantaggi) ... Lazy è davvero lì solo per un'istanziazione lazy sicura in un ambiente multi-thread. –

+0

Ma grazie per avermi fatto vedere cosa stavi pensando;) –

Problemi correlati