Асинхронное программирование в C#: полное руководство с примерами
Асинхронное программирование — обязательный навык для любого C#-разработчика. Оно позволяет создавать отзывчивые приложения, которые не блокируют поток во время длительных операций (работа с файлами, запросы к сети, обращение к БД). В этой статье мы разберём ключевые концепции, научимся правильно использовать async/await и рассмотрим реальные примеры.
Что такое асинхронность и зачем она нужна?
В синхронном коде каждая операция выполняется последовательно и блокирует текущий поток до своего завершения. Если операция долгая (например, чтение большого файла), приложение «зависает» — пользователь не может взаимодействовать с интерфейсом, а сервер не обрабатывает другие запросы.
Асинхронный код позволяет запустить длительную операцию и вернуть управление вызывающему коду, а когда операция завершится — продолжить выполнение. Поток не блокируется и может заниматься другой работой.
Основные понятия: Task, async, await
- Task — представляет асинхронную операцию. Может возвращать значение (
Task<TResult>) или не возвращать (Task). - async — модификатор метода, который позволяет использовать внутри
await. Такой метод должен возвращатьTask,Task<T>илиvoid(только для обработчиков событий). - await — оператор, который приостанавливает выполнение метода до завершения ожидаемой задачи, при этом не блокируя поток.
Пример синхронного кода и его проблемы
Представьте, что мы скачиваем данные из интернета и сохраняем их в файл. Синхронная версия:
public void DownloadAndSave()
{
string data = new WebClient().DownloadString("https://example.com/data");
File.WriteAllText("data.txt", data);
Console.WriteLine("Готово!");
}
Пока выполняется DownloadString и WriteAllText, поток приложения заблокирован. В GUI-приложении интерфейс перестанет отвечать, в веб-приложении поток пула будет занят и не сможет обрабатывать другие запросы.
Переписываем на асинхронный лад
Используем асинхронные версии методов (DownloadStringTaskAsync и WriteAllTextAsync):
public async Task DownloadAndSaveAsync()
{
using (var client = new HttpClient())
{
string data = await client.GetStringAsync("https://example.com/data");
await File.WriteAllTextAsync("data.txt", data);
Console.WriteLine("Готово!");
}
}
Метод помечен async и возвращает Task. Внутри мы используем await — во время ожидания скачивания или записи поток освобождается. После завершения операции выполнение продолжается с того же места.
Обработка ошибок в асинхронных методах
Исключения в асинхронных методах обрабатываются стандартным try-catch, но есть нюанс: если метод возвращает Task, исключение сохраняется в задачу и выбрасывается при ожидании (await).
public async Task ProcessAsync()
{
try
{
await DownloadAndSaveAsync();
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Ошибка сети: {ex.Message}");
}
catch (IOException ex)
{
Console.WriteLine($"Ошибка ввода-вывода: {ex.Message}");
}
}
Важно: никогда не используйте async void кроме обработчиков событий — исключения в таких методах невозможно поймать снаружи, и они могут уронить приложение.
Советы по производительности и лучшие практики
1. Избегайте async void
Всегда возвращайте Task или Task<T> из асинхронных методов, если только это не обработчик события (например, в WPF или WinForms).
2. Используйте ConfigureAwait(false) в библиотеках
Если вы пишете код, который не взаимодействует с UI или контекстом ASP.NET, добавляйте ConfigureAwait(false), чтобы избежать лишнего захвата контекста синхронизации и повысить производительность.
await File.WriteAllTextAsync("data.txt", data).ConfigureAwait(false);
3. ValueTask для часто используемых операций
Если метод часто завершается синхронно (например, чтение из кэша), можно вернуть ValueTask/ValueTask<T>, чтобы избежать выделения объекта Task. Но используйте осторожно — ValueTask нельзя ожидать несколько раз.
4. Не блокируйте асинхронный код
Никогда не используйте .Result или .Wait() для асинхронных методов — это приводит к взаимоблокировкам (deadlock) в окружениях с контекстом синхронизации (UI, ASP.NET).
Практический пример: асинхронное чтение файла и запрос к API
Допустим, нам нужно прочитать список URL из файла, скачать каждый из них и сохранить результаты в отдельные файлы. Вот как это можно реализовать асинхронно:
public async Task ProcessUrlsAsync(string filePath)
{
var urls = await File.ReadAllLinesAsync(filePath);
var tasks = urls.Select(async url =>
{
using var client = new HttpClient();
string content = await client.GetStringAsync(url);
string fileName = $"{Guid.NewGuid()}.html";
await File.WriteAllTextAsync(fileName, content);
Console.WriteLine($"Сохранено: {fileName}");
});
await Task.WhenAll(tasks);
}
Task.WhenAll позволяет запустить все задачи параллельно и дождаться их завершения. Если нужна обработка ошибок по каждой задаче, можно обернуть тело Select в try-catch.
Заключение
Асинхронное программирование в C# с async/await — это элегантный способ писать неблокирующий код. Главное — понять базовые принципы, избегать типичных ошибок и всегда помнить про контекст выполнения. Надеюсь, эта статья помогла вам разобраться в теме. Экспериментируйте с кодом и внедряйте асинхронность в свои проекты!