2010-09-28 9 views
7

Z logiki biznesowej obudowane za synchronicznego serwisu wymaga np .:Strategie wywołanie synchroniczne usługi wywołuje asynchronicznie w C#

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

Jaki jest najlepszy sposób na rozszerzenie/korzystania z tych zgłoszeń serwisowych w asynchronicznego mody?

Obecnie Stworzyłem prostą klasę 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); 
      } 
     } 
    } 
} 

który pozwala mi nazwać coś asynchronicznie:

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

Chociaż to działa w prostych przypadkach używać go szybko staje się trudne, jeśli innych procesów Muszę skoordynować wyniki, na przykład, jeśli potrzebuję zapisać asynchronicznie trzy obiekty, zanim bieżący wątek będzie mógł kontynuować, chciałbym mieć sposób oczekiwania na dołączenie do wątków roboczych.

Opcje myślałem od tej pory to:

  • o AsyncUtils zwrócić WaitHandle
  • o AsyncUtils użyć AsyncMethodCaller i zwracają IAsyncResult
  • przepisanie API obejmują Begin End połączeń async

np. coś podobnego:

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

Czy są inne podejścia, które powinienem rozważyć? Jakie są zalety i potencjalne pułapki każdego z nich?

Idealnie chciałbym, aby warstwa usługi była prosta/synchroniczna i zapewniała kilka łatwych w użyciu metod narzędziowych do wywoływania ich asynchronicznie. Chciałbym usłyszeć o rozwiązaniach i pomysłach mających zastosowanie do C# 3.5 i C# 4 (nie zaktualizowaliśmy jeszcze, ale zrobimy to w najbliższej przyszłości).

Czekam na Twoje pomysły.

+3

Dlaczego nie używasz biblioteki zadań? Został stworzony do tego. http://msdn.microsoft.com/en-us/library/dd460717.aspx –

+0

Co John powiedział, ale w szczególności, spróbuj 'Lazy '. –

+1

@Steven: To powinno być zadanie , dla tego typu operacji. Lazy służy do inicjowania. –

Odpowiedz

3

Biorąc pod uwagę Twoje wymagania dotyczące pobytu w .NET 2.0, a nie działają w wersji 3.5 lub 4.0, jest to prawdopodobnie najlepsza opcja.

Mam trzy uwagi na temat twojej obecnej realizacji.

  1. Czy istnieje szczególny powód, dla którego używasz ThreadPool.UnsafeQueueUserWorkItem? O ile nie jest to konieczne, zaleca się używanie ThreadPool.QueueUserWorkItem, zwłaszcza jeśli pracujesz w dużym zespole programistycznym. Wersja Unsafe może potencjalnie pozwolić na pojawienie się luk w zabezpieczeniach w wyniku utraty stosu wywołań, a co za tym idzie, możliwości kontrolowania uprawnień tak ściśle.

  2. Obecny projekt obsługi wyjątków, przy użyciu failureCallback, połknie wszystkie wyjątki i nie przekaże informacji zwrotnej, chyba że zdefiniowane jest wywołanie zwrotne. Lepiej może zaproponować wyjątek i pozwolić mu się upaść, jeśli nie będziesz go właściwie obsługiwał. Alternatywnie, możesz w ten sposób wcisnąć to z powrotem w wątek wywołujący, chociaż wymagałoby to użycia czegoś bardziej przypominającego IAsyncResult.

  3. Obecnie nie można określić, czy połączenie asynchroniczne zostało zakończone. Byłaby to kolejna zaleta korzystania z projektu IAsyncResult (choć nie jest to skomplikowane w implementacji).


Po uaktualnieniu do .NET 4, jednakże, polecam to tylko wprowadzenie w Task lub Task<T>, ponieważ został zaprojektowany do obsługi to bardzo czysto. Zamiast:

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

Można użyć wbudowanych narzędzi i po prostu napisać:

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

prawda, ostatni wiersz jest nieco dziwne, ale to głównie dlatego, że starałem się zachować istniejące API. Jeśli przerobisz go nieco, możesz go uprościć ...

+0

Dzięki aplikacji Reed używamy obecnie wersji 3.5, ale mamy nadzieję na uaktualnienie do wersji 4. Czy Twoja rada różni się od tego? – chillitom

+1

@Chillitom: Jeśli możesz to zrobić, dostanę strukturę Rx (dla .NET 3.5), ponieważ zawiera ona protokół zadania Parallel Lib z .NET 4. Możesz wtedy użyć zadania/zadania i uzyskać wszystkie korzyści, które zapewniają. Można go pobrać bezpłatnie pod adresem: http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx –

+0

@Chillitom: Wprowadziłem wersję kodu TPL - nie jest ona tak zwięzła (choć można ją łatwo zrobić metoda do robienia tego, co robisz), ale używa wszystkich narzędzi ramowych, a ponadto zapewnia wszystkie korzyści związane z możliwością oczekiwania (task.Wait()), zapytanie o zakończenie i pominięcie części, których nie potrzebujesz ... –

2

Interfejs asynchroniczny (oparty na IAsyncResult) jest przydatny tylko wtedy, gdy masz pod ręką nie blokujące połączenie. Głównym punktem interfejsu jest umożliwienie wykonania połączenia bez blokowania wątku wywołującego.

  • Funkcja ta jest przydatna w sytuacjach, kiedy można dokonać pewnych wywołań systemowych, a system powiadomi cię z powrotem, gdy coś się dzieje (np po odebraniu odpowiedzi HTTP lub gdy zdarzenie dzieje).

  • Cena za korzystanie z interfejsu opartego na IAsyncResult polega na tym, że trzeba napisać kod w nieco niezręczny sposób (wykonując każde połączenie za pomocą oddzwaniania). Co gorsza, asynchroniczny interfejs API uniemożliwia korzystanie ze standardowych konstrukcji językowych, takich jak while, for lub try .. catch.

ja naprawdę nie widzę sensu owijanie synchroniczny API do asynchroniczny interfejs, bo nie dostaniesz korzyści (nie zawsze będzie jakiś wątek zablokowany) i będziesz po prostu bardziej niezręczny sposób nazywania go.

Oczywiście ma sens, aby uruchomić kod synchroniczny na wątku tła (aby uniknąć zablokowania głównego wątku aplikacji). Albo przy użyciu Task<T> na .NET 4.0 lub przy użyciu QueueUserWorkItem na .NET 2.0. Nie jestem jednak pewien, czy powinno to być wykonywane automatycznie w usłudze - wydaje się, że wykonanie tej czynności po stronie osoby dzwoniącej byłoby łatwiejsze, ponieważ może być konieczne wykonanie wielu połączeń z usługą. Korzystanie z asynchronicznego API, trzeba by napisać coś takiego:

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

Podczas korzystania synchroniczny API, można by napisać coś takiego:

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

"Naprawdę nie widzę sensu pakowania synchronicznego interfejsu API w asynchroniczny interfejs" Jeśli twoje opakowanie popycha pracę do wątku tła, użycie IAsyncResult i standardowego asynchronicznego wzorca może dać ci sposób sprawdzenia, czy operacja ukończone bez blokowania. Istnieją powody, aby to zrobić, ponieważ skutecznie sprawiasz, że interfejs API jest asynchroniczny. –

+1

@Reed: Masz rację, że sprawdzenie, czy operacja zakończyła się, jest zaletą używania 'IAsyncresult', ale myślę, że wady schematu (programowanie oparte na oddzwanianiu) są jeszcze bardziej znaczące. –

+0

O ile nie można korzystać z platformy .NET 4, często nie ma lepszych alternatyw. IAsyncResult to naprawdę jedyny sposób w .NET 2.0, który daje ci możliwość sprawdzenia, czy operacja asynchroniczna jest kompletna, a także do propagowania błędów z powrotem do wątku wywołującego ... –

1

Poniżej znajduje się odpowiedź na kolejne pytanie Reeda . Nie sugeruję, że to właściwa droga.

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

Prawdopodobnie, używanie Lazy może mieć więcej sensu, jeśli implementacja była nad kolejką puli wątków, a nie Zadanie. Zadanie jest po prostu zbyt czyste, aby skorzystać z dalszego pakowania. –

+1

@Steven: Problem z Lazy polega na tym, że blokuje się, gdy tylko spróbujesz uzyskać wartość. W tym momencie zadanie robi dokładnie to samo (plus zapewnia inne korzyści) ... Lazy jest naprawdę po to, aby zapewnić bezpieczną instancję leniwą w środowisku wielowątkowym. –

+0

Ale dziękuję za umożliwienie mi zobaczenia, o czym myślałeś;) –

Powiązane problemy