web-dev-qa-db-fra.com

Forcer les tâches asynchrones C # à être paresseux?

J'ai une situation où j'ai un arbre d'objets créé par une usine spéciale. Ceci est un peu similaire à un conteneur DI, mais pas tout à fait.

La création d'objets se fait toujours via un constructeur et les objets sont immuables.

Certaines parties de l'arborescence d'objets peuvent ne pas être nécessaires dans une exécution donnée et doivent être créées paresseusement. L'argument constructeur doit donc être simplement une fabrique pour la création à la demande. Cela ressemble à un travail pour Lazy.

Cependant, la création d'objets peut nécessiter l'accès à des ressources lentes et est donc toujours asynchrone. (La fonction de création de la fabrique d'objets renvoie Task.) Cela signifie que la fonction de création de Lazy doit être asynchrone et que, par conséquent, le type injecté doit être Lazy<Task<Foo>>.

Mais je préférerais ne pas avoir le double emballage. Je me demande s’il est possible de forcer une Task à être paresseuse, c’est-à-dire à créer une Task dont la garantie de ne pas être exécutée tant qu’elle n’est pas attendue. Si j'ai bien compris, un Task.Run ou un Task.Factory.StartNew peut commencer à s'exécuter à tout moment (par exemple, si un thread du pool est inactif), même si rien ne l'attend.

public class SomePart
{
  // Factory should create OtherPart immediately, but SlowPart
  // creation should not run until and unless someone actually
  // awaits the task.
  public SomePart(OtherPart eagerPart, Task<SlowPart> lazyPart)
  {
    EagerPart = eagerPart;
    LazyPart = lazyPart;
  }

  public OtherPart EagerPart {get;}
  public Task<SlowPart> LazyPart {get;}
}
16
Sebastian Redl

Je ne sais pas exactement pourquoi vous voulez éviter d'utiliser Lazy<Task<>>,, mais si vous voulez que l'API soit plus facile à utiliser, car il s'agit d'une propriété, vous pouvez le faire avec un champ de sauvegarde:

public class SomePart
{
    private readonly Lazy<Task<SlowPart>> _lazyPart;

    public SomePart(OtherPart eagerPart, Func<Task<SlowPart>> lazyPartFactory)
    {
        _lazyPart = new Lazy<Task<SlowPart>>(lazyPartFactory);
        EagerPart = eagerPart;
    }

    OtherPart EagerPart { get; }
    Task<SlowPart> LazyPart => _lazyPart.Value;
}

De cette façon, l’utilisation est comme s’il s’agissait d’une tâche, mais l’initialisation est paresseuse et ne nécessitera le travail que si cela est nécessaire.

19
Max

La réponse de @Max est bonne, mais j'aimerais ajouter la version qui repose sur l'article de Stephen Toub 'mentionné dans les commentaires:

public class SomePart: Lazy<Task<SlowPart>>
{
    public SomePart(OtherPart eagerPart, Func<Task<SlowPart>> lazyPartFactory)
        : base(() => Task.Run(lazyPartFactory))
    {
        EagerPart = eagerPart;
    }

    public OtherPart EagerPart { get; }
    public TaskAwaiter<SlowPart> GetAwaiter() => Value.GetAwaiter();
}
  1. SomePart étant explicitement hérité de Lazy<Task<>>, il est donc clair qu'il s'agit de lazy et asyncronous .

  2. L'appel du constructeur de base renvoie lazyPartFactory à Task.Run pour éviter les blocages longs si cette usine a besoin de travaux lourds en cpu avant une partie asynchrone réelle. Si ce n'est pas votre cas, changez-le simplement en base(lazyPartFactory)

  3. SlowPart est accessible via TaskAwaiter. Donc, l'interface publique de SomePart est:

    • var eagerValue = somePart.EagerPart;
    • var slowValue = await somePart;
2
pkuderov

Utiliser le constructeur pour Task rend la tâche paresseuse a.k.a pas en cours d'exécution tant que vous ne le dites pas, afin que vous puissiez faire quelque chose comme ceci:

public class TestLazyTask
{
    private Task<int> lazyPart;

    public TestLazyTask(Task<int> lazyPart)
    {
        this.lazyPart = lazyPart;
    }

    public Task<int> LazyPart
    {
        get
        {
            // You have to start it manually at some point, this is the naive way to do it
            this.lazyPart.Start();
            return this.lazyPart;
        }
    }
}


public static async void Test()
{
    Trace.TraceInformation("Creating task");
    var lazyTask = new Task<int>(() =>
    {
        Trace.TraceInformation("Task run");
        return 0;
    });
    var taskWrapper = new TestLazyTask(lazyTask);
    Trace.TraceInformation("Calling await on task");
    await taskWrapper.LazyPart;
} 

Résultat:

SandBox.exe Information: 0 : Creating task
SandBox.exe Information: 0 : Calling await on task
SandBox.exe Information: 0 : Task run

Cependant, je vous recommande vivement d'utiliser Rx.NET et IObservable car, dans votre cas, vous aurez beaucoup moins de problèmes pour gérer des cas moins naïfs et commencer votre tâche au bon moment. De plus, cela rend le code un peu plus propre à mon avis

public class TestLazyObservable
{
    public TestLazyObservable(IObservable<int> lazyPart)
    {
        this.LazyPart = lazyPart;
    }

    public IObservable<int> LazyPart { get; }
}


public static async void TestObservable()
{
    Trace.TraceInformation("Creating observable");
    // From async to demonstrate the Task compatibility of observables
    var lazyTask = Observable.FromAsync(() => Task.Run(() =>
    {
        Trace.TraceInformation("Observable run");
        return 0;
    }));

    var taskWrapper = new TestLazyObservable(lazyTask);
    Trace.TraceInformation("Calling await on observable");
    await taskWrapper.LazyPart;
}

Résultat: 

SandBox.exe Information: 0 : Creating observable
SandBox.exe Information: 0 : Calling await on observable
SandBox.exe Information: 0 : Observable run

Pour être plus clair: la Observable manipule ici le moment de démarrer la tâche. Par défaut, elle est Lazy et sera exécutée à chaque fois qu'elle est abonnée (ici, subscribe utilise l'attribut qui permet d'utiliser le mot clé await.

Vous pouvez, si vous en avez besoin, faire en sorte que la tâche ne soit exécutée qu'une fois par minute (ou jamais) et que son résultat soit publié sur tous les abonnés afin d'économiser les performances, par exemple, comme dans une application du monde réel, tout cela et beaucoup plus est géré par observables.

0
Uwy