Введение в асинхронное программирование в C#
Асинхронное программирование — это фундаментальная парадигма в современной разработке на C# (.NET), позволяющая создавать отзывчивые и производительные приложения. В отличие от синхронного подхода, где выполнение кода блокирует текущий поток до завершения операции, асинхронный код освобождает поток для выполнения других задач, пока ожидается результат длительной операции (например, запрос к базе данных, HTTP-вызов или чтение файла).
С появлением ключевых слов async и await в C# 5.0 асинхронное программирование перестало быть сложным и стало доступным каждому разработчику. В этом руководстве мы разберем все ключевые концепции: от основ Task и ValueTask до обработки ошибок и отмены операций.
Основы async/await и Task
Что такое Task и Task<TResult>
Task — это центральный тип в TPL (Task Parallel Library), представляющий собой асинхронную операцию. Task (без возвращаемого значения) и Task<TResult> (с возвращаемым значением) являются строительными блоками асинхронного кода.
// Простой асинхронный метод без возвращаемого значения
public async Task DoSomethingAsync()
{
await Task.Delay(1000);
Console.WriteLine("Готово!");
}
// Асинхронный метод с возвращаемым значением
public async Task<int> CalculateSumAsync(int a, int b)
{
await Task.Delay(500); // Имитация длительной операции
return a + b;
}
Ключевые слова async и await
Модификатор async указывает компилятору, что метод содержит асинхронный код. Оператор await приостанавливает выполнение метода до завершения ожидаемой задачи, не блокируя поток.
public async Task ProcessDataAsync()
{
Console.WriteLine("Начало обработки...");
// Асинхронное ожидание — поток не блокируется
string result = await FetchDataFromApiAsync();
Console.WriteLine($"Получен результат: {result}");
}
Правила использования async/await
- Не используйте async void (кроме обработчиков событий) — это делает невозможным отслеживание исключений.
- Избегайте блокирующих вызовов (
.Result,.Wait()) — они могут привести к deadlock'ам. - Называйте асинхронные методы с суффиксом "Async" для ясности.
Продвинутые техники: ValueTask, CancellationToken и параллелизм
ValueTask vs Task
ValueTask — это оптимизация для случаев, когда асинхронная операция часто завершается синхронно (например, кэшированные данные). В отличие от Task, который является ссылочным типом, ValueTask — структура, что уменьшает нагрузку на сборщик мусора.
public ValueTask<int> GetCachedOrFetchAsync(int id)
{
if (cache.ContainsKey(id))
{
// Синхронное завершение — без выделения памяти
return new ValueTask<int>(cache[id]);
}
// Асинхронное завершение
return new ValueTask<int>(FetchFromDatabaseAsync(id));
}
Отмена операций с CancellationToken
CancellationToken позволяет корректно прерывать длительные асинхронные операции. Всегда передавайте токен отмены, если метод его принимает.
public async Task<string> DownloadFileAsync(string url, CancellationToken ct)
{
using var client = new HttpClient();
var response = await client.GetAsync(url, ct); // ct передается в API
return await response.Content.ReadAsStringAsync();
}
// Использование
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5)); // Таймаут 5 секунд
try
{
string data = await DownloadFileAsync("https://example.com", cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Операция отменена по таймауту.");
}
Параллельное выполнение задач
Для запуска нескольких асинхронных операций одновременно используйте Task.WhenAll или Task.WhenAny.
public async Task ProcessMultipleAsync()
{
var task1 = FetchDataAsync("url1");
var task2 = FetchDataAsync("url2");
var task3 = FetchDataAsync("url3");
// Ожидаем завершения всех задач
string[] results = await Task.WhenAll(task1, task2, task3);
foreach (var result in results)
{
Console.WriteLine(result);
}
}
// Или с WhenAny для получения первого завершенного
public async Task<string> GetFirstResponseAsync()
{
var task1 = FetchDataAsync("fast-server");
var task2 = FetchDataAsync("slow-server");
Task<string> firstCompleted = await Task.WhenAny(task1, task2);
return await firstCompleted;
}
Обработка ошибок и лучшие практики
Исключения в асинхронном коде
Исключения в асинхронных методах обрабатываются стандартным блоком try-catch. Однако есть нюансы при использовании Task.WhenAll — все исключения агрегируются в AggregateException.
public async Task HandleExceptionsAsync()
{
try
{
var tasks = new[]
{
RiskyOperationAsync(1),
RiskyOperationAsync(2),
RiskyOperationAsync(3)
};
await Task.WhenAll(tasks);
}
catch (Exception ex) when (ex is HttpRequestException or InvalidOperationException)
{
// Фильтр исключений — C# 6+
Console.WriteLine($"Ошибка сети или логики: {ex.Message}");
}
catch (AggregateException aggEx)
{
// Обработка нескольких исключений
foreach (var inner in aggEx.InnerExceptions)
{
Console.WriteLine($"Внутреннее исключение: {inner.Message}");
}
}
}
Лучшие практики асинхронного программирования
- ConfigureAwait(false) — используйте в библиотечном коде, чтобы избежать захвата контекста синхронизации (особенно в ASP.NET Core).
- Избегайте async void — за исключением обработчиков событий (например, Button_Click в WPF).
- Не смешивайте синхронный и асинхронный код — это приводит к deadlock'ам и снижению производи