При разработке приложений могут возникать ситуации, когда нам необходимо не просто запустить несколько асинхронных задач и далее выполнять синхронный код, а ожидать выполнения одной или нескольких задач из списка и только затем продолжить работу. Погрузимся чуть глубже в работу с 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);
В данном случае, для получения результатов нам необходимо отдельно получать результаты каждой задачи, например, так:
//Ожидаем выполнение 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, то в консоли мы увидите результат выполнения именно этого метода.
Итого
Для ожидания выполнения асинхронных методов мы можем использовать два статических метода класса Task — WhenAll и WhenAny. Метод WhenAll создает задачу только тогда, когда все задачи переданные в параметрах метода завершат работу. WhenAny вернет результат как только хотя бы одна из задач выполнит работу.