Задачи aync Force С# быть ленивыми?

У меня есть ситуация, когда у меня есть дерево объектов, созданное специальным factory. Это несколько похоже на контейнер DI, но не совсем.

Создание объектов всегда происходит через конструктор, и объекты неизменяемы.

Некоторые части дерева объектов могут не понадобиться в данном исполнении и должны создаваться лениво. Таким образом, аргумент конструктора должен быть чем-то вроде factory для создания по требованию. Это выглядит как работа для Lazy.

Однако для создания объектов может потребоваться доступ к медленным ресурсам и, следовательно, всегда асинхронный. (Функция создания объекта factory возвращает Task.) Это означает, что функция создания для Lazy должна быть асинхронной, и, следовательно, вводимый тип должен быть Lazy<Task<Foo>>.

Но я бы предпочел не иметь двойную упаковку. Интересно, можно ли заставить Task лениться, т.е. Создать Task, который, как гарантируется, не будет выполняться до тех пор, пока он не будет ожидаться. Насколько я понимаю, Task.Run или Task.Factory.StartNew могут запускаться в любое время (например, если поток из пула простаивает), даже если ничего не ждет.

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

Ответы

Ответ 1

Я не уверен, почему вы хотите избежать использования Lazy<Task<>>,, но если это просто для того, чтобы упростить использование API, поскольку это свойство, вы можете сделать это с помощью поля поддержки:

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

Таким образом, использование выглядит так, как если бы это была просто задача, но инициализация ленива и при необходимости будет выполнять только работу.

Ответ 2

@Max 'ответ хороший, но я хотел бы добавить версию, которая построена поверх статьи Стивена Тууба, упомянутой в комментариях:

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();
}
  • SomePart явно унаследован от Lazy<Task<>>, поэтому он понимает, что он ленив и асинхронен.

  • Оболочка базового конструктора обертывает lazyPartFactory в Task.Run, чтобы избежать длинного блока, если для этого factory требуется некоторая работа с процессором до реальной части async. Если это не ваш случай, просто измените его на base(lazyPartFactory)

  • SlowPart доступен через TaskAwaiter. Итак, открытый интерфейс SomePart:

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

Ответ 3

Использование конструктора для Task делает задачу ленивой a.k.a не запущенной до тех пор, пока вы не скажете, что она запускается, поэтому вы можете сделать что-то вроде этого:

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

Результат:

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

Однако Я настоятельно рекомендую вам использовать Rx.NET и IObservable, как в вашем случае вы будете получите меньше проблем для обработки менее наивных случаев, чтобы начать свою задачу в нужный момент. Также он делает код немного более чистым, на мой взгляд

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

Результат:

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

Чтобы быть более понятным: Observable здесь обрабатывать, когда запускать задачу, она по умолчанию является Lazy и будет запускать задачу каждый раз, когда она подписана (здесь подписка используется awaiter, которая позволяет использовать await).

Вы могли бы, если вам нужно, запустить задачу только один раз каждую минуту (или когда-либо) и опубликовать ее результат среди всех подписчиков, чтобы, например, сохранить производительность, например, в реальном приложении, все это и многое другое обрабатывается наблюдаемыми.