Ожидание выполнения асинхронных задач

При разработке приложений могут возникать ситуации, когда нам необходимо не просто запустить несколько асинхронных задач и далее выполнять синхронный код, а ожидать выполнения одной или нескольких задач из списка и только затем продолжить работу. Погрузимся чуть глубже в работу с Task и посмотрим, как можно организовать ожидание асинхронных задач в C#.

Тестовый пример асинхронных методов

Допустим, нам необходимо с помощью трех асинхронных методов вычислить результат выражения (a+b)*c и вывести этот результат в консоль. Очевидно, что метод вывода результата в консоль будет зависеть от результата вычислений, а метод умножения зависеть от результат суммирования двух чисел. То есть, мы должны получить вот такой код (если все расписывать подробно):

class Program
{
            //сложение двух чисел
    private static async Task<int> Sum(int a, int b)
    { 
        await Task.Delay(1000);//немного подождем
        return a + b;
    }
            
            //умножение двух чисел
    private static async Task<int> Multiply(int a, int b)
    {
        await Task.Delay(2000);
        return a * b;
    }
            
            //вывод результата
    private static async Task<string> GetResult(int a) 
    {
        await Task.Delay(100);
        return $"Результат равен {a}";
    }

    static async Task Main(string[] args)
    {
        
        int a = 0;
        int b = 3;
        int c = 4;
        int sumResult;
        int multiplyResult;

        var sum = Sum(a, b);
        Console.WriteLine("Суммирование запущено");	
        
        //ожидаем результат сложения
        sumResult = await sum;

        var multiply = Multiply(sumResult, c);
        Console.WriteLine("Умножение запущено");

        multiplyResult = await multiply;
        //выводим результат
        Console.WriteLine(await GetResult(multiplyResult));
    }
}

Ожидание в методах (Task.Delay) добавлено, чтобы показать ИБД (имитацию бурной деятельности) приложения. Здесь все сработает как надо. В результате мы должны получить (0+3)*4 = 12. Мы последовательно ожидаем сначала выполнения задачи sum, потом — multiply и только потом выводим в консоль результат. Такой подход вполне имеет право на существование, но, чем больше кода — тем быстрее в нем можно запутаться, да и асинхронные методы не всегда содержат всего две строчки кода, поэтому, используя возможности класса Task мы можем сделать наш пример более лаконичным и понятным.

Методы WhenAll и WhenAny

Методы WhenAll и WhenAny — это два статических метода класса Task. Метод WhenAll получает в параметрах несколько задач и создает задачу, которая будет выполнена в том случае, когда все задачи, переданные в параметрах метода будут выполнены. Этот метод подходит нам в качестве работы с примером выше. Перепишем пример, используя WhenAll. Пусть нам необходимо посчитать выражение: (a+b)*(c+d)*e:

static async Task Main(string[] args)
{
    
    int a = 0;
    int b = 3;
    int c = 4;
    int d = 5;
    int e = 6;

    //создаем две задачи
    var sum = Sum(a, b);
    var sum2 = Sum(c, d);

    //Ожидаем выполнение двух задач суммирования
    int[] results = await Task.WhenAll(sum, sum2);
    
    //последовательно перемножаем числа
    int multiply = await Multiply(results[0], results[1]);
    int itog = await Multiply(multiply, e);

    //выводим результат
    Console.WriteLine(await GetResult(itog));
}

Так как результат умножения зависит от выполнения операций сложения, то вначале мы создаем две задачи на сложение чисел и ожидаем пока они не обе не выполнятся. После этого мы последовательно перемножаем полученные результаты и выводим итог в консоль. Метод Task.WhenAll, так как мы использовали оператор await вернул нам массив чисел. Размерность массива совпадает с количеством параметров (отдельных задач) переданных методу. Здесь стоит рассмотреть вопрос: что вернет метод WhenAll, если асинхронные задачи возвращают разные типы результатов? Например, первые две задачи должны вернуть int, а вторая — string. Тут мы уже явно не сможем присвоить результат WhelAll массиву int[]. Такой код вызовет ошибку:

 //создаем две задачи
var sum = Sum(a, b);
var sum2 = Sum(c, d);
var dataPrint = GetResult(100);

//Ожидаем выполнение 
int[] results = await Task.WhenAll(sum, sum2, dataPrint);
Ошибка CS0029 Не удается неявно преобразовать тип «void» в «int[]». ConsoleApp1 Program.cs

В данном случае, для получения результатов нам необходимо отдельно получать результаты каждой задачи, например, так:

//Ожидаем выполнение 
await Task.WhenAll(sum, sum2, dataPrint);

Console.WriteLine(sum.Result);
Console.WriteLine(sum2.Result);
Console.WriteLine(dataPrint.Result);

В отличие от Task.WhenAll, метод Task.WhenAny возвращает задачу в тос случае, если хотя бы одна из задач завершится. Например,

var sum = Sum(a, b);
var sum2 = Sum(c, d);
Task<int> data = await Task.WhenAny(sum, sum2);
Console.WriteLine(data.Result);

В консоль будет выведен первый из полученных результатов. Опять же, так как обе задачи возвращают Task<int>, то и результатом метода WhenAny можно ожидать задачу Task<int>. Если же переданные в параметрах задачи возвращают различные типы, то результат выполнения отдельных задач нам необходимо получать через свойство Result:

await Task.WhenAny(sum, sum2, dataPrint);

if (sum.IsCompleted)
  Console.WriteLine(sum.Result);

if (sum2.IsCompleted)
    Console.WriteLine(sum2.Result);

if (dataPrint.IsCompleted)
    Console.WriteLine(dataPrint.Result);

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

Итого

Для ожидания выполнения асинхронных методов мы можем использовать два статических метода класса TaskWhenAll и WhenAny. Метод WhenAll создает задачу только тогда, когда все задачи переданные в параметрах метода завершат работу. WhenAny вернет результат как только хотя бы одна из задач выполнит работу.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии