JavaRush /Курсы /C# SELF /Ключевые слова async ...

Ключевые слова async и await

C# SELF
59 уровень , 2 лекция
Открыта

1. Введение

Прежде чем прыгать в код, давайте разберёмся с мотивацией и представим себе типичную ситуацию. Допустим, ваша программа скачивает файл из интернета:

// Псевдокод
var data = DownloadFile("https://example.com/file");
ProcessData(data);

Проблема тут такая: пока идет скачивание, программа "замерла". Никакие другие действия не выполняются — пользователь не может ни двигать мышкой, ни кликать кнопки, ни даже ждать окончания без унылой "зависшей" формы.

Раньше (и в других языках) для решения этой проблемы приходилось использовать потоки (Thread), задачи (Task), делегаты, таймеры — всё это приводило к нагромождению кода, который сложно читать и поддерживать. В C# команда решила сделать жизнь проще: появилась асинхронность с помощью ключевых слов async и await. Теперь можно писать асинхронный код почти так же просто, как обычный.

Классическая "асинхронная боль" без async/await

Для контраста, глянем, как выглядело бы выполнение долгой операции с помощью потоков, если бы мы хотели не блокировать интерфейс:

// Пример без async/await, вручную
var thread = new Thread(() =>
{
    var data = DownloadFile("https://example.com/file");
    Console.WriteLine("Файл скачан!");
});
thread.Start();

Этот способ довольно топорный: приходится заботиться о потоках вручную, нет простого способа "ожидать" результата, и ошибки обработать сложно.

2. Асинхронность в C#: синтаксис

Определение: что такое async и await?

async — это модификатор, который запрещает методу быть скучным и делает его асинхронным. Такой метод обычно возвращает либо Task (или Task<T>), либо ValueTask. Это обещание, что результат будет, но чуть позже (как заказ из интернет-магазина — заказал и ждём).

await — это оператор, который говорит: "Остановись на этой строчке, когда дойдёшь до неё. Жди, пока операция завершится, но не блокируй поток! Всё остальное можно делать дальше".

Как выглядит асинхронный метод?

public async Task MyAsyncMethod()
{
    Console.WriteLine("Загрузка файла...");
    var data = await DownloadFileAsync("https://example.com/file"); // Ждём результат асинхронно!
    Console.WriteLine("Готово!");
}

Обратите внимание:

  • К методу добавлен модификатор async.
  • Внутри метода используется await для асинхронной операции.
  • Метод возвращает Task (или Task<T>, если есть возвращаемое значение).

Визуализация: что происходит при вызове async-метода?

graph LR
A[Вызов MyAsyncMethod] --> B[Выполнение до await]
B --> C[Вызов DownloadFileAsync]
C --> D{Ожидание}
D --> |Файл не загружен| E[Освобождение потока]
D --> |Файл загружен| F[Выполнение после await]
F --> G[Завершение метода]
  • До первого await метод выполняется синхронно.
  • На await выполнение метода приостанавливается, управление возвращается вызывающему коду.
  • Когда асинхронная операция завершена, выполнение продолжается после await — как будто ничего и не было.

3. Пример: асинхронная загрузка с использованием async/await

Пусть наше учебное приложение теперь скачивает текст из интернета и печатает его размер, не блокируя остальной код.

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    // Асинхронный метод (возвращает Task)
    public static async Task DownloadAndPrintLengthAsync(string url)
    {
        Console.WriteLine("Начинаем загрузку...");

        // Используем HttpClient — он поддерживает асинхронные методы
        using (var client = new HttpClient())
        {
            string data = await client.GetStringAsync(url);
            Console.WriteLine($"Загрузка завершена! Длина текста: {data.Length} символов.");
        }

        Console.WriteLine("Работа метода завершена.");
    }

    static void Main()
    {
        // Запускаем асинхронную операцию и ожидаем её завершения
        var task = DownloadAndPrintLengthAsync("https://www.example.com");
        task.Wait(); // Для простых консольных примеров это допустимо. В реальных UI или веб-приложениях этот вызов приведёт к блокировке и возможным дэдлокам.
    }
}

Пояснения:

  • DownloadAndPrintLengthAsync — полностью асинхронный, благодаря async и await.
  • Внутри метода мы ждем окончания асинхронного скачивания строки с помощью await.
  • В Main() мы запускаем задачу и явно ждём её завершения через Wait(). В современных C# можно делать и сам Main асинхронным (async Task Main) и писать просто await.

4. Как это работает?

Различие синхронного и асинхронного кода

СИНХРОННЫЙ ВАРИАНТ

Console.WriteLine("Начало");
string data = client.GetStringAsync(url).Result; // .Result блокирует поток!
Console.WriteLine("Операция завершена");

АСИНХРОННЫЙ ВАРИАНТ

Console.WriteLine("Начало");
string data = await client.GetStringAsync(url); // Поток не блокируется
Console.WriteLine("Операция завершена");

Как работает await «под капотом»?

Когда вы используете await, C# автоматически “разбивает” ваш метод на две (или больше) части: всё, что до await, и всё, что после. Когда вызываемый асинхронный метод возвращает Task, ваш метод возвращается вызывающему коду, а когда Task завершён — выполнение продолжается после await. Всё это происходит автоматически; вам не нужно заботиться о потоках и переключениях вручную.

Интересный факт: C# “превращает” ваш асинхронный метод в состояние-машину, где каждый “await” — это новая “точка возврата”.

Использование async/await в реальных приложениях

static async Task Main(string[] args)
{
    var downloadTask = DownloadAndPrintLengthAsync("https://www.example.com");
    // Пока идет загрузка, делаем что-то еще
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine($"Работаем... итерация {i}");
        await Task.Delay(500); // Пауза на 0,5 сек, имитируем работу
    }
    await downloadTask; // Дожидаемся завершения загрузки
}

Теперь программа и скачивает что-то, и не блокируется: “живая” мультизадачность, практически как у кота, который одновременно спит и наблюдает за миской.

Асинхронность ≠ многопоточность

Важное отличие, которое часто путают новички: асинхронный код может выполняться на одном и том же потоке! Асинхронность — про то, чтобы не блокировать поток, а не про то, чтобы обязательно создавать новые. Потоки ОС — дорогой ресурс! Асинхронность позволяет “отпустить” поток работать дальше, не дожидаясь длительных операций (сетевых, файловых, таймерных и т.д.).

Таблица сравнения: Когда выбирать что?

Сценарий Thread/Task async/await
CPU-bound задачи Да Да (через Task.Run)
I/O-bound задачи Неэффективно Идеально
Много параллелизма Сложно Просто
Простота кода Хардкор Легко читать

5. Полезные нюансы

Асинхронный Main

С современной версией C# можно делать и метод Main асинхронным!

static async Task Main(string[] args)
{
    // Весь ваш асинхронный код можно await-ить прямо в Main
    await DownloadAndAnalyzeFileAsync("https://example.com/file");
}

Типичная ошибка: забыли await — задача "утекла"

SomeAsyncFunction(); // не await-им, никто не ждёт окончания!

В результате эта задача будет выполняться "в пустоту" — и если возникнет ошибка, вы об этом даже не узнаете!

Когда НЕ стоит писать async-методы

  • Если внутри метода нет ни одной асинхронной операции (ни одного await), не нужно делать его async.
  • Не стоит писать методы с async void (только если вы — обработчик события).

Краткие правила и типовые вопросы

  • Можно ли использовать await вне async-метода? Нет! Всегда только внутри методов, отмеченных как async.
  • Можно ли иметь несколько await в одном методе? Да, их может быть сколько угодно — каждое “точка ожидания”.
  • Что если хочется вернуть результат? Используйте Task<T> и пишите return.
  • Можно ли комбинировать несколько асинхронных задач? Конечно! Можно запускать несколько задач параллельно и ожидать их все через Task.WhenAll.

6. Типичные ошибки при использовании async/await

Ошибка №1: "огненный шар" — await после вызова асинхронной задачи.

DownloadAndPrintLengthAsync("https://www.example.com");
Console.WriteLine("Всё выполнено!"); // На самом деле загрузка ещё идет!

Код НЕ дождётся завершения асинхронной операции. Все асинхронные задачи нужно либо await-ить, либо явно использовать Wait() (но второй способ опасен, может привести к блокировкам).

Ошибка №2: смешивание синхронного и асинхронного кода через .Result или .Wait().

Это антипаттерн, который сводит на нет все преимущества асинхронности. В UI или ASP.NET приложениях это почти гарантированно приведёт к дэдлоку (взаимной блокировке): асинхронная задача ждёт освобождения потока, а поток заблокирован в ожидании завершения этой задачи. Запомните мантру: "async all the way" (асинхронность до самого верха).

Ошибка №3: Использование async void.

Методы async void нельзя ожидать (await), а исключения, выброшенные из них, не могут быть перехвачены стандартным try-catch и обычно приводят к аварийному завершению всего приложения. Единственное допустимое применение async void — это обработчики событий (например, async void Button_Click(...)), где этого требует сигнатура. Во всех остальных случаях используйте async Task.

Ошибка №4: Лишний async в методе без await.

Если вы помечаете метод как async, но не используете внутри него await, компилятор выдаст предупреждение. Такой код будет выполняться полностью синхронно, но с дополнительными накладными расходами на создание ненужной "машины состояний". Это вводит в заблуждение и снижает производительность.

2
Задача
C# SELF, 59 уровень, 2 лекция
Недоступна
Асинхронный метод с паузой
Асинхронный метод с паузой
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ