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

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

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

Начать курс

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

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

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

В синхронном программировании каждая операция выполняется последовательно. Если метод вызывает, например, загрузку данных из сети, поток выполнения блокируется до завершения этой операции. Это приводит к «зависанию» интерфейса в десктопных приложениях или к неэффективному использованию ресурсов сервера в веб-приложениях.

Асинхронное программирование решает эту проблему: поток не блокируется, а возвращается в пул потоков для обработки других задач. Когда операция завершается, выполнение возобновляется с того места, где было прервано.

Основные преимущества асинхронности в C#:

  • Отзывчивость UI — приложение не зависает при долгих операциях.
  • Масштабируемость серверов — один поток может обслуживать множество запросов.
  • Эффективное использование ресурсов — меньше простоев потоков.

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

Ключевые слова async и await — это синтаксический сахар, который упрощает работу с асинхронными операциями. Рассмотрим базовый пример:

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class AsyncExample
{
    public async Task<string> DownloadDataAsync(string url)
    {
        using (var client = new HttpClient())
        {
            // await не блокирует поток
            string result = await client.GetStringAsync(url);
            return result;
        }
    }

    public async Task RunAsync()
    {
        Console.WriteLine("Начинаем загрузку...");
        string data = await DownloadDataAsync("https://api.example.com/data");
        Console.WriteLine($"Загружено: {data.Length} символов");
    }
}

Пояснение к коду:

  • Метод помечен async — это разрешает использование await внутри.
  • Возвращаемый тип — Task<string> (или просто Task для void-методов).
  • await приостанавливает выполнение метода до получения результата, но не блокирует поток.

Важно: Метод Main в консольном приложении C# 7.1+ тоже может быть асинхронным:

static async Task Main(string[] args)
{
    var example = new AsyncExample();
    await example.RunAsync();
}

Работа с Task и Task<T>

Тип Task представляет асинхронную операцию. Он может находиться в состояниях: ожидание (Pending), выполнение (Running), завершён (Completed), с ошибкой (Faulted) или отменён (Canceled).

Рассмотрим создание и запуск задач вручную:

public async Task ProcessDataAsync()
{
    // Создаём задачу, которая выполняется в пуле потоков
    Task<int> task = Task.Run(() =>
    {
        // Имитация долгой работы
        Task.Delay(2000).Wait();
        return 42;
    });

    // Ожидаем результат
    int result = await task;
    Console.WriteLine($"Результат: {result}");
}

Для параллельного выполнения нескольких задач используйте Task.WhenAll:

public async Task DownloadMultipleAsync()
{
    var urls = new[] { "https://site1.com", "https://site2.com", "https://site3.com" };
    var tasks = urls.Select(url => DownloadDataAsync(url));
    
    string[] results = await Task.WhenAll(tasks);
    Console.WriteLine($"Загружено страниц: {results.Length}");
}

А для обработки по мере завершения — Task.WhenAny:

public async Task FirstCompletedAsync()
{
    var task1 = Task.Delay(3000);
    var task2 = Task.Delay(1000);
    
    Task completed = await Task.WhenAny(task1, task2);
    Console.WriteLine($"Первая завершённая задача: {completed.Id}");
}

Лучшие практики: ConfigureAwait и обработка ошибок

Одна из частых проблем — захват контекста синхронизации (SynchronizationContext). В десктопных приложениях (WPF, WinForms) после await выполнение возвращается в исходный поток UI. В библиотеках и серверных приложениях это может вызвать deadlock (взаимную блокировку).

Решение — использовать ConfigureAwait(false):

public async Task<string> ReadFileAsync(string path)
{
    using (var reader = new StreamReader(path))
    {
        // Не возвращаемся в исходный контекст
        string content = await reader.ReadToEndAsync().ConfigureAwait(false);
        return content;
    }
}

Когда использовать ConfigureAwait(false):

  • В библиотечном коде (классы, которые не управляют UI).
  • В серверных приложениях (ASP.NET Core).
  • Если не требуется доступ к UI после await.

Обработка ошибок в асинхронном коде стандартна — через try-catch:

public async Task SafeOperationAsync()
{
    try
    {
        await DownloadDataAsync("https://invalid.url");
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"Ошибка сети: {ex.Message}");
    }
    catch (TaskCanceledException)
    {
        Console.WriteLine("Операция отменена");
    }
}

Также можно использовать CancellationToken для отмены операций:

public async Task CancellableOperationAsync(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(1000, token);
        Console.WriteLine($"Шаг {i}");
    }
}

// Использование:
var cts = new CancellationTokenSource();
cts.CancelAfter(3000); // Отмена через 3 секунды
await CancellableOperationAsync(cts.Token);

Заключение

Асинхронное программирование в C# — мощный инструмент, который стал

Блоги

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