Как и когда использовать ‘async’ и ‘await’ в C#
По моему пониманию, одной из основных целей async и await является упрощение написания и чтения кода. Но действительно ли их использование создает фоновые потоки для выполнения длительных операций?
Я пытаюсь понять базовое поведение async/await на следующем примере. Не могли бы кто-то прояснить эти моменты для меня?
// Почему этот метод должен быть помечен как `async`?
private async void button1_Click(object sender, EventArgs e)
{
Task<int> access = DoSomethingAsync();
// Код, не зависящий от Task, здесь
// Эта строка достигается после 5-секундной паузы из
// DoSomethingAsync(). Не должна ли она достигаться немедленно?
int a = 1;
// По моему пониманию, ожидание должно происходить здесь.
int x = await access;
}
async Task<int> DoSomethingAsync()
{
// Этот код выполняется на фоновом потоке?
System.Threading.Thread.Sleep(5000);
return 1;
}
Конкретно, я хотел бы понять:
- Почему методы, использующие
await, должны быть помечены какasync - Как работает выполнение потоков с
asyncиawait - Время выполнения кода в асинхронных методах
- Создают ли
async/awaitавтоматически фоновые потоки
Ключевые слова async и await в C# предоставляют мощный способ написания асинхронного кода, который выглядит более линейным и читабельным, но они не автоматически создают фоновые потоки. Вместо этого они используют механизм машины состояний для управления потоком выполнения без блокировки потоков.
Содержание
- Основы Async/Await
- Почему методы, использующие Await, должны быть помечены как Async
- Как работает выполнение потоков с Async/Await
- Время выполнения кода в асинхронных методах
- Создают ли Async/Await автоматически фоновые потоки?
- Лучшие практики и распространенные шаблоны
- Устранение распространенных проблем
Основы Async/Await
Ключевое слово await предоставляет неблокирующий способ запуска задачи, после которого выполнение продолжается при завершении задачи. В отличие от традиционного асинхронного программирования, требовавшего сложных обратных вызовов (callback), async/await позволяет писать асинхронный код, который читается как синхронный.
Когда вы вызываете асинхронный метод, он начинает выполняться в текущем потоке до тех пор, пока не встретит ключевое слово await. В этот момент выполнение метода приостанавливается, и управление возвращается вызывающему методу источник.
Почему методы, использующие Await, должны быть помечены как Async
Методы, использующие await, должны быть помечены ключевым словом async, потому что:
-
Требование компилятора: Компилятору C# необходимо обнаруживать методы, содержащие ключевые слова
await, чтобы преобразовать их в машины состояний. Без модификатораasyncкомпилятор сгенерирует ошибку компиляции. -
Генерация машины состояний: Когда вы помечаете метод как
async, компилятор преобразует его в машину состояний, которая может приостанавливать и возобновлять выполнение. Эта машина состояний отслеживает текущую точку выполнения и управляет продолжением при завершении ожидаемой операции. -
Гибкость типов возвращаемых значений: Модификатор
asyncпозволяет методам возвращатьTask,Task<T>илиvoid(для обработчиков событий), что является essential для асинхронной модели программирования.
Важно: Только методы, содержащие
awaitилиreturn Task/return Task<T>, нужно помечать какasync. Помечение метода какasyncбез использованияawaitсоздает ненужные накладные расходы.
Как работает выполнение потоков с Async/Await
Поток выполнения в вашем примере работает следующим образом:
private async void button1_Click(object sender, EventArgs e)
{
// 1. Метод начинается в потоке пользовательского интерфейса
Task<int> access = DoSomethingAsync();
// Код, не зависящий от задачи, выполняется сразу
int a = 1;
// 2. При достижении этой строки выполнение метода приостанавливается,
// и управление возвращается в вызывающий контекст
int x = await access;
// 3. Этот код выполняется только после завершения DoSomethingAsync
// (после 5-секундной задержки)
}
Согласно Microsoft Learn, внутри асинхронного метода:
- Код, связанный с вводом-выводом, запускает операцию, представленную объектом
TaskилиTask<T> - Код, связанный с процессором, должен запускаться в фоновом потоке с помощью
Task.Run
Время выполнения кода в асинхронных методах
В вашем примере время работы выглядит так:
- Начальное выполнение:
button1_Clickначинает выполняться в потоке пользовательского интерфейса - Создание задачи:
DoSomethingAsync()вызывается и немедленно возвращаетTask<int>, не дожидаясь завершения - Немедленное выполнение:
int a = 1;выполняется сразу, так как не зависит от задачи - Точка приостановки: При достижении
await access;метод приостанавливается - Возврат к вызывающему коду: Управление возвращается в цикл обработки сообщений UI, поддерживая отзывчивость интерфейса
- Продолжение: После завершения
DoSomethingAsyncостальная частьbutton1_Clickвозобновляет выполнение
Путаница возникает из-за того, что Task<int> access = DoSomethingAsync(); на самом деле не ожидает завершения операции - он просто создает задачу. Ожидание происходит в точке await.
Создают ли Async/Await автоматически фоновые потоки?
Нет, async и await не автоматически создают фоновые потоки. Это важное заблуждение, которое есть у многих разработчиков.
Как четко указано в Microsoft Learn:
Ключевые слова async и await не вызывают создания дополнительных потоков. Асинхронные методы не требуют многопоточности, потому что асинхронный метод не выполняется в своем собственном потоке. Метод выполняется в текущем контексте синхронизации и использует время в потоке только тогда, когда метод фактически выполняет работу.
В вашем примере:
async Task<int> DoSomethingAsync()
{
// Этот код выполняется в том же потоке, из которого он был вызван
System.Threading.Thread.Sleep(5000);
return 1;
}
Thread.Sleep(5000) фактически блокирует поток, в котором он выполняется, что противоречит цели асинхронного программирования. Для работы, связанной с процессором, следует использовать Task.Run:
async Task<int> DoSomethingAsync()
{
// Это выполняется в фоновом потоке из пула потоков
return await Task.Run(() =>
{
System.Threading.Thread.Sleep(5000);
return 1;
});
}
Лучшие практики и распространенные шаблоны
1. Используйте Async на всем протяжении
// Не делайте так - блокирующий асинхронный код
public async Task<string> GetDataAsync()
{
var result = await GetDataFromDatabaseAsync();
Thread.Sleep(1000); // Блокирует поток
return result;
}
// Делайте так - действительно асинхронный код
public async Task<string> GetDataAsync()
{
var result = await GetDataFromDatabaseAsync();
await Task.Delay(1000); // Неблокирующая задержка
return result;
}
2. Используйте ConfigureAwait для кода библиотек
// Для кода библиотеки используйте ConfigureAwait(false)
public async Task<int> CalculateAsync()
{
// Не нужно возвращаться в исходный контекст для работы
var result = await DoWorkAsync().ConfigureAwait(false);
return result;
}
3. Правильно обрабатывайте исключения
try
{
var result = await SomeOperationAsync();
}
catch (Exception ex)
{
// Обработка исключений из асинхронных операций
}
Устранение распространенных проблем
Взаимные блокировки
Возникают при смешивании блокирующего и асинхронного кода, особенно в потоках пользовательского интерфейса.
Истощение пула потоков
Может возникать при выполнении слишком многих операций, связанных с процессором, с помощью Task.Run без соответствующих ограничений.
Асинхронные методы void
Следует использовать только для обработчиков событий. Обычные асинхронные методы должны возвращать Task или Task<T>.
В заключение, async и await - это конструкции на уровне компилятора, которые управляют потоком выполнения без необходимости создания новых потоков. Они делают ваш код более читаемым и отзывчивым, позволяя писать асинхронный код, который выглядит линейным, но базовое управление потоками требует понимания модели асинхронного программирования на основе задач (Task-based Asynchronous Pattern, TAP).
Источники
- C# Async/Await Explained: Complete Guide with Examples [2025] - NDepend Blog
- Asynchronous programming scenarios - C# | Microsoft Learn
- How Async/Await Really Works in C# - .NET Blog
- C# Asynchronous Programming: Tasks, Threads, and Async/Await | Medium
- Asynchronous programming - C# | Microsoft Learn
- The Task Asynchronous Programming (TAP) model with async and await - Microsoft Learn
- Asynchronous programming with async, await, Task in C# - TutorialTeacher
- If async-await doesn’t create any additional threads, then how does it make applications responsive? - Stack Overflow
- Difference Between Asynchronous and Multithreading in C# - Code Maze
- C# “async and await” feature and threads - Stack Overflow
Заключение
- Async/await не создает фоновые потоки - он управляет потоком выполнения с помощью машин состояний
- Методы, использующие await, должны быть async, потому что компилятору нужно генерировать машину состояний
- Время выполнения кода - метод приостанавливается в await и продолжается после завершения, освобождая потоки
- Работа, связанная с процессором, должна использовать
Task.Runдля фактического переноса работы в фоновые потоки - Работа, связанная с вводом-выводом, выигрывает от использования async/await без создания новых потоков
- Лучшая практика - использовать async на всем протяжении вашего приложения, избегая блокирующих вызовов в асинхронных методах
Понимание этих основ поможет вам писать более эффективные и отзывчивые приложения, избегая распространенных ловушек, таких как взаимные блокировки и истощение потоков.