it-swarm.com.ru

Заставить асинхронные задачи C # быть ленивыми?

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

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

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

Однако для создания объекта может потребоваться доступ к медленным ресурсам, и поэтому он всегда асинхронный. (Функция создания фабрики объектов возвращает 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;}
}
16
Sebastian Redl

Я не совсем уверен, почему вы хотите избежать использования 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;
}

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

19
Max

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

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

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

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

Использование конструктора для 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 и будет запускать задачу каждый раз, когда она подписана (здесь подписчик используется ожидающим, который разрешает использование ключевого слова await).

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

0
Uwy