web-dev-qa-db-fra.com

Écrire une API async/non-async bien conçue

Je suis confronté au problème de la conception de méthodes permettant d'effectuer des E/S réseau (pour une bibliothèque réutilisable). J'ai lu cette question

C # 5 wait/pattern async dans la conception de l'API

et aussi d'autres plus proches de mon problème.

Donc, la question est, si je veux fournir à la fois asynchrone et non asynchrone méthode, comment dois-je les concevoir?

Par exemple, pour exposer une version non asynchrone d’une méthode, je dois faire quelque chose comme:

public void DoSomething() {
  DoSomethingAsync(CancellationToken.None).Wait();
}

et je pense que ce n'est pas un bon design. J'aimerais une suggestion (par exemple) sur la façon de définir des méthodes privées pouvant être intégrées à des méthodes publiques pour fournir les deux versions.

49
jay

Si vous souhaitez utiliser l'option la plus facile à gérer, fournissez uniquement une API async, implémentée sans appel de blocage ni utilisation de threads de pool de threads.

Si vous voulez vraiment avoir les API async et synchrone, vous rencontrerez un problème de maintenabilité. Vous devez vraiment l'implémenter deux fois: une fois async et une fois synchrone. Ces deux méthodes sembleront presque identiques, ce qui facilitera la mise en œuvre initiale, mais vous obtiendrez deux méthodes distinctes et presque identiques, ce qui rend la maintenance problématique.

En particulier, il n’existe aucun moyen simple et efficace de créer simplement un "wrapper" async ou synchrone. Stephen Toub a la meilleure information sur le sujet:

  1. Dois-je exposer les wrappers asynchrones pour les méthodes synchrones?
  2. Dois-je exposer les wrappers synchrones pour les méthodes asynchrones?

(la réponse courte aux deux questions est "non")

58
Stephen Cleary

Je suis d'accord avec Marc et Stephen (Cleary). 

(BTW, j’ai commencé à écrire ceci comme commentaire à la réponse de Stephen, mais elle s’est avérée trop longue; faites-moi savoir si vous pouvez écrire ceci comme réponse ou non, et n'hésitez pas à en prendre des passages et à ajouter à la réponse de Stephen, dans l’esprit de "fournir la meilleure réponse").

Cela "dépend" vraiment: comme l'a dit Marc, il est important de savoir comment DoSomethingAsync est asynchrone. Nous sommes tous d’accord sur le fait qu’il est inutile de demander à la méthode "sync" d’appeler la méthode "async" et à "wait": cela peut être fait en code utilisateur. Le seul avantage d’une méthode distincte est d’avoir des gains de performances réels, une implémentation sous le capot différente et adaptée au scénario synchrone. Ceci est particulièrement vrai si la méthode "async" crée un thread (ou le prend dans un pool de threads): vous vous retrouvez avec quelque chose qui utilise en dessous deux "flux de contrôle", tandis que "promet" avec ses apparences synchrones à exécuter dans le contexte des appelants. Cela peut même avoir des problèmes de concurrence, en fonction de la mise en œuvre.

Dans d'autres cas également, comme les entrées/sorties intensives mentionnées dans l'OP, il peut être intéressant de disposer de deux implémentations différentes. La plupart des systèmes d'exploitation (Windows, bien sûr) ont pour les E/S des mécanismes différents adaptés aux deux scénarios: par exemple, l'exécution asynchrone et le fonctionnement des E/S tirent parti des mécanismes au niveau du système d'exploitation tels que les ports de complétion des E/S overhead (non significatif, mais non nul) dans le noyau (après tout, ils doivent faire la comptabilité, la répartition, etc.) et une implémentation plus directe pour les opérations synchrones. La complexité du code varie également beaucoup, en particulier dans les fonctions où plusieurs opérations sont effectuées/coordonnées.

Ce que je ferais c'est:

  • avoir quelques exemples/tests pour une utilisation typique et des scénarios
  • voir quelle variante de l'API est utilisée, où et mesurer. Mesurer également la différence de performance entre une variante "pure sync" et "sync". (pas pour toute l'API, mais pour quelques cas typiques sélectionnés)
  • sur la base de la mesure, décidez si le coût supplémentaire en vaut la peine.

Ceci principalement parce que deux objectifs sont en quelque sorte en contraste l'un avec l'autre. Si vous voulez du code maintenable, le choix évident consiste à implémenter sync en termes d'async/wait (ou l'inverse) (ou, mieux encore, de ne fournir que la variante async et de laisser l'utilisateur "attendre"); si vous voulez des performances, vous devez implémenter les deux fonctions différemment, pour exploiter différents mécanismes sous-jacents (du framework ou du système d'exploitation). Je pense que cela ne devrait pas faire de différence du point de vue des tests unitaires de la manière dont vous implémentez réellement votre API. 

3
Lorenzo Dematté

J'ai rencontré le même problème, mais j'ai réussi à trouver un compromis entre efficacité et facilité de maintenance en utilisant deux faits simples sur les méthodes asynchrones:

  • méthode asynchrone qui n'exécute aucune attente est synchrone;
  • méthode asynchrone qui n'attend que des méthodes synchrones est synchrone.

Ceci est préférable pour être montré sur l'exemple:

//Simple synchronous methods that starts third party component, waits for a second and gets result.
public ThirdPartyResult Execute(ThirdPartyOptions options)
{
    ThirdPartyComponent.Start(options);
    System.Threading.Thread.Sleep(1000);
    return ThirdPartyComponent.GetResult();
}

Pour fournir une version maintenable sync/async de cette méthode, celle-ci a été scindée en trois couches:

//Lower level - parts that work differently for sync/async version.
//When isAsync is false there are no await operators and method is running synchronously.
private static async Task Wait(bool isAsync, int milliseconds)
{
    if (isAsync)
    {
        await Task.Delay(milliseconds);
    }
    else
    {
        System.Threading.Thread.Sleep(milliseconds);
    }
}

//Middle level - the main algorithm.
//When isAsync is false the only awaited method is running synchronously,
//so the whole algorithm is running synchronously.
private async Task<ThirdPartyResult> Execute(bool isAsync, ThirdPartyOptions options)
{
    ThirdPartyComponent.Start(options);
    await Wait(isAsync, 1000);
    return ThirdPartyComponent.GetResult();
}

//Upper level - public synchronous API.
//Internal method runs synchronously and will be already finished when Result property is accessed.
public ThirdPartyResult ExecuteSync(ThirdPartyOptions options)
{
    return Execute(false, options).Result;
}

//Upper level - public asynchronous API.
public async Task<ThirdPartyResult> ExecuteAsync(ThirdPartyOptions options)
{
    return await Execute(true, options);
}

L'avantage principal ici est que l'algorithme de niveau moyen qui est le plus susceptible de changer est implémenté une seule fois, de sorte que le développeur n'a pas à gérer deux morceaux de code presque identiques.

0
Madruel