2
votes

Comment créer et implémenter des interfaces pour des opérations parfois asynchrones

Disons que j'ai des centaines de classes qui implémentent une interface commune avec une méthode "calculer". Certaines classes exécuteront async (par exemple, lire un fichier), et d'autres classes implémentant la même interface exécuteront du code qui est synchronisé (par exemple, en ajoutant deux nombres). Quel est le bon moyen de coder cela, pour la maintenance et pour les performances?

Les articles que j'ai lus jusqu'à présent recommandent toujours de faire remonter les méthodes async / await aux appelants. Donc, si vous avez une opération qui est asynchrone, rendez l'appelant asynchrone, puis son appelant asynchrone, et ainsi de suite. Cela me fait donc penser que l'interface doit être une interface asynchrone. Cependant, cela crée un problème lors de l'implémentation de l'interface avec un code synchrone.

Une idée à laquelle j'ai pensé est d'exposer dans l'interface 2 méthodes, une async et une sync, et une propriété booléenne pour indiquer au appelant quelle méthode appeler. Cela aurait l'air vraiment moche.

Le code que j'ai actuellement n'est qu'une seule méthode d'interface qui est asynchrone. Ensuite, pour les implémentations synchrones, elles encapsulent le code dans un objet Task:

using System.IO;
using System.Threading.Tasks;

namespace TestApp
{
    interface IBlackBox
    {
        Task<string> PullText();
    }

    sealed class MyAsyncBlackBox : IBlackBox
    {
        public async Task<string> PullText()
        {
            using (var reader = File.OpenText("Words.txt"))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }

    sealed class MyCachedBlackBox : IBlackBox
    {
        public Task<string> PullText()
        {
            return Task.Run(() => "hello world");
        }
    }
}

Est-ce la bonne approche pour créer et implémenter une interface qui n'est que parfois asynchrone? J'ai beaucoup de classes qui implémentent de courtes opérations synchrones, et je crains que cela puisse ajouter beaucoup de frais généraux. Y a-t-il un autre moyen de faire cela qui me manque?


4 commentaires

Je pense que vous avez la bonne idée en faisant renvoyer l'interface une tâche, mais votre implémentation synchrone est fausse. N'utilisez pas Task.Run (...) , utilisez Task.FromResult (...); . Votre bibliothèque ne devrait pas utiliser Task.Run (...) car cela utilisera un nouveau thread (en quelque sorte, c'est plus compliqué que cela) au lieu de simplement s'exécuter sur le thread en cours d'exécution.


Pourquoi pensez-vous que vous avez besoin d'avoir "une propriété booléenne pour indiquer à l'appelant quelle méthode appeler"?


Je n'ai pas besoin du booléen, c'était une idée pour éviter d'encapsuler le code dans Task.


Le commentaire de @Nelson semble être la meilleure réponse


3 Réponses :


1
votes

Habituellement, dans ces cas, vous avez quelque chose devant l'appel qui gère la demande et la transmet aux classes "worker" (par exemple TestApp). Si tel est le cas, je ne vois pas pourquoi avoir une interface "IAsyncable" où vous pourriez tester si la classe était compatible asynchrone ne fonctionnerait pas.

if(thisObject is IAscyncAble) {
  ... call the ansync request.
}


0 commentaires

3
votes

C'est une situation courante avec les interfaces . Si vous avez un contrat qui doit spécifier une tâche pour le Async Await Pattern et que nous devons implémenter cette Task dans l'interface .

En supposant que l'appelant utilisera wait , vous pouvez simplement supprimer async et renvoyer une Task . p>

Cependant, vous devez être prudent avec vos exceptions . Il est supposé que des exceptions sont placées sur la tâche . Donc, pour garder cette plomberie, l'appelant s'attendra à ce que vous deviez les gérer légèrement différemment.

Utilisations courantes

Standard async

#pragma warning disable 1998
public async Task<string> PullText()()
#pragma warning restore 1998
{
    return Task.FromResult("someString");
}

Renvoyer une Tâche pour un travail lié ​​au CPU (capturer l'exception et la placer sur la Tâche code>)

#pragma warning disable 1998
public async Task<string> PullText()()
#pragma warning restore 1998
{
    return Task.Run(() => "hello world");
}

Légèrement moins efficace car nous installons une IAsyncStateMachine

public Task<string> PullText()
{
   try
   {
      // simplified example
      return Task.FromResult("someString");
   }
   catch (Exception e)
   {
      return Task.FromException<string>(e);
   }
}

Retour une tâche terminée avec un résultat simple (capturer l'exception et la placer sur la Task)

public async Task<string> PullText()
{
    return await Task.Run(() => DoCpuWork());
}


2 commentaires

D'accord avec cette réponse en général, mais pas avec l'utilisation de Task.Run . IMO Task.Run ne doit être utilisé que si nécessaire par le code consommateur , c'est-à-dire lorsqu'il est appelé depuis le thread de l'interface utilisateur mais pas depuis un thread de pool de threads.


Merci @StephenCleary, j'ai acheté votre livre!



0
votes

J'ai fini par utiliser le code suivant:

using System.IO;
using System.Threading.Tasks;

namespace TestApp
{
    interface IBlackBox // interface for both sync and async execution
    {
        Task<string> PullText();
    }

    sealed class MyAsyncBlackBox : IBlackBox
    {
        public async Task<string> PullText()
        {
            using (var reader = File.OpenText("Words.txt"))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }

    sealed class MyCachedBlackBox : IBlackBox
    {
        public Task<string> PullText() // notice no 'async' keyword
        {
            return Task.FromResult("hello world");
        }
    }
}


0 commentaires