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

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

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

Начать курс

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

Асинхронное программирование — одна из ключевых концепций современной разработки на C#. В эпоху высоконагруженных веб-приложений, микросервисов и облачных решений умение эффективно работать с асинхронностью стало обязательным навыком для каждого .NET-разработчика.

Основная цель асинхронного программирования — не блокировать поток выполнения во время ожидания длительных операций (ввод-вывод, работа с сетью, запросы к базе данных). Вместо этого поток освобождается для обработки других задач, что значительно повышает производительность и масштабируемость приложения.

В этой статье мы разберем:

  • Основы async/await в C#
  • Работу с классом Task
  • Правила написания асинхронных методов
  • Типичные ошибки и способы их избежать

Основы async/await в C#

Ключевая парадигма асинхронного программирования в C# строится на двух ключевых словах: async и await. Они были введены в C# 5.0 и с тех пор стали стандартом для работы с асинхронностью.

Как работает async/await

Ключевое слово async используется для объявления асинхронного метода. await применяется к задаче (Task), чтобы приостановить выполнение метода до ее завершения, не блокируя при этом текущий поток.

public async Task<string> GetDataAsync()
{
    // Имитация асинхронного HTTP-запроса
    using var client = new HttpClient();
    string result = await client.GetStringAsync("https://api.example.com/data");
    return result;
}

public async Task ProcessAsync()
{
    string data = await GetDataAsync();
    Console.WriteLine($"Получены данные: {data}");
}

Важно понимать: await не блокирует поток. Он передает управление вызывающему коду, а когда задача завершается, метод продолжает выполнение с того же места.

Типы возвращаемых значений асинхронных методов

Асинхронный метод может возвращать один из трех типов:

  • Task — для методов, не возвращающих значение
  • Task<T> — для методов, возвращающих значение типа T
  • ValueTask<T> — для сценариев с высокой производительностью, когда результат часто доступен синхронно
// Без возвращаемого значения
public async Task SaveDataAsync(string data)
{
    await File.WriteAllTextAsync("data.txt", data);
}

// С возвращаемым значением
public async Task<int> CalculateSumAsync(int a, int b)
{
    await Task.Delay(100); // Имитация длительной операции
    return a + b;
}

// ValueTask для оптимизации
public async ValueTask<bool> CheckCacheAsync(string key)
{
    if (_cache.ContainsKey(key))
        return true;
    
    await Task.Delay(50);
    return false;
}

Продвинутая работа с Task

Класс Task — основа асинхронного программирования в C#. Рассмотрим наиболее полезные методы и паттерны.

Параллельное выполнение задач

Методы Task.WhenAll и Task.WhenAny позволяют эффективно управлять несколькими асинхронными операциями.

public async Task ProcessMultipleRequestsAsync()
{
    var task1 = FetchDataAsync("url1");
    var task2 = FetchDataAsync("url2");
    var task3 = FetchDataAsync("url3");
    
    // Ожидание завершения всех задач
    await Task.WhenAll(task1, task2, task3);
    
    Console.WriteLine("Все запросы завершены");
}

public async Task<string> GetFirstResponseAsync()
{
    var task1 = FetchDataAsync("url1");
    var task2 = FetchDataAsync("url2");
    
    // Ожидание первой завершившейся задачи
    Task<string> completedTask = await Task.WhenAny(task1, task2);
    return await completedTask;
}

Отмена асинхронных операций

Для корректной отмены длительных операций используйте CancellationToken.

public async Task<string> DownloadWithCancellationAsync(
    string url, CancellationToken cancellationToken)
{
    using var client = new HttpClient();
    
    try
    {
        var response = await client.GetAsync(url, cancellationToken);
        return await response.Content.ReadAsStringAsync();
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Операция отменена");
        return null;
    }
}

// Использование
public async Task ExecuteWithTimeoutAsync()
{
    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
    
    try
    {
        string result = await DownloadWithCancellationAsync(
            "https://api.example.com/large-data", cts.Token);
        Console.WriteLine(result);
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Таймаут операции");
    }
}

Типичные ошибки и лучшие практики

Асинхронное программирование требует особой внимательности. Рассмотрим наиболее частые проблемы.

Ошибка 1: Блокировка асинхронного кода

Использование .Result или .Wait() на Task приводит к блокировке потока и может вызвать deadlock.

// ❌ ПЛОХО: блокировка потока
public string GetDataBlocking()
{
    return GetDataAsync().Result; // Может вызвать deadlock!
}

// ✅ ХОРОШО: асинхронный подход
public async Task<string> GetDataCorrectAsync()
{
    return await GetDataAsync();
}

Ошибка 2: Игнорирование ConfigureAwait

В библиотечном коде (не UI-приложениях) используйте ConfigureAwait(false) для предотвращения возврата в исходный контекст синхронизации.

public async Task<string> ReadFileAsync(string path)
{
    using var stream = File.OpenRead(path);
    using var reader = new StreamReader(stream);
    
    // Для библиотечного кода
    string content = await reader.ReadToEndAsync()
        .ConfigureAwait(false);
    
    return content;
}

Лучшие практики