Асинхронное программирование в C#

Что такое асинхронность и зачем она нужна?

Асинхронное программирование в C#: полное руководство с примерами

Асинхронное программирование — обязательный навык для любого C#-разработчика. Оно позволяет создавать отзывчивые приложения, которые не блокируют поток во время длительных операций (работа с файлами, запросы к сети, обращение к БД). В этой статье мы разберём ключевые концепции, научимся правильно использовать async/await и рассмотрим реальные примеры.

Что такое асинхронность и зачем она нужна?

В синхронном коде каждая операция выполняется последовательно и блокирует текущий поток до своего завершения. Если операция долгая (например, чтение большого файла), приложение «зависает» — пользователь не может взаимодействовать с интерфейсом, а сервер не обрабатывает другие запросы.

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

Основные понятия: Task, async, 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 — это элегантный способ писать неблокирующий код. Главное — понять базовые принципы, избегать типичных ошибок и всегда помнить про контекст выполнения. Надеюсь, эта статья помогла вам разобраться в теме. Экспериментируйте с кодом и внедряйте асинхронность в свои проекты!