Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Force C# async tasks to be lazy?

I have a situation where I have an object tree created by a special factory. This is somewhat similar to a DI container, but not quite.

Creation of objects always happens via constructor, and the objects are immutable.

Some parts of the object tree may not be needed in a given execution and should be created lazily. So the constructor argument should be something that is just a factory for on-demand creation. This looks like a job for Lazy.

However, object creation may need to access slow resources and is thus always async. (The object factory's creation function returns a Task.) This means that the creation function for the Lazy would need to be async, and thus the injected type needs to be Lazy<Task<Foo>>.

But I'd rather not have the double wrapping. I wonder if it is possible to force a Task to be lazy, i.e. to create a Task that is guaranteed to not execute until it is awaited. As I understand it, a Task.Run or Task.Factory.StartNew may start executing at any time (e.g. if a thread from the pool is idle), even if nothing is waiting for it.

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;}
}
like image 870
Sebastian Redl Avatar asked Jul 24 '17 08:07

Sebastian Redl


2 Answers

I'm not sure exactly why you want to avoid using Lazy<Task<>>,, but if it's just for keeping the API easier to use, as this is a property, you could do it with a backing field:

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

That way, the usage is as if it were just a task, but the initialisation is lazy and will only incur the work if needed.

like image 130
Max Avatar answered Nov 20 '22 04:11

Max


@Max' answer is good but I'd like to add the version which is built on top of Stephen Toub' article mentioned in comments:

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's explicitly inherited from Lazy<Task<>> so it's clear that it's lazy and asyncronous.

  2. Calling base constructor wraps lazyPartFactory to Task.Run to avoid long block if that factory needs some cpu-heavy work before real async part. If it's not your case, just change it to base(lazyPartFactory)

  3. SlowPart is accessible through TaskAwaiter. So SomePart' public interface is:

    • var eagerValue = somePart.EagerPart;
    • var slowValue = await somePart;
like image 7
pkuderov Avatar answered Nov 20 '22 02:11

pkuderov