2010-09-28 5 views
7

Avec la logique métier encapsulées derrière synchrone appels de service par exemple .:Stratégies pour appeler le service synchrone appels de manière asynchrone en C#

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

Quelle est la meilleure façon d'étendre/utiliser ces appels de service de façon asynchrone?

À l'heure actuelle, j'ai créé une classe simple de 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); 
      } 
     } 
    } 
} 

Ce qui me permet d'appeler quoi que ce soit de manière asynchrone:

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

Bien que cela fonctionne dans les cas simples d'utilisation, il devient rapidement difficile si d'autres processus besoin de coordonner sur les résultats, par exemple si j'ai besoin d'enregistrer trois objets de manière asynchrone avant que le thread en cours puisse continuer alors je voudrais un moyen d'attendre/rejoindre les threads de travail.

Options J'ai pensé jusqu'à présent comprennent:

  • ayant AsyncUtils retour un WaitHandle
  • AsyncUtils ayant fait usage d'un AsyncMethodCaller et retourner un IAsyncResult
  • réécriture de l'API pour inclure Begin, les appels async End

par exemple quelque chose qui ressemble à:

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

Y a-t-il d'autres approches que je devrais envisager? Quels sont les avantages et les pièges potentiels de chacun?

Idéalement, je voudrais garder la couche de service simple/synchrone et fournir quelques méthodes utilitaires faciles à utiliser pour les appeler de manière asynchrone. Je serais intéressé d'entendre parler de solutions et d'idées applicables à C# 3.5 et C# 4 (nous ne l'avons pas encore amélioré mais nous le ferons dans un avenir relativement proche).

Dans l'attente de vos idées.

+3

Pourquoi n'utilisez-vous pas la bibliothèque parallèle de tâches? C'était fait pour ça. http://msdn.microsoft.com/en-us/library/dd460717.aspx –

+0

Ce que Jean dit, mais en particulier, essayez '' Lazy . –

+1

@Steven: Cela devrait être la tâche , pour ce type d'opération. Lazy est pour l'initialisation. –

Répondre

3

Compte tenu de votre exigence de rester .NET 2.0 seulement, et ne fonctionne pas sur 3.5 ou 4.0, c'est probablement la meilleure option.

J'ai trois remarques sur votre implémentation actuelle.

  1. Y at-il une raison particulière pour laquelle vous utilisez ThreadPool.UnsafeQueueUserWorkItem? À la place, il est recommandé d'utiliser ThreadPool.QueueUserWorkItem à la place, surtout si vous faites partie d'une grande équipe de développement. La version non sécurisée peut potentiellement laisser apparaître des failles de sécurité lorsque vous perdez la pile appelante, et par conséquent, la possibilité de contrôler les autorisations aussi étroitement. La conception actuelle de votre gestion des exceptions, utilisant le failureCallback, avale toutes les exceptions et ne fournit aucun retour, sauf si un rappel est défini. Il serait peut-être préférable de propager l'exception et de la laisser exploser si vous ne la manipulez pas correctement. Alternativement, vous pourriez repousser le fil appelant d'une certaine manière, mais cela nécessiterait d'utiliser quelque chose de plus comme IAsyncResult.

  2. Vous avez actuellement aucun moyen de savoir si un appel asynchrone est terminée. Ce serait l'autre avantage d'utiliser IAsyncResult dans votre conception (bien que cela ajoute de la complexité à l'implémentation).


Une fois que vous passez à 4 .NET, cependant, je recommanderais simplement mettre cela dans un Task ou Task<T>, comme il a été conçu pour gérer ce très propre. Au lieu de:

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

Vous pouvez utiliser les outils intégrés et il suffit d'écrire:

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

Certes, la dernière ligne il y a un peu bizarre, mais c'est surtout parce que j'ai essayé de garder votre existant API Si vous l'avez retravaillé un peu, vous pouvez le simplifier ...

+0

Merci Reed, nous utilisons actuellement 3,5 mais espérons passer à 4. Votre avis serait-il différent? – chillitom

+1

@Chillitom: Si vous pouvez le faire, j'obtiendrais le framework Rx (pour .NET 3.5), car il inclut un backport du Task Parallel Lib à partir de .NET 4. Vous pouvez alors utiliser Task/Task et obtenir tout des avantages qu'ils offrent. C'est un téléchargement gratuit à: http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx –

+0

@Chillitom: J'ai mis dans une version TPL de votre code - ce n'est pas tout à fait aussi succincte (même si vous pourriez facilement faire une méthode pour faire ce que vous faites), mais il utilise tous les outils du framework, en plus il vous donne tous les avantages d'être en mesure d'attendre (task.Wait()), d'effectuer des requêtes et d'omettre des parties inutiles ... –

2

L'interface asynchrone (basée sur IAsyncResult) n'est utile que si vous avez un appel non bloquant en couverture. Le point principal de l'interface est de permettre de faire l'appel sans bloquer le thread appelant.

  • Ceci est utile dans les scénarios où vous pouvez faire appel système certains et le système vous informe en arrière quand il arrive quelque chose (par exemple lorsqu'une réponse HTTP est reçue ou lorsqu'un événement se produit). Le prix pour l'utilisation de l'interface basée sur IAsyncResult est que vous devez écrire du code d'une manière quelque peu gênante (en effectuant chaque appel en utilisant le rappel). Pire encore, l'API asynchrone rend impossible d'utiliser des constructions de langage standards comme while, for ou try .. catch.

Je ne vois pas vraiment le point d'emballage API synchrone dans interface asynchrone, parce que vous n'obtiendrez l'avantage (il y aura toujours un fil bloqué) et vous aurez juste obtenir manière plus maladroite de l'appeler.

Bien sûr, il est un sens parfait pour exécuter le code synchrone sur un thread d'arrière-plan d'une certaine manière (pour éviter de bloquer le fil principal de l'application). Soit en utilisant Task<T> sur .NET 4.0 ou en utilisant QueueUserWorkItem sur .NET 2.0. Cependant, je ne suis pas sûr que cela devrait être fait automatiquement dans le service - il semble que cela soit plus facile du côté des appelants, car vous devrez peut-être effectuer plusieurs appels au service. En utilisant l'API asynchrone, vous devriez écrire quelque chose comme:

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

Lorsque vous utilisez l'API synchrone, vous devez écrire quelque chose comme:

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

"Je ne vois pas vraiment l'intérêt d'encapsuler une API synchrone dans une interface asynchrone" Si votre encapsuleur pousse le travail sur un thread d'arrière-plan, l'utilisation de IAsyncResult et du modèle asynchrone standard peut vous permettre de vérifier si l'opération terminé sans blocage. Il y a des raisons de le faire, car vous rendez l'API asynchrone. –

+1

@Reed: Vous avez raison de dire que vérifier si l'opération est terminée est un avantage d'utiliser 'IAsyncresult', mais je pense que les inconvénients du modèle (programmation basée sur les callbacks) sont encore plus importants. –

+0

Sauf si vous pouvez utiliser .NET 4, cependant, il n'y a souvent pas de meilleures alternatives. IAsyncResult est vraiment la seule façon de .NET 2.0 pour vous donner un moyen de dire si une opération asynchrone est terminée, et aussi de propager les erreurs dans le thread appelant ... –

1

Ce qui suit est une réponse à la question de suivi de Reed . Je ne dis pas que c'est la bonne façon de procéder.

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

L'utilisation de Lazy pourrait avoir plus de sens si l'implémentation était sur la file d'attente du pool de threads, pas sur Task. La tâche est déjà trop propre pour bénéficier d'un emballage supplémentaire. –

+1

@Steven: Le problème avec Lazy est qu'il bloque dès que vous essayez d'obtenir la valeur. À ce stade, la tâche fait exactement la même chose (plus fournit d'autres avantages) ... Lazy est vraiment juste là pour une instanciation paresseuse en toute sécurité dans un environnement multithread. –

+0

Mais merci de me laisser voir ce que vous pensiez;) –

Questions connexes