1. Введение
Вообразите, что ваш код — это очередь в магазине. Если вы запускаете синхронный метод, весь поток (ваша очередь) ждет, пока кассир закончит работу с одним покупателем, прежде чем приступить к следующему. Если же метод асинхронный, то вы просто кладете покупки на кассу и уходите заниматься своими делами, а когда кассир расправится — он вас позовет.
Асинхронный метод (async-метод) в .NET — это обычный метод с особой "надписью" перед ним (async), который позволяет внутри использовать оператор await для неблокирующего ожидания других асинхронных операций (например, работы с сетью или файлами) без блокировки главного потока. Именно эта "надпись" превращает скучный синхронный магазин в суперсовременное обслуживание без очередей.
Базовая сигнатура асинхронного метода
Асинхронный метод обязательно объявляется с модификатором async в сигнатуре. Вот самый базовый пример:
public async Task MyAsyncMethod()
{
// Ваш асинхронный код здесь
}
Возвращаемые типы асинхронных методов:
| Возвращаемый тип | Описание | Пример |
|---|---|---|
|
Метод выполняет работу асинхронно, но не возвращает результат | |
|
Метод выполняет работу асинхронно и возвращает результат типа T | |
|
Используется только для событий. Не рекомендуется в остальном коде! | |
|
Для высокопроизводительных сценариев, когда результат часто готов синхронно | — |
Важно: Не используйте async void нигде, кроме обработчиков событий! Иначе рискуете остаться без нормальной обработки ошибок.
2. Простой пример асинхронного метода
Вернемся к нашему тренировочному приложению (пусть это будет обычная консольная программа), и добавим асинхронный метод, который получает данные из сети. Мы будем эмулировать задержку.
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Запуск асинхронного метода...");
await MyFakeDownloadAsync();
Console.WriteLine("Асинхронная операция завершена!");
}
static async Task MyFakeDownloadAsync()
{
Console.WriteLine("Начинаем загрузку (эмулируем задержку 2 сек)...");
await Task.Delay(2000); // Эмулируем долгую операцию
Console.WriteLine("Загрузка завершена!");
}
}
Комментарии к примеру:
- Ключевое слово async указано у Main и MyFakeDownloadAsync.
- Оператор await можно использовать только внутри методов, помеченных как async.
- Метод Task.Delay(2000) эмулирует долгую операцию (например, сетевой запрос).
- После появления оператора await поток не блокируется — выполнение Main "приостанавливается" до завершения задержки.
3. Как работает асинхронный метод под капотом?
Когда компилятор видит метод с ключевым словом async, он превращает его в "машину состояния", которая запоминает место остановки, локальные переменные и продолжает выполнение после завершения awaited-операции.
Вот очень упрощенная схема:
+-----------------------------------------------------------+
| Вызов асинхронного метода (например, await X) |
+-------------------------+---------------------------------+
|
v
Операция не завершена? (не Task.Completed)
| |
| да | нет
v v
Приостановить Сразу вернуть
выполнение, результат
запомнить (или бросить ошибку)
контекст
|
Асинхронная операция завершается...
|
v
Машина состояния "будит" метод,
выполнение продолжается с места после await
Запоминать детали реализации не требуется: контекст (место остановки и локальные переменные) сохраняется и автоматически восстанавливается, когда операция завершится.
Где можно (и нельзя) использовать ключевые слова async и await
- async ставится перед декларацией метода (или лямбды).
- Внутри async-метода можно (и нужно) использовать await.
- Если попытаться написать await в методе без async — будет ошибка компиляции.
- Если указать async, но в методе нет ни одного await — получите предупреждение; метод выполнится синхронно и вернет уже завершённую задачу.
4. Варианты возвращаемых значений и их особенности
Результат Task
Если метод выполняет асинхронные действия, но не возвращает значение, используйте Task:
public async Task SaveToFileAsync(string path)
{
await Task.Delay(1000);
// Сохраняем что-то в файл
}
Результат Task<T>
Если нужен результат, используйте Task<T>:
public async Task<int> CalculateAsync()
{
await Task.Delay(500);
return 42;
}
Вызов:
int result = await CalculateAsync();
Результат async void
Используйте только для событий! Например, обработчик клика кнопки:
private async void Button_Click(object sender, EventArgs e)
{
await Task.Delay(1000);
// Делаем что-то еще
}
Почему нельзя использовать async void в обычных методах? Потому что вы не узнаете, когда такой метод завершится, и не сможете корректно отловить исключения внутри него.
Результаты ValueTask и ValueTask<T>
Появились для высокопроизводительных библиотек, когда результат часто уже готов синхронно — позволяют избежать лишних аллокаций задач. На старте карьеры можно не использовать: встречаются не так часто.
5. Пример: асинхронный калькулятор с возвратом результата
Пусть программа подсчитывает сумму двух чисел "с задержкой", будто это очень сложная операция:
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Введите первое число:");
int a = int.Parse(Console.ReadLine()!);
Console.WriteLine("Введите второе число:");
int b = int.Parse(Console.ReadLine()!);
Console.WriteLine("Выполняем расчет...");
int sum = await CalculateSumAsync(a, b);
Console.WriteLine($"Сумма: {sum}");
}
static async Task<int> CalculateSumAsync(int x, int y)
{
await Task.Delay(2000); // "Долгая" операция
return x + y;
}
}
Что тут важно:
- CalculateSumAsync — асинхронный метод, возвращает Task<int>, внутри есть await.
- Вызов в Main происходит с использованием await (напоминание: начиная с C# 7.1 метод Main может быть асинхронным).
- Пока считается сумма, поток не блокируется — можно показывать прогресс или выполнять другую работу.
6. Вложенные методы и вложенные await
Асинхронный метод может вызывать другой асинхронный метод, а тот — ещё один. Вся цепочка продолжится автоматически:
public async Task<int> StartWorkAsync()
{
int data = await GetDataAsync();
int result = await ProcessDataAsync(data);
return result;
}
Всё просто: ждём завершения одной операции, затем другой.
7. Ловушки и частые ошибки
1. Не забывайте async!
Если забыть async, компилятор оскорбится, увидев внутри метода await, и не даст собрать код.
2. Не используйте async void в обычных методах
Асинхронные void-методы (кроме обработчиков событий) — "черные дыры" для исключений. Ошибки в них могут "утонуть", и вы об этом даже не узнаете.
3. Асинхронный метод БЕЗ await
Так можно, но бессмысленно — метод выполнится синхронно и вернёт уже завершённую задачу.
public async Task DoNothingAsync()
{
// Ой, никакого await!
}
4. Возврат Task из НЕ-асинхронного метода
Так делают для унификации интерфейсов: часть операций реально асинхронные, часть — мгновенные.
public Task<int> Foo()
{
// return 42; // Так нельзя!
return Task.FromResult(42); // Можно, но никакой асинхронности тут нет.
}
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ