При разработке приложений могут возникать ситуации, когда нам необходимо не просто запустить несколько асинхронных задач и далее выполнять синхронный код, а ожидать выполнения одной или нескольких задач из списка и только затем продолжить работу. Погрузимся чуть глубже в работу с 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
вернет результат как только хотя бы одна из задач выполнит работу.