Почти 100% асинхронных задач, с которыми вы сталкиваетесь в своем коде C #, выполняются или уже выполнены, независимо от того, «ожидаете» вы их или нет. Это можно показать на следующем примере:

static async Task Main()
{
    var task = GetValueAsync();
    await Task.Delay(500);
    Console.WriteLine("Awating...");
    Console.WriteLine("Result is: " + await task);
}
static async Task<int> GetValueAsync()
{
    Console.WriteLine("Async Start");
    await Task.Delay(100);
    Console.WriteLine("Async End");
    return 42;
}

Вывод:

Async Start
Async End
Awating...
Result is: 42

Как вы видите, GetValueAsync завершился до await, но что, если поведение нежелательно и вы не хотите, чтобы задача начинала выполняться, пока не будет хотя бы один «ожидающий»? Конечно, вы можете использовать Lazy - общий класс из пространства имен System:

static async Task Main()
{
    var task = new Lazy<Task<int>>(GetValueAsync);
    await Task.Delay(500);
    Console.WriteLine("Awating...");
    Console.WriteLine("Result is: " + await task.Value);
}
static async Task<int> GetValueAsync()
{
    Console.WriteLine("Async Start");
    await Task.Delay(100);
    Console.WriteLine("Async End");
    return 42;
}

Вывод:

Awating...
Async Start
Async End
Result is: 42

Однако есть другое решение, которое незначительно повлияет на ваш клиентский код - это замена Task, возвращаемого вашей асинхронной функцией, на LazyTask - класс, который вы можете создать своим использовать новую функцию C # 7 - Обобщенные асинхронные возвращаемые типы

В результате ваш код будет выглядеть следующим образом:

static async Task Main()
{
    var task = GetValueAsync();
    await Task.Delay(500);
    Console.WriteLine("Awating...");
    Console.WriteLine("Result is: " + await task);
}
static async LazyTask<int> GetValueAsync()
{
    Console.WriteLine("Async Start");
    await Task.Delay(100);
    Console.WriteLine("Async End");
    return 42;
}

Вывод:

Awating...
Async Start
Async End
Result is: 42

Как вы знаете, компилятор C # преобразует методы, помеченные как async, в конечный автомат, где каждое состояние представляет собой завершение асинхронной операции с пометкой await. Обобщенные асинхронные возвращаемые типы позволяют получить доступ к выполнению конечного автомата (я подробно изучал этот механизм в одной из своих предыдущих статей.), а в нашем случае нам просто нужно отложить первый шаг пока у нас не будет хотя бы одного ожидающего. Это можно сделать, настроив метод Старт:

LazyTaskMethodBuilder.cs

public class LazyTaskMethodBuilder<T>
{
    public LazyTaskMethodBuilder() => this.Task = new LazyTask<T>();
    public void Start<TStateMachine>(
        ref TStateMachine stateMachine)..
    {
        //instead of stateMachine.MoveNext();
        this.Task.SetStateMachine(stateMachine);
    }
    ...
}

Вместо запуска первого шага метод Start просто сохраняет ссылку на конечный автомат внутри объекта LazyTask.

Теперь первый шаг можно вызвать только тогда, когда у нас есть первый «ожидающий»:

LazyTask.cs

public void OnCompleted(Action continuation)                               {
    ...
    this._asyncStateMachine.MoveNext();
    ...
}

Примечание. Интересно, что этот трюк работает даже с методами, не имеющими внутри асинхронных вызовов (без ключевого слова «await»).

Это все!

В заключение я хочу сказать, что Обобщенные асинхронные возвращаемые типы - мощный механизм, а LazyTask - лишь один из примеров того, как его можно использовать.

Ссылки

  1. Исходный код на Gitnub;

2. «Может быть монада через async / await в C # (No Tasks!) »- статья, в которой подробно рассматриваются обобщенные типы возврата async.