Нередко программа выполняет такие операции, которые могут занять продолжительное время, например, обращение к сетевым ресурсам, чтение-запись файлов, обращение к базе данных и т.д. Такие операции могут серьезно нагрузить приложение. Особенно это актуально в графических (десктопных или мобильных) приложениях, где продолжительные операции могут блокировать интерфейс пользователя и негативно повлиять на желание пользователя работать с программой, или в веб-приложениях, которые должны быть готовы обслуживать тысячи запросов в секунду. В синхронном приложении при выполнении продолжительных операций в основном потоке этот поток просто бы блокировался на время выполнения операции. И чтобы продолжительные операции не блокировали общую работу приложения, в C# можно задействовать асинхронность.
Асинхронность позволяет вынести отдельные задачи из основного потока в специальные асинхронные методы и при этом более экономно использовать потоки. Асинхронные методы выполняются в отдельных потоках. Однако при выполнении продолжительной операции поток асинхронного метода возвратится в пул потоков и будет использоваться для других задач. А когда продолжительная операция завершит свое выполнение, для асинхронного метода опять выделяется поток из пула потоков, и асинхронный метод продолжает свою работу.
Ключевыми для работы с асинхронными вызовами в C# являются два оператора: async и await, цель которых - упростить написание асинхронного кода. Они используются вместе для создания асинхронного метода.
Асинхронный метод обладает следующими признаками:
В заголовке метода используется модификатор async
Метод содержит одно или несколько выражений await
В качестве возвращаемого типа используется один из следующих:
void
Task
Task<T>
ValueTask<T>
Асинхронный метод, как и обычный, может использовать любое количество параметров или не использовать их вообще. Однако асинхронный метод не может определять параметры с модификаторами out, ref и in.
Также стоит отметить, что слово async, которое указывается в определении метода, НЕ делает автоматически метод асинхронным. Оно лишь указывает, что данный метод может содержать одно или несколько выражений await.
Рассмотрим простейший пример определения и вызова асинхронного метода:
await PrintAsync(); // вызов асинхронного метода Console.WriteLine("Некоторые действия в методе Main"); void Print() { Thread.Sleep(3000); // имитация продолжительной работы Console.WriteLine("Hello METANIT.COM"); } // определение асинхронного метода async Task PrintAsync() { Console.WriteLine("Начало метода PrintAsync"); // выполняется синхронно await Task.Run(() => Print()); // выполняется асинхронно Console.WriteLine("Конец метода PrintAsync"); }
Здесь прежде всего определен обычный метод Print, который просто выводит некоторую строку на консоль. Для имитации долгой работы в нем используется
задержка на 3 секунд с помощью метода Thread.Sleep()
.
То есть условно Print - это некоторый метод, который выполняет некоторую продолжительную операцию. В реальном приложении это могло бы быть обращение к базе данных или чтение-запись файлов,
но для упрощения понимания он просто выводит строку на консоль.
Также здесь определен асинхронный метод PrintAsync()
. Асинхронным он является потому, что имеет в определении перед возвращаемым типом
модификатор async, его возвращаемым типом является Task, и в теле метода определено выражение
await.
Стоит отметить, что явным образом метод PrintAsync не возвращает никакого объекта Task, однако поскольку в теле метода применяется выражение await, то в качестве возвращаемого типа можно использовать тип Task.
Оператор await предваряет выполнение задачи, которая будет выполняться асинхронно. В данном случае подобная операция представляет выполнение метода Print:
await Task.Run(()=>Print());
По негласным правилам в названии асинхроннных методов принято использовать суффикс Async - PrintAsync()
,
хотя в принципе это необязательно делать.
И затем в программе (в данном случае в методе Main) вызывается этот асинхронный метод.
await PrintAsync(); // вызов асинхронного метода
Посмотрим, какой у программы будет консольный вывод:
Начало метода PrintAsync Hello METANIT.COM Конец метода PrintAsync Некоторые действия в методе Main
Разберем поэтапно, что здесь происходит:
Запускается программа, а точнее метод Main, в котором вызывается асинхронный метод PrintAsync.
Метод PrintAsync начинает выполняться синхронно вплоть до выражения await.
Console.WriteLine("Начало метода PrintAsync"); // выполняется синхронно
Выражение await запускает асинхронную задачу Task.Run(()=>Print())
Пока выполняется асинхронная задача Task.Run(()=>Print())
(а она может выполняться довольно продожительное время),
выполнение кода возвращается в вызывающий метод - то есть в метод Main.
Когда асинхронная задача завершила свое выполнение (в случае выше - вывела строку через три секунды), продолжает работу асинхронный метод PrintAsync, который вызвал асинхронную задачу.
После завершения метода PrintAsync продолжает работу метод Main.
Стоит учитывать, что оператор await можно применять только в методе, который имеет модификатор async. И если мы в методе Main используем оператор await, то метод Main тоже должен быть определен как асинхронный. То есть предыдущий пример фактически будет аналогичен следующему:
class Program { async static Task Main(string[] args) { await PrintAsync(); // вызов асинхронного метода Console.WriteLine("Некоторые действия в методе Main"); void Print() { Thread.Sleep(3000); // имитация продолжительной работы Console.WriteLine("Hello METANIT.COM"); } // определение асинхронного метода async Task PrintAsync() { Console.WriteLine("Начало метода PrintAsync"); // выполняется синхронно await Task.Run(() => Print()); // выполняется асинхронно Console.WriteLine("Конец метода PrintAsync"); } } }
В асинхронных методах для остановки метода на некоторое время можно применять метод Task.Delay(). В качестве параметра он принимает количество миллисекунд в виде значения int, либо объект TimeSpan, который задает время задержки:
await PrintAsync(); // вызов асинхронного метода Console.WriteLine("Некоторые действия в методе Main"); // определение асинхронного метода async Task PrintAsync() { await Task.Delay(3000); // имитация продолжительной работы // или так //await Task.Delay(TimeSpan.FromMilliseconds(3000)); Console.WriteLine("Hello METANIT.COM"); }
Причем метод Task.Delay
сам по себе представляет асинхронную операцию, поэтому к нему применяется оператор await.
Выше приведенные примеры являются упрощением, и вряд ли их можно считать показательным. Рассмотрим другой пример:
PrintName("Tom"); PrintName("Bob"); PrintName("Sam"); void PrintName(string name) { Thread.Sleep(3000); // имитация продолжительной работы Console.WriteLine(name); }
Данный код является синхронным и выполняет последовательно три вызова метода PrintName. Поскольку для имитации продолжительной работы в методе установлена задержка на три секунды, то общее выполнение программы займет не менее 9 секунд. Так как каждый последующий вызов PrintName будет ждать пока завершится предыдущий.
Изменим в программе синхронный метод PrintName на асинхронный:
await PrintNameAsync("Tom"); await PrintNameAsync("Bob"); await PrintNameAsync("Sam"); // определение асинхронного метода async Task PrintNameAsync(string name) { await Task.Delay(3000); // имитация продолжительной работы Console.WriteLine(name); }
Вместо метода PrintName теперь вызывается три раза PrintNameAsync. Для имитации продолжительной работы в методе установлена
задержка на 3 секунды с помощью вызова Task.Delay(3000)
. И поскольку при вызовае каждого метода применяется оператор await,
который останавливает выполнение до завершения асинхронного метода, то общее выполнение программы опять же займет не менее 9 секунд. Тем не менее теперь выполнение
асинхронных операций не блокирует основной поток.
Теперь оптимизируем программу:
var tomTask = PrintNameAsync("Tom"); var bobTask = PrintNameAsync("Bob"); var samTask = PrintNameAsync("Sam"); await tomTask; await bobTask; await samTask; // определение асинхронного метода async Task PrintNameAsync(string name) { await Task.Delay(3000); // имитация продолжительной работы Console.WriteLine(name); }
В данном случае задачи фактически запускаются при определении. А оператор await применяется лишь тогда, когда нам нужно дождаться завершения асинхронных операций - то есть в конце программы. И в этом случае общее выполнение программы займет не менее 3 секунд, но гораздо меньше 9 секунд.
Асинхронную операцию можно определить не только с помощью отдельного метода, но и с помощью лямбда-выражения:
// асинхронное лямбда-выражение Func<string, Task> printer = async (message) => { await Task.Delay(1000); Console.WriteLine(message); }; await printer("Hello World"); await printer("Hello METANIT.COM");