Кружок Программирования

Ключевое слово async в .NET. Асинхронное программирование в ASP.NET

Начиная с .NET Framework 4.5 в язык C# были добавлены ключевые слова async и await, значительно упростившие написание асинхронного кода. Сегодня практически все современные приложения на .NET используют асинхронное программирование: веб-сайты, REST API, облачные сервисы и даже многие настольные приложения.

В ASP.NET Framework 4.8 поддержка async/await полностью присутствует, поэтому даже старые проекты могут использовать асинхронные методы. В более современных платформах (.NET 6, .NET 7, .NET 8 и новее) асинхронное программирование стало фактически стандартом разработки.

Что такое async

Ключевое слово async говорит компилятору, что данный метод содержит асинхронные операции и может использовать оператор await.

Само по себе слово async ничего не делает — оно лишь разрешает использовать await внутри метода.

📋
public async Task LoadDataAsync()
{
    await SomeMethodAsync();
}

Что делает await

Оператор await ожидает завершения асинхронной операции. При этом поток не блокируется и может выполнять другую работу.

Когда операция завершается, выполнение метода продолжается с той строки, которая следует за await.

📋
public async Task Example()
{
    Console.WriteLine("Начало");

    await Task.Delay(3000);

    Console.WriteLine("Прошло три секунды");
}

Во время ожидания поток не простаивает, а может обслуживать другие запросы.

Что происходит при выполнении await на самом деле?

Это один из самых распространённых вопросов об асинхронности в .NET. На первый взгляд кажется, что после выполнения оператора await автоматически создаётся новый поток, затем ещё один, и выполнение метода постепенно «прыгает» между потоками. Однако в действительности это не так.

Оператор await сам по себе никогда не создаёт новый поток. Он лишь говорит: «Если выполняемая операция ещё не закончилась, сохрани текущее состояние метода и верни управление вызывающему коду. Когда операция завершится, продолжи выполнение метода».

Именно поэтому правильнее думать не о потоках, а о продолжении (continuation), которое будет выполнено позже.

Разберём выполнение метода по шагам

Предположим, имеется следующий код:

📋
public async Task Example()
{
    Console.WriteLine("A");

    await Task.Delay(5000);

    Console.WriteLine("B");
}

Последовательность событий выглядит следующим образом.

  1. Метод начинает выполняться в некотором потоке.
  2. Доходит до оператора await.
  3. Если операция ещё не завершилась, метод временно прекращает выполнение.
  4. Текущий поток освобождается и может заниматься другой работой.
  5. Когда операция завершается, планировщик запускает продолжение метода.
  6. Выполнение продолжается со строки после await.

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

  • в том же самом потоке;
  • в другом существующем потоке из ThreadPool;
  • в UI-потоке (например, WinForms или WPF);
  • в потоке ASP.NET, если это требуется средой выполнения.

Создаётся ли поток для выполнения await?

Нет.

Это очень важное отличие между await и Task.Run().

Например:

📋
await Task.Delay(5000);

Здесь вообще никакой поток не работает все пять секунд. Операционная система просто заводит таймер. Когда таймер срабатывает, .NET получает уведомление и только тогда планирует продолжение метода.

А вот такой код действительно запускает работу в отдельном потоке ThreadPool:

📋
await Task.Run(() =>
{
    HeavyCalculation();
});

Здесь уже создаётся задача, которая будет выполнена одним из потоков пула.

Можно ли представить это как потоки А → Б → В?

Не совсем.

Более правильная схема выглядит так:

📋
Поток А

↓

код до await

↓

ожидание операции

↓

Поток освобождается

↓

операция завершилась

↓

любой подходящий поток

↓

код после await

То есть после await выполнение может продолжиться вообще в любом доступном рабочем потоке. Никаких специальных потоков «Б» или «В» создавать не требуется.

Что происходит с ThreadLocal?

Да, здесь возникает очень важная особенность.

Если после await выполнение продолжилось уже в другом потоке, то все значения, хранящиеся в ThreadLocal<T>, окажутся другими.

Например:

📋
ThreadLocal<int> number =
    new ThreadLocal<int>();

number.Value = 10;

await Task.Delay(1000);

Console.WriteLine(number.Value);

После await вполне возможно получить не 10, а значение по умолчанию, поскольку продолжение выполнилось уже в другом потоке.

Поэтому хранить важные данные в ThreadLocal при использовании async/await обычно не рекомендуется.

Что использовать вместо ThreadLocal?

Для асинхронного кода в .NET существует специальный класс AsyncLocal<T>.

В отличие от ThreadLocal он привязан не к потоку, а к асинхронному контексту выполнения.

Даже если после await выполнение перейдёт в другой поток, значение AsyncLocal сохранится.

📋
AsyncLocal<string> user =
    new AsyncLocal<string>();

user.Value = "Roman";

await Task.Delay(1000);

Console.WriteLine(user.Value);

В этом случае будет выведено значение Roman, независимо от того, на каком потоке продолжилось выполнение.

Правда ли, что async превращает метод в конечный автомат?

Да. Это абсолютно верное утверждение.

При компиляции метод, содержащий ключевые слова async и await, преобразуется компилятором C# в специальный класс, реализующий так называемую машину состояний (state machine).

Именно поэтому слово async почти ничего не делает во время выполнения программы. Его основная задача — сообщить компилятору, что метод необходимо преобразовать особым образом.

Что такое конечный автомат?

Конечный автомат — это объект, который может находиться в одном из нескольких состояний и переходить между ними.

Например, обычный светофор является конечным автоматом:

📋
Красный

↓

Красный + Жёлтый

↓

Зелёный

↓

Жёлтый

↓

Красный

В каждый момент времени светофор находится только в одном состоянии. После наступления некоторого события он переходит в следующее состояние.

Как выглядит конечный автомат для async-метода?

Предположим, имеется следующий метод:

📋
public async Task Test()
{
    Step1();

    await A();

    Step2();

    await B();

    Step3();
}

После компиляции его логика становится примерно такой:

📋
State = 0

↓

Step1()

↓

await A()

↓

State = 1

↓

ожидание

↓

State == 1

↓

Step2()

↓

await B()

↓

State = 2

↓

ожидание

↓

State == 2

↓

Step3()

↓

завершение

То есть компилятор буквально разбивает метод на несколько частей. Перед каждым await он сохраняет текущее состояние объекта, а после завершения ожидаемой операции продолжает выполнение с нужного места.

Почему именно конечный автомат позволяет реализовать await?

Если бы метод был обычной последовательностью инструкций, после выхода из него невозможно было бы «вернуться» ровно к той строке, где встретился await.

Машина состояний сохраняет всю необходимую информацию:

  • номер текущего состояния;
  • значения локальных переменных;
  • ссылки на ожидаемые задачи;
  • место, с которого нужно продолжить выполнение.

Благодаря этому после завершения асинхронной операции выполнение продолжается точно с той строки, которая следует за соответствующим оператором await, словно метод никогда не прерывался.

Почему обычный код может создавать проблемы

Предположим, страница должна получить данные из базы данных.

📋
var users = repository.GetUsers();

Если выполнение SQL-запроса занимает одну секунду, поток сервера всё это время ничего не делает, а просто ждёт ответа базы данных.

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

Как работает async

При использовании асинхронного метода поток начинает выполнение запроса, после чего освобождается и может обслуживать других пользователей. Когда база данных возвращает результат, ASP.NET продолжает выполнение метода.

📋
var users = await repository.GetUsersAsync();

Таким образом один и тот же сервер способен обслужить значительно больше одновременных запросов.

Важно понимать

Асинхронность не делает запрос к базе данных быстрее.

Если SQL-запрос выполняется две секунды, то и синхронный, и асинхронный вариант будут ждать примерно две секунды.

Разница заключается в том, что во время ожидания асинхронный вариант освобождает рабочий поток ASP.NET.

Какой выигрыш это даёт

Представим, что сервер имеет 100 рабочих потоков.

Если каждый запрос ожидает базу данных по одной секунде, то синхронная версия сможет одновременно обслуживать примерно 100 пользователей.

При использовании async эти потоки освобождаются во время ожидания, поэтому те же самые 100 потоков смогут обслужить значительно большее количество одновременных запросов.

Поэтому асинхронность увеличивает не скорость одного пользователя, а общую производительность веб-сервера.

Возвращаемые типы async-методов

Асинхронный метод обычно возвращает один из следующих типов.

Тип Назначение
Task Метод ничего не возвращает.
Task<T> Метод возвращает результат типа T.
ValueTask Более экономичная альтернатива Task для некоторых сценариев.
void Используется почти исключительно для обработчиков событий.

Асинхронные страницы в ASP.NET Web Forms

В ASP.NET Framework 4.8 страницы Web Forms могут работать асинхронно. Для этого необходимо добавить атрибут Async="true".

📋
<%@ Page Language="C#" Async="true" %>

После этого обработчики страницы могут быть объявлены как async.

📋
protected async void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        await LoadUsersAsync();
    }
}

Без атрибута Async="true" использование await внутри страницы Web Forms невозможно.

Асинхронность в ASP.NET MVC

В ASP.NET MVC контроллеры также могут быть асинхронными.

📋
public async Task<ActionResult> Index()
{
    var users = await repository.GetUsersAsync();

    return View(users);
}

Асинхронность в ASP.NET Core

В ASP.NET Core практически вся инфраструктура построена вокруг Task и async. Большинство библиотек сразу предоставляют асинхронные версии методов.

Сегодня считается хорошей практикой писать все операции ввода-вывода в асинхронном виде.

Когда стоит использовать async

Асинхронность рекомендуется применять для:

  • запросов к базе данных;
  • HTTP-запросов;
  • работы с файлами;
  • обращений к облачным сервисам;
  • сетевых операций;
  • длительных операций ввода-вывода.

Когда async использовать не нужно

Если метод выполняет только вычисления процессора и не ожидает никаких внешних операций, async не принесёт пользы.

📋
public int Sum(int a, int b)
{
    return a + b;
}

Такой метод должен оставаться обычным синхронным.

Асинхронное сохранение счётчика посещений страницы

Предположим, при каждом открытии страницы необходимо увеличить счётчик посещений в базе данных.

Вместо синхронного вызова:

📋
CounterRepository.Increment(pageId);

лучше использовать асинхронную версию:

📋
await CounterRepository.IncrementAsync(pageId);

Пример реализации метода с использованием ADO.NET:

📋
public async Task IncrementAsync(int pageId)
{
    using (var connection = new SqlConnection(connectionString))
    {
        await connection.OpenAsync();

        var command = new SqlCommand(
            "UPDATE Pages SET Visits = Visits + 1 WHERE Id = @id",
            connection);

        command.Parameters.AddWithValue("@id", pageId);

        await command.ExecuteNonQueryAsync();
    }
}

Во время выполнения SQL-запроса поток ASP.NET освобождается и может обслуживать других пользователей.

Стоит ли делать запись счётчика полностью асинхронной?

Если посещаемость сайта небольшая, приведённого выше решения вполне достаточно.

Однако на высоконагруженных сайтах даже ожидание завершения SQL-запроса может быть нежелательным. В этом случае запрос пользователя можно вообще не заставлять ждать обновления счётчика.

Популярные варианты:

  • поместить запись в очередь сообщений (RabbitMQ, Azure Service Bus и т.п.);
  • записывать посещения в память и периодически сохранять их в базу;
  • использовать отдельный фоновый сервис для обработки счётчиков.

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

Итоги

Ключевые слова async и await позволяют писать асинхронный код практически так же просто, как обычный синхронный. Они особенно полезны при работе с базами данных, файлами и сетевыми сервисами, где значительная часть времени тратится на ожидание ответа.

В ASP.NET Framework 4.8 использование асинхронных страниц остаётся полностью актуальным, особенно для сайтов с высокой посещаемостью. В современных версиях .NET (ASP.NET Core и .NET 6–10) асинхронное программирование стало основной моделью разработки, и большинство библиотек изначально ориентированы на использование async/await.

Страница создана 07.07.2026
Изменена 11.07.2026
Просмотров 3
🖌️🎨