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

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в асинхронное программирование в 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'ам и снижению производи

Блоги

Рекомендаций по книгам