Форум программистов, компьютерный форум, киберфорум
UnmanagedCoder
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

Async/await в C#: Асинхронное программировани­е в действии

Запись от UnmanagedCoder размещена 19.03.2025 в 20:10
Показов 5942 Комментарии 0

Нажмите на изображение для увеличения
Название: 528b32ba-b25f-41f8-8efd-f2430c9486a8.jpg
Просмотров: 234
Размер:	179.4 Кб
ID:	10461
Разработка современных приложений немыслима без понимания асинхронного программирования. Если вы когда-либо создавали приложение, которое взаимодействует с внешними ресурсами — будь то работа с файлами, сетевые запросы или обращения к базам данных — вы наверняка сталкивались с проблемой "зависания" пользовательского интерфейса или снижения производительности сервера при большой нагрузке.

Проблемы синхронного кода



Традиционный синхронный подход к программированию имеет серьезные ограничения, особенно в контексте современных приложений. Представьте ситуацию: пользователь нажимает кнопку в приложении WPF для загрузки данных с сервера. При синхронном выполнении этой операции интерфейс приложения "замораживается" до завершения запроса. Пользователь не может взаимодействовать с приложением, не видит индикации прогресса и часто думает, что программа "зависла".

C#
1
2
3
4
5
6
7
private void Button_Click(object sender, RoutedEventArgs e)
{
    var httpClient = new HttpClient();
    var response = httpClient.GetAsync(url).Result; // Блокирующий вызов!
    var data = response.Content.ReadAsStringAsync().Result; // Еще один блок!
    textBlock.Text = data;
}
Этот код выглядит простым, но вызывает ряд проблем:
1. Блокировка UI-потока — приложение перестаёт отвечать на действия пользователя.
2. Неэффективное использование ресурсов — поток простаивает в ожидании завершения операции ввода-вывода.
3. Снижение масштабируемости — в серверных приложениях каждый заблокированный поток означает меньшую пропускную способность.

На серверной стороне, например в ASP.NET, синхронные операции приводят к блокировке потоков из пула, что существенно ограничивает количество одновременно обрабатываемых запросов и может вызвать их отклонение при высокой нагрузке.

Async/ await как правильно ввести данные в async метод (консоль)
Привет , кто то может помочь ?) проблема в тому что у меня есть async метод который запускается...

Чем отличается объявление метода async void от async Task?
Обсуждение вынесено из темы https://www.cyberforum.ru/windows-forms/thread3200300.html Еще...

Асинхронный метод: async + await
Доброго времени суток! Есть достаточно простой метод: public UserControl1 ListToArray() ...

Socket.Send + Async & Await: свой асинхронный метод отправки данных
Привет. Хочу написать свой асинх. метод для отправки данных при помощи класса Socket и его метода...


Эволюция асинхронных моделей в C#



Язык C# прошел долгий путь в развитии подходов к асинхронному программированию:

1. Многопоточность с использованием Thread — первый подход, позволяющий выполнять параллельные операции. Однако ручное управление потоками требует глубокого понимания синхронизации и часто приводит к ошибкам.

C#
1
2
3
4
5
6
new Thread(() => {
    // Выполнение длительной операции
    var result = PerformLongOperation();
    // Обновление UI требует синхронизации
    Dispatcher.Invoke(() => DisplayResult(result));
}).Start();
2. Пул потоков (ThreadPool) — улучшение, позволяющее переиспользовать потоки вместо создания новых.

C#
1
2
3
4
5
ThreadPool.QueueUserWorkItem(state => {
    var result = PerformLongOperation();
    // Нужна синхронизация с UI
    Dispatcher.Invoke(() => DisplayResult(result));
});
3. Асинхронная модель программирования (APM) с методами Begin/End — первая формальная модель асинхронности в .NET.

C#
1
2
3
4
webClient.BeginDownloadString(new Uri(url), ar => {
    var result = webClient.EndDownloadString(ar);
    // Снова нужна синхронизация
}, null);
4. Модель на основе событий (EAP) — попытка упростить работу с асинхронными операциями.

C#
1
2
3
var webClient = new WebClient();
webClient.DownloadStringCompleted += (s, e) => DisplayResult(e.Result);
webClient.DownloadStringAsync(new Uri(url));
5. Модель на основе задач (Task Parallel Library) — значительный шаг вперёд, представленный в .NET Framework 4.0.

C#
1
2
3
4
5
Task.Run(() => PerformLongOperation())
    .ContinueWith(t => {
        // Всё ещё нужна синхронизация
        Dispatcher.Invoke(() => DisplayResult(t.Result));
    });
Все эти подходы решали проблему блокировки, но создавали другие проблемы — запутанный код, сложность обработки ошибок, громоздкие конструкции для последовательных асинхронных операций. Представьте себе цепочку из пяти последовательных асинхронных операций — код превращается в "пирамиду судьбы" из вложенных обратных вызовов.

Сравнение Thread, ThreadPool и Task API



Перед появлением async/await разработчики использовали разные подходы.

Thread предоставляет полный контроль, но требует больше ресурсов:
  • Каждый поток занимает ~1 МБ памяти для стека.
  • Создание и уничтожение потока — затратные операции.
  • Слишком много потоков приводит к частому переключению контекста.

ThreadPool решает проблему создания/уничтожения потоков:
  • Использует ограниченное число потоков (обычно по числу ядер процессора).
  • Эффективнее распределяет нагрузку.
  • Но не предоставляет простых механизмов для получения результатов и обработки ошибок.

Task API добавляет абстракцию над ThreadPool:
  • Представляет операцию, а не поток выполнения.
  • Позволяет компоновать и связывать асинхронные операции.
  • Предоставляет механизмы для обработки результатов и ошибок.
  • Но при этом код остается сложным для написания и понимания.

Каждый из этих подходов имеет свои преимущества и недостатки, но ни один не решал главную проблему — сложность написания асинхронного кода, сохраняя при этом простоту и читаемость синхронного кода. И вот тут появляется async/await — революционный подход, который полностью изменил разработку асинхронных приложений в C#.

Основы async/await



В 2012 году с выходом C# 5.0 произошла настоящая революция в мире асинхронного программирования — были представлены ключевые слова async и await. Эти инструменты серьезно изменили подход к написанию асинхронного кода, сделав его почти таким же читаемым и понятным, как синхронный код.

Ключевые слова и их функции



Для понимания async/await давайте рассмотрим простую задачу. Например, загрузим данные с веб-сервера:

C#
1
2
3
4
5
6
7
8
private async void Button_Click(object sender, RoutedEventArgs e)
{
    var httpClient = new HttpClient();
    var response = await httpClient.GetAsync(url);
    response.EnsureSuccessStatusCode();
    var data = await response.Content.ReadAsStringAsync();
    textBlock.Text = data;
}
Этот код выглядит почти как синхронный, но работает асинхронно. Что здесь происходит? Ключевое слово async маркирует метод как асинхронный, указывая компилятору, что внутри могут использоваться операторы await. Само по себе оно не делает метод асинхронным — это лишь указание компилятору о необходимости трансформирования этого метода. Ключевое слово await применяется к задаче (Task) и означает: "Подожди завершения этой задачи, не блокируя текущий поток, и верни результат после завершения". Когда выполнение кода достигает оператора await, метод приостанавливается и возвращает управление вызывающему коду. После завершения ожидаемой задачи выполнение метода продолжается с точки, следующей за await.

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

Как работает под капотом



Чтобы понять магию async/await, нужно заглянуть под капот. Когда вы используете эти ключевые слова, компилятор C# трансформирует ваш метод в конечный автомат (state machine). Рассмотрим пример:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Исходный код
async Task<int> CalculateAsync()
{
    await Task.Delay(1000);
    return 42;
}
 
// Концептуальное представление того, что генерирует компилятор
Task<int> CalculateAsync()
{
    var stateMachine = new CalculateAsyncStateMachine();
    stateMachine.builder = AsyncTaskMethodBuilder<int>.Create();
    stateMachine.state = -1; // Исходное состояние
    stateMachine.MoveNext(); // Запуск автомата
    return stateMachine.builder.Task; // Возвращаем задачу
}
 
struct CalculateAsyncStateMachine
{
    public AsyncTaskMethodBuilder<int> builder;
    public int state;
    
    public void MoveNext()
    {
        int result;
        try {
            if (state == -1) {
                // Начало метода
                var delayTask = Task.Delay(1000);
                // Настраиваем продолжение и выходим
                state = 0;
                builder.AwaitUnsafeOnCompleted(ref delayTask, ref this);
                return;
            }
            else if (state == 0) {
                // Продолжение после Task.Delay
                result = 42;
                state = -2; // Финальное состояние
            }
        }
        catch (Exception ex) {
            state = -2;
            builder.SetException(ex);
            return;
        }
        
        if (state == -2) {
            builder.SetResult(result); // Устанавливаем результат
        }
    }
}
Эта трансформация выполняет несколько важных функций:
  • Разбивает асинхронный метод на кусочки (по каждому await).
  • Сохраняет все локальные переменные между вызовами.
  • Отслеживает на каком этапе остановилось выполнение.
  • Настраивает продолжения для каждой асинхронной операции.
  • Обрабатывает исключения, сохраняя их контекст.

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

Жизненный цикл асинхронного метода



Жизненный цикл асинхронного метода можно представить в виде диаграммы состояний:
1. Состояние "Запущен" — метод начинает выполнение.
2. Состояние "Приостановлен" — достигнут оператор await, ожидается завершение задачи.
3. Состояние "Продолжение" — асинхронная операция завершена, выполнение метода продолжается.
4. Состояние "Завершен" — метод полностью выполнен, результат доступен.

При каждом await метод переходит из состояния "Запущен" или "Продолжение" в состояние "Приостановлен", а затем снова в "Продолжение". Если в методе несколько операторов await, этот цикл повторяется несколько раз.

Различия между Task, Task<T> и ValueTask<T>



Асинхронные методы обычно возвращают один из трех типов:

Task — представляет асинхронную операцию, которая не возвращает значение. Используется, когда метод ничего не возвращает (в синхронном мире аналогичен void).

C#
1
2
3
4
5
async Task UpdateDatabaseAsync()
{
    await dbConnection.OpenAsync();
    await dbCommand.ExecuteNonQueryAsync();
}
Task<T> — представляет асинхронную операцию, возвращающую значение типа T.

C#
1
2
3
4
5
6
async Task<int> CountUsersAsync()
{
    await dbConnection.OpenAsync();
    var result = await dbCommand.ExecuteScalarAsync();
    return Convert.ToInt32(result);
}
ValueTask<T> — появился в C# 7.0 как альтернатива Task<T> для снижения накладных расходов на выделение памяти. Используется, когда асинхронная операция часто завершается синхронно или результат можно кэшировать.

C#
1
2
3
4
5
6
7
8
9
10
11
async ValueTask<User> GetUserAsync(int id)
{
    // Проверяем кэш
    if (_cache.TryGetValue(id, out var user))
        return user; // Синхронное возвращение без создания Task
    
    // Если не в кэше, делаем асинхронную операцию
    user = await _dataAccess.LoadUserAsync(id);
    _cache[id] = user;
    return user;
}
ValueTask<T> особенно полезен в высоконагруженных системах, где снижение аллокаций памяти критически важно. Следует отметить, что в отличие от Task, ValueTask нельзя многократно ожидать.

Статистический анализ производительности



Исследования показывают существенную разницу в производительности при использовании асинхронного подхода. Согласно данным Microsoft, использование async/await в веб-приложениях ASP.NET может увеличить количество одновременно обрабатываемых запросов на несколько порядков при тех же аппаратных ресурсах. Например, на сервере с 4 ядрами синхронное веб-приложение может обрабатывать около 1000-2000 одновременных запросов, в то время как асинхронное справится с десятками тысяч запросов. Это происходит потому, что потоки не блокируются в ожидании завершения операций ввода-вывода, а используются эффективно для обработки других запросов.

В клиентских приложениях async/await не повышает производительность операций, но делает UI отзывчивым даже при выполнении длительных задач. Пользователи могут продолжать взаимодействие с приложением, пока асинхронные операции выполняются в фоновом режиме.

Синтаксический сахар и реальные трансформации компилятора



Когда мы говорим об async/await как о "синтаксическом сахаре", это не совсем точно отражает его суть. На самом деле трансформация, которую выполняет компилятор, гораздо сложнее и глубже. Рассмотрим, как компилятор преобразует простой асинхронный метод:

C#
1
2
3
4
5
6
async Task<string> GetDataAsync(string url)
{
    var responseTask = await httpClient.GetAsync(url);
    var data = await responseTask.Content.ReadAsStringAsync();
    return data;
}
Если упростить, компилятор примерно преобразует его в:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Task<string> GetDataAsync(string url)
{
    var awaiter = httpClient.GetAsync(url).GetAwaiter();
    
    if (!awaiter.IsCompleted)
    {
        var tcs = new TaskCompletionSource<string>();
        awaiter.OnCompleted(() => {
            try {
                var responseTask = awaiter.GetResult();
                var contentAwaiter = responseTask.Content.ReadAsStringAsync().GetAwaiter();
                
                contentAwaiter.OnCompleted(() => {
                    try {
                        var data = contentAwaiter.GetResult();
                        tcs.SetResult(data);
                    }
                    catch (Exception ex) {
                        tcs.SetException(ex);
                    }
                });
            }
            catch (Exception ex) {
                tcs.SetException(ex);
            }
        });
        return tcs.Task;
    }
    
    // Синхронный путь выполнения
    var response = awaiter.GetResult();
    return response.Content.ReadAsStringAsync();
}
Это сильно упрощенная версия, но она демонстрирует интересный факт – компилятор создаёт оптимизированные пути для случаев, когда задача завершается синхронно (без реального ожидания). Это позволяет избежать лишних операций, если результат уже доступен. Важным аспектом этой трансформации является то, что компилятор создаёт конечный автомат, сохраняя состояние локальных переменных между вызовами. Это значительно сложнее, чем простое добавление коллбэков.

Интерфейс IAwaiter и его роль



Механизм await опирается на паттерн ожидания, определяемый интерфейсом:

C#
1
2
3
4
5
6
public interface IAwaiter : INotifyCompletion
{
    bool IsCompleted { get; }
    void OnCompleted(Action continuation);
    TResult GetResult();
}
Любой тип может стать "ожидаемым" (awaitable), если имеет метод GetAwaiter(), возвращающий объект с:
- Свойством IsCompleted,
- Методом OnCompleted(),
- Методом GetResult().

Это значит, что вы можете создавать собственные ожидаемые типы и использовать их с оператором await:

C#
1
2
3
4
5
6
7
8
9
10
11
public class CustomTask
{
    public CustomAwaiter GetAwaiter() => new CustomAwaiter();
}
 
public class CustomAwaiter : INotifyCompletion
{
    public bool IsCompleted => false;
    public void OnCompleted(Action continuation) => Task.Run(continuation);
    public void GetResult() { }
}

Повторное использование асинхронных задач и кеширование результатов



Часто мы сталкиваемся с ситуацией, когда нужно многократно использовать результат одной и той же асинхронной операции. Вот несколько подходов к решению этой задачи:

Кеширование задач



Вместо повторного запуска одинаковых операций можно кешировать задачи:

C#
1
2
3
4
5
6
7
8
9
10
private Task<ApiData> _dataTask;
 
public Task<ApiData> GetApiDataAsync()
{
    if (_dataTask == null)
    {
        _dataTask = FetchApiDataAsync();
    }
    return _dataTask;
}
Этот подход работает для статических данных, но у него есть недостатки:
  • Нет механизма обновления устаревших данных.
  • Кешируется не только результат, но и задача целиком.
  • Не учитывается состояние задачи (завершена с ошибкой).

Кеширование с проверкой состояния



Улучшенный вариант с проверкой состояния задачи:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Task<ApiData> _dataTask;
private readonly object _lockObj = new object();
 
public Task<ApiData> GetApiDataAsync()
{
    lock (_lockObj)
    {
        if (_dataTask == null || _dataTask.IsFaulted || _dataTask.IsCanceled)
        {
            _dataTask = FetchApiDataAsync();
        }
    }
    return _dataTask;
}

Кеширование с временем жизни



Для данных, которые устаревают со временем:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private Task<ApiData> _dataTask;
private DateTime _lastFetchTime;
private readonly TimeSpan _cacheLifetime = TimeSpan.FromMinutes(10);
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);
 
public async Task<ApiData> GetApiDataAsync()
{
    await _semaphore.WaitAsync();
    try
    {
        if (_dataTask == null || 
            _dataTask.IsFaulted || 
            _dataTask.IsCanceled || 
            DateTime.Now - _lastFetchTime > _cacheLifetime)
        {
            _dataTask = FetchApiDataAsync();
            _lastFetchTime = DateTime.Now;
        }
    }
    finally
    {
        _semaphore.Release();
    }
    
    return await _dataTask;
}

Lazy<Task<T>>



Элегантный способ с использованием Lazy<T>:

C#
1
2
3
private Lazy<Task<ApiData>> _lazyTask = new Lazy<Task<ApiData>>(() => FetchApiDataAsync());
 
public Task<ApiData> GetApiDataAsync() => _lazyTask.Value;
Для обновляемых данных можно комбинировать с проверкой времени жизни:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private Lazy<Task<ApiData>> _lazyTask;
private DateTime _initTime;
private readonly TimeSpan _cacheLifetime = TimeSpan.FromMinutes(10);
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);
 
public async Task<ApiData> GetApiDataAsync()
{
    await _semaphore.WaitAsync();
    try
    {
        if (_lazyTask == null || DateTime.Now - _initTime > _cacheLifetime)
        {
            _lazyTask = new Lazy<Task<ApiData>>(() => FetchApiDataAsync());
            _initTime = DateTime.Now;
        }
    }
    finally
    {
        _semaphore.Release();
    }
    
    return await _lazyTask.Value;
}

MemoryCache для более сложных случаев



Для более сложных сценариев кеширования можно использовать MemoryCache:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
private readonly string _cacheKey = "ApiDataKey";
 
public async Task<ApiData> GetApiDataAsync()
{
    if (!_cache.TryGetValue(_cacheKey, out Task<ApiData> task))
    {
        task = FetchApiDataAsync();
        var options = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(10))
            .SetSlidingExpiration(TimeSpan.FromMinutes(2));
            
        _cache.Set(_cacheKey, task, options);
    }
    
    return await task;
}

Внутренние оптимизации для асинхронных методов



Современные версии компилятора C# реализуют ряд оптимизаций для асинхронных методов:

1. Избегание аллокаций



Компилятор старается минимизировать количество выделений памяти в куче:

C#
1
2
3
4
5
// До оптимизации - каждый вызов создавал новый объект Task
public Task<int> GetValueAsync() => Task.FromResult(42);
 
// После оптимизации - используется пул предварительно созданных задач
public Task<int> GetValueAsync() => Task.FromResult(42); // Под капотом используется Task<int>.s_cachedTasks

2. Пулинг объектов автоматов состояний



Современные версии компилятора могут переиспользовать структуры state machine вместо создания новых:

C#
1
2
3
4
5
6
7
8
9
10
11
12
// Упрощенная концепция
struct MoveNextRunner
{
    public IAsyncStateMachine StateMachine;
    
    public void Run()
    {
        StateMachine.MoveNext();
    }
}
 
// После выполнения метода, структура может вернуться в пул

3. Предотвращение лишних продолжений



Компилятор оптимизирует случаи, когда задача уже завершена:

C#
1
2
3
4
5
6
7
8
9
10
11
var task = SomeAsyncMethod();
if (task.IsCompleted) 
{
    // Используем результат напрямую без настройки продолжения
    UseResult(task.Result);
}
else 
{
    // Настраиваем продолжение только если нужно ждать
    task.ContinueWith(t => UseResult(t.Result));
}

4. Специализированные типы для конечных автоматов



C# 7.0 и новее могут использовать структуры вместо классов для реализации конечных автоматов, что уменьшает нагрузку на сборку мусора.

C#
1
2
3
4
5
6
// Примерно так выглядит обработка async метода в сгенерированном коде
[CompilerGenerated]
private struct <MethodName>d__1 : IAsyncStateMachine
{
    // Реализация автомата
}
Эти внутренние оптимизации позволяют использовать async/await с минимальными накладными расходами во многих сценариях, что часто делает асинхронный код почти таким же эффективным, как и ручное построение цепочек продолжений, но при этом значительно более читаемым и простым в сопровождении.

Практическое применение



В этом разделе мы рассмотрим наиболее распространённые сценарии использования async/await в реальных проектах.

Типичные сценарии использования



Асинхронность особенно полезна в следующих ситуациях:
1. Сетевые запросы — загрузка данных с серверов, API-вызовы, передача файлов.
2. Дисковые операции — чтение и запись файлов, работа с большими объёмами данных.
3. Работа с базами данных — запросы, транзакции.
4. Длительные вычисления — с правильной реализацией.
5. Ожидание внешних процессов — результаты работы других приложений.

При этом не рекомендуется использовать async/await для:
  • Простых и быстрых операций (overhead может превысить выгоду).
  • Передачи управления между потоками (для этого есть специальные механизмы).

Примеры кода с HTTP-запросами и работой с файлами



HTTP-запросы



Одна из самых распространённых задач — выполнение HTTP-запросов. Вот практический пример получения и обработки данных с веб-сервиса:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public async Task<UserProfile> GetUserProfileAsync(int userId)
{
    try
    {
        // Создаём HttpClient (в реальном проекте лучше использовать IHttpClientFactory)
        using var client = new HttpClient();
        
        // Устанавливаем базовый адрес и заголовки
        client.BaseAddress = new Uri("https://api.example.com/");
        client.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
        
        // Выполняем запрос
        var response = await client.GetAsync($"users/{userId}");
        
        // Проверяем статус
        response.EnsureSuccessStatusCode();
        
        // Читаем и десериализуем ответ
        var json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<UserProfile>(json);
    }
    catch (HttpRequestException ex)
    {
        // Обрабатываем сетевые ошибки
        throw new ServiceException($"Ошибка при запросе профиля пользователя: {ex.Message}", ex);
    }
    catch (JsonException ex)
    {
        // Обрабатываем ошибки парсинга JSON
        throw new FormatException($"Недопустимый формат данных профиля: {ex.Message}", ex);
    }
}
Для упрощения запросов можно создать обёрточный класс:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ApiClient
{
    private readonly HttpClient _client;
    
    public ApiClient(string baseUrl, string apiKey)
    {
        _client = new HttpClient();
        _client.BaseAddress = new Uri(baseUrl);
        _client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
        _client.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
    }
    
    public async Task<T> GetAsync<T>(string endpoint)
    {
        var response = await _client.GetAsync(endpoint);
        response.EnsureSuccessStatusCode();
        var json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<T>(json);
    }
    
    public async Task<T> PostAsync<T>(string endpoint, object data)
    {
        var content = JsonSerializer.Serialize(data);
        var stringContent = new StringContent(content, Encoding.UTF8, "application/json");
        
        var response = await _client.PostAsync(endpoint, stringContent);
        response.EnsureSuccessStatusCode();
        
        var json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<T>(json);
    }
}
 
// Использование
var apiClient = new ApiClient("https://api.example.com/", "your-api-key");
var user = await apiClient.GetAsync<UserProfile>("users/42");

Работа с файлами



Файловые операции могут блокировать поток, поэтому они идеальные кандидаты для асинхронной обработки:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public async Task<string> ReadLogFileAsync(string path)
{
    // Асинхронное чтение файла
    using var stream = new FileStream(
        path, 
        FileMode.Open, 
        FileAccess.Read, 
        FileShare.Read, 
        bufferSize: 4096, 
        useAsync: true); // Важный флаг для асинхронной работы
    
    using var reader = new StreamReader(stream);
    return await reader.ReadToEndAsync();
}
 
public async Task SaveDocumentAsync(string path, string content)
{
    // Создаём директории, если их нет
    var directory = Path.GetDirectoryName(path);
    if (!Directory.Exists(directory))
        Directory.CreateDirectory(directory);
    
    // Асинхронная запись в файл
    using var stream = new FileStream(
        path, 
        FileMode.Create, 
        FileAccess.Write, 
        FileShare.None, 
        bufferSize: 4096, 
        useAsync: true);
    
    using var writer = new StreamWriter(stream);
    await writer.WriteAsync(content);
}
Обратите внимание на параметр useAsync: true. Это необходимо для настоящего асинхронного ввода-вывода. Без него файловые операции будут выполняться в фоновом потоке, но всё равно блокировать его.

Асинхронная работа с базами данных и Entity Framework



Современные ORM, включая Entity Framework Core, поддерживают асинхронные операции для всех видов взаимодействия с базой данных:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class CustomerRepository
{
    private readonly AppDbContext _context;
    
    public CustomerRepository(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<Customer> GetByIdAsync(int id)
    {
        return await _context.Customers
            .Include(c => c.Orders)
            .FirstOrDefaultAsync(c => c.Id == id);
    }
    
    public async Task<List<Customer>> GetAllActiveAsync()
    {
        return await _context.Customers
            .Where(c => c.IsActive)
            .ToListAsync();
    }
    
    public async Task<int> AddAsync(Customer customer)
    {
        _context.Customers.Add(customer);
        await _context.SaveChangesAsync();
        return customer.Id;
    }
    
    public async Task UpdateAsync(Customer customer)
    {
        _context.Entry(customer).State = EntityState.Modified;
        await _context.SaveChangesAsync();
    }
}
В веб-приложении это может использоваться так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
    private readonly CustomerRepository _repository;
    
    public CustomersController(CustomerRepository repository)
    {
        _repository = repository;
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<Customer>> Get(int id)
    {
        var customer = await _repository.GetByIdAsync(id);
        if (customer == null)
            return NotFound();
        
        return customer;
    }
    
    [HttpPost]
    public async Task<ActionResult<Customer>> Create(Customer customer)
    {
        await _repository.AddAsync(customer);
        
        return CreatedAtAction(
            nameof(Get), 
            new { id = customer.Id }, 
            customer);
    }
}

Реализация паттерна Producer-Consumer с использованием async/await



Паттерн Producer-Consumer (Производитель-Потребитель) — это классический подход к организации параллельной обработки, когда один компонент создаёт данные, а другой их обрабатывает. С помощью async/await можно элегантно реализовать этот паттерн:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class AsyncProducerConsumer<T>
{
    private readonly Channel<T> _channel;
    private readonly int _maxItems;
    
    public AsyncProducerConsumer(int capacity = 100)
    {
        _maxItems = capacity;
        _channel = Channel
            .CreateBounded<T>(new BoundedChannelOptions(capacity) 
            {
                FullMode = BoundedChannelFullMode.Wait
            });
    }
    
    public async Task ProduceAsync(T item, CancellationToken cancellationToken = default)
    {
        await _channel.Writer.WriteAsync(item, cancellationToken);
    }
    
    public async Task ProduceRangeAsync(IEnumerable<T> items, 
                                     CancellationToken cancellationToken = default)
    {
        foreach (var item in items)
        {
            await _channel.Writer.WriteAsync(item, cancellationToken);
        }
    }
    
    public async Task CompleteProducerAsync()
    {
        _channel.Writer.Complete();
    }
    
    public async Task ConsumeAsync(Func<T, Task> processor, 
                               CancellationToken cancellationToken = default)
    {
        await foreach (var item in _channel.Reader.ReadAllAsync(cancellationToken))
        {
            await processor(item);
        }
    }
    
    public async Task ConsumeParallelAsync(Func<T, Task> processor, 
                                       int maxConcurrency = 4,
                                       CancellationToken cancellationToken = default)
    {
        var tasks = new List<Task>();
        for (int i = 0; i < maxConcurrency; i++)
        {
            tasks.Add(Task.Run(async () =>
            {
                await foreach (var item in _channel.Reader.ReadAllAsync(cancellationToken))
                {
                    await processor(item);
                }
            }));
        }
        
        await Task.WhenAll(tasks);
    }
}
Использование этого класса выглядит так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var pipeline = new AsyncProducerConsumer<string>(capacity: 1000);
 
// Запускаем потребителей
var consumer = pipeline.ConsumeAsync(async item => 
{
    await ProcessItemAsync(item);
});
 
// Добавляем элементы асинхронно
await pipeline.ProduceAsync("Item 1");
await pipeline.ProduceAsync("Item 2");
 
await pipeline.CompleteProducerAsync(); // Сигнализируем о завершении
await consumer; // Ожидаем завершения обработки всех данных
Для параллельной обработки:

C#
1
2
3
4
5
// Запускаем потребителей с параллельной обработкой
var consumer = pipeline.ConsumeParallelAsync(
    async item => await ProcessItemAsync(item),
    maxConcurrency: Environment.ProcessorCount
);

Реализация cancellation token для управления асинхронными операциями



Механизм отмены — важная часть надежного асинхронного кода. В .NET для этого служат токены отмены (CancellationToken), которые позволяют корректно прервать длительные операции по запросу. Базовое использование выглядит так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public async Task<string> DownloadDataWithTimeoutAsync(string url, TimeSpan timeout)
{
    // Создаем источник токенов отмены
    using var cts = new CancellationTokenSource(timeout);
    
    try
    {
        using var client = new HttpClient();
        // Передаем токен в асинхронный метод
        return await client.GetStringAsync(url, cts.Token);
    }
    catch (OperationCanceledException)
    {
        throw new TimeoutException($"Не удалось загрузить данные с {url} за {timeout.TotalSeconds} секунд");
    }
}
Для более сложных сценариев можно комбинировать несколько токенов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public async Task ProcessJobsAsync(
    IEnumerable<Job> jobs, 
    CancellationToken externalToken, 
    TimeSpan timeout)
{
    // Создаем источник с таймаутом
    using var timeoutCts = new CancellationTokenSource(timeout);
    
    // Объединяем два источника: внешний и таймаут
    using var linkedCts = CancellationTokenSource
        .CreateLinkedTokenSource(externalToken, timeoutCts.Token);
    
    var token = linkedCts.Token;
    
    foreach (var job in jobs)
    {
        // Проверяем, не запрошена ли отмена
        token.ThrowIfCancellationRequested();
        
        // Выполняем задачу и передаем объединенный токен
        await ProcessJobAsync(job, token);
    }
}
Для поддержки отмены в собственном коде нужно следовать нескольким правилам:
1. Принимать CancellationToken как параметр (обычно последний).
2. Регулярно проверять token.IsCancellationRequested или вызывать token.ThrowIfCancellationRequested().
3. Корректно обрабатывать исключение OperationCanceledException.
4. Передавать токен во вложенные асинхронные вызовы.

Вот пример метода с поддержкой отмены:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public async Task<List<SearchResult>> SearchFilesAsync(
    string rootPath, 
    string searchPattern, 
    CancellationToken cancellationToken = default)
{
    var results = new List<SearchResult>();
    
    // Проверка на отмену перед началом
    cancellationToken.ThrowIfCancellationRequested();
    
    try
    {
        var files = Directory.GetFiles(rootPath, searchPattern, SearchOption.AllDirectories);
        
        foreach (var file in files)
        {
            // Проверяем отмену в цикле
            cancellationToken.ThrowIfCancellationRequested();
            
            var content = await File.ReadAllTextAsync(file, cancellationToken);
            
            if (content.Contains(searchPattern))
            {
                results.Add(new SearchResult { FilePath = file });
            }
        }
        
        return results;
    }
    catch (OperationCanceledException)
    {
        // Пробрасываем исключение отмены дальше
        throw;
    }
    catch (Exception ex)
    {
        throw new SearchException($"Ошибка при поиске файлов: {ex.Message}", ex);
    }
}

Паттерн асинхронной инициализации для классов и сервисов



Иногда классам требуется асинхронная инициализация перед использованием. Вот несколько подходов к этой задаче:

1. Фабричный метод



C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class DatabaseService
{
    private DatabaseConnection _connection;
    
    // Приватный конструктор предотвращает прямое создание
    private DatabaseService() { }
    
    // Фабричный метод для асинхронного создания
    public static async Task<DatabaseService> CreateAsync(string connectionString)
    {
        var service = new DatabaseService();
        await service.InitializeAsync(connectionString);
        return service;
    }
    
    private async Task InitializeAsync(string connectionString)
    {
        _connection = new DatabaseConnection(connectionString);
        await _connection.OpenAsync();
    }
    
    public async Task<Data> GetDataAsync(int id)
    {
        // Используем инициализированное соединение
        return await _connection.QueryAsync(id);
    }
}
 
// Использование:
var dbService = await DatabaseService.CreateAsync("connection-string");
var data = await dbService.GetDataAsync(42);

2. Ленивая инициализация



C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class AnalyticsClient
{
    private readonly string _apiKey;
    private readonly SemaphoreSlim _initLock = new SemaphoreSlim(1, 1);
    private ApiConnection _connection;
    
    public AnalyticsClient(string apiKey)
    {
        _apiKey = apiKey;
    }
    
    private async Task EnsureInitializedAsync()
    {
        if (_connection != null)
            return;
            
        await _initLock.WaitAsync();
        try
        {
            if (_connection != null)
                return;
                
            _connection = new ApiConnection(_apiKey);
            await _connection.AuthenticateAsync();
        }
        finally
        {
            _initLock.Release();
        }
    }
    
    public async Task<AnalyticsData> GetMetricsAsync(DateTime startDate, DateTime endDate)
    {
        await EnsureInitializedAsync();
        return await _connection.QueryMetricsAsync(startDate, endDate);
    }
}

3. Асинхронный конструктор с помощью IAsyncLifetime (ASP.NET Core)



C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class EmailService : IAsyncDisposable, IAsyncLifetime
{
    private readonly SmtpClient _client;
    private readonly string _username;
    private readonly string _password;
    
    public EmailService(string host, int port, string username, string password)
    {
        _client = new SmtpClient(host, port);
        _username = username;
        _password = password;
    }
    
    // Реализация IAsyncLifetime
    public async Task InitializeAsync()
    {
        await _client.ConnectAsync(_client.Host, _client.Port);
        await _client.AuthenticateAsync(_username, _password);
    }
    
    public async Task SendEmailAsync(string to, string subject, string body)
    {
        var message = new MimeMessage();
        message.To.Add(new MailboxAddress("", to));
        message.Subject = subject;
        message.Body = new TextPart("plain") { Text = body };
        
        await _client.SendAsync(message);
    }
    
    // Реализация IAsyncDisposable
    public async ValueTask DisposeAsync()
    {
        await _client.DisconnectAsync(true);
        _client.Dispose();
    }
    
    // Второй метод IAsyncLifetime
    public Task DisposeAsync() => Task.CompletedTask;
}
 
// В ASP.NET Core DI:
services.AddSingleton<EmailService>(sp => 
    new EmailService("smtp.example.com", 587, "user", "pass"));

Реализация таймаутов в асинхронных операциях без блокировки потоков



Таймауты — важнейший механизм для обеспечения надежности распределенных систем. С помощью async/await можно реализовать таймауты без блокировки потоков.

Базовый подход с Task.WhenAny



C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public async Task<T> WithTimeout<T>(Task<T> task, TimeSpan timeout)
{
    var delayTask = Task.Delay(timeout);
    
    var completedTask = await Task.WhenAny(task, delayTask);
    
    if (completedTask == delayTask)
        throw new TimeoutException($"Операция превысила отведенное время {timeout}");
    
    return await task; // Ждем таск еще раз для пробрасывания исключений
}
 
// Использование:
try
{
    var result = await WithTimeout(
        LongRunningOperationAsync(),
        TimeSpan.FromSeconds(5)
    );
}
catch (TimeoutException ex)
{
    // Обработка истечения времени ожидания
}

Подход с CancellationToken



C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public async Task<T> WithCancellableTimeout<T>(
    Func<CancellationToken, Task<T>> taskFactory, 
    TimeSpan timeout)
{
    using var cts = new CancellationTokenSource(timeout);
    try
    {
        return await taskFactory(cts.Token);
    }
    catch (OperationCanceledException) when (cts.IsCancellationRequested)
    {
        throw new TimeoutException($"Операция превысила отведенное время {timeout}");
    }
}
 
// Использование:
var result = await WithCancellableTimeout(
    async token => await _repository.GetDataAsync(id, token),
    TimeSpan.FromSeconds(5)
);

Расширенный подход с опциональной отменой



C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static class TaskExtensions
{
    public static async Task<T> TimeoutAfter<T>(
        this Task<T> task, 
        TimeSpan timeout,
        bool cancelOnTimeout = false)
    {
        using var cts = cancelOnTimeout ? new CancellationTokenSource() : null;
        
        var delayTask = Task.Delay(timeout, cts?.Token ?? CancellationToken.None);
        var resultTask = await Task.WhenAny(task, delayTask);
        
        if (resultTask == delayTask)
        {
            // Если нужно, отменяем исходную задачу
            if (cancelOnTimeout)
                cts.Cancel();
                
            throw new TimeoutException($"Операция превысила отведенное время {timeout}");
        }
        
        // Отменяем задачу задержки, если она еще не завершена
        if (cancelOnTimeout && !delayTask.IsCompleted)
            cts.Cancel();
            
        return await task;
    }
}
 
// Использование:
var result = await _dataService
    .GetLargeFileAsync()
    .TimeoutAfter(TimeSpan.FromMinutes(2), cancelOnTimeout: true);

Асинхронная обработка коллекций с ограничением параллелизма



При асинхронной обработке коллекций данных часто необходимо ограничить степень параллелизма, чтобы избежать перегрузки системы.

Последовательная обработка



C#
1
2
3
4
5
6
7
8
9
public async Task ProcessItemsSequentiallyAsync<T>(
    IEnumerable<T> items, 
    Func<T, Task> processAsync)
{
    foreach (var item in items)
    {
        await processAsync(item);
    }
}

Параллельная обработка с ограничением



C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public async Task ProcessWithLimitedParallelismAsync<T>(
    IEnumerable<T> items, 
    Func<T, Task> processAsync, 
    int maxParallelism)
{
    using var semaphore = new SemaphoreSlim(maxParallelism);
    var tasks = new List<Task>();
    
    foreach (var item in items)
    {
        await semaphore.WaitAsync();
        
        tasks.Add(Task.Run(async () => 
        {
            try
            {
                await processAsync(item);
            }
            finally
            {
                semaphore.Release();
            }
        }));
    }
    
    await Task.WhenAll(tasks);
}

Расширенный вариант с управлением ошибками



C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public async Task<ProcessingResult<T>> ProcessBatchWithControlAsync<T>(
    IEnumerable<T> items,
    Func<T, Task> processAsync,
    int maxParallelism,
    CancellationToken cancellationToken = default)
{
    var result = new ProcessingResult<T>();
    using var semaphore = new SemaphoreSlim(maxParallelism);
    var tasks = new List<Task>();
 
    foreach (var item in items)
    {
        cancellationToken.ThrowIfCancellationRequested();
        await semaphore.WaitAsync(cancellationToken);
        
        tasks.Add(Task.Run(async () => 
        {
            try
            {
                await processAsync(item);
                lock (result)
                    result.Successful.Add(item);
            }
            catch (Exception ex)
            {
                lock (result)
                    result.Failed[item] = ex;
            }
            finally
            {
                semaphore.Release();
            }
        }, cancellationToken));
    }
    
    await Task.WhenAll(tasks);
    return result;
}
 
public class ProcessingResult<T>
{
    public List<T> Successful { get; } = new List<T>();
    public Dictionary<T, Exception> Failed { get; } = new Dictionary<T, Exception>();
    
    public bool HasErrors => Failed.Count > 0;
}

Еще более продвинутый вариант с партиями и отчетом о прогрессе



C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public async Task ProcessInBatchesAsync<T>(
    IEnumerable<T> items,
    Func<T, Task> processAsync,
    int batchSize = 100,
    int maxParallelism = 10,
    IProgress<ProcessingProgress> progress = null,
    CancellationToken cancellationToken = default)
{
    var batches = items.ChunkBy(batchSize);
    var totalItems = items.Count();
    var processedCount = 0;
    
    foreach (var batch in batches)
    {
        var batchTasks = new List<Task>();
        using var semaphore = new SemaphoreSlim(maxParallelism);
        
        foreach (var item in batch)
        {
            await semaphore.WaitAsync(cancellationToken);
            
            batchTasks.Add(Task.Run(async () => 
            {
                try
                {
                    await processAsync(item);
                    
                    var currentProcessed = Interlocked.Increment(ref processedCount);
                    progress?.Report(new ProcessingProgress(
                        currentProcessed, totalItems));
                }
                finally
                {
                    semaphore.Release();
                }
            }, cancellationToken));
        }
        
        await Task.WhenAll(batchTasks);
    }
}
 
public class ProcessingProgress
{
    public ProcessingProgress(int processed, int total)
    {
        ProcessedItems = processed;
        TotalItems = total;
        PercentComplete = (double)processed / total * 100;
    }
    
    public int ProcessedItems { get; }
    public int TotalItems { get; }
    public double PercentComplete { get; }
}

Типичные ошибки и их решения



Несмотря на все преимущества async/await, эта парадигма программирования вводит собственный набор подводных камней и типичных ошибок. Даже опытные разработчики иногда попадают в эти ловушки. Рассмотрим наиболее распространённые проблемы и способы их решения.

Deadlocks и как их избежать



Deadlock (взаимная блокировка) — одна из самых коварных проблем асинхронного программирования. Классический сценарий возникает при смешивании асинхронного и синхронного кода:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DeadlockDemo
{
    // Асинхронный метод
    public async Task<int> GetValueAsync()
    {
        await Task.Delay(1000); // Здесь происходит возврат в контекст синхронизации
        return 42;
    }
    
    // Синхронный метод, вызывающий асинхронный
    public int GetValueSync()
    {
        // Здесь блокируется поток, ожидая завершения задачи
        return GetValueAsync().Result;
    }
}
Что происходит при вызове GetValueSync в UI-приложении:
1. Метод вызывается в UI-потоке и блокирует его с помощью .Result.
2. GetValueAsync запускается и доходит до оператора await.
3. После выполнения Task.Delay, метод пытается продолжить выполнение в UI-потоке.
4. UI-поток уже заблокирован ожиданием результата — deadlock!

Решения этой проблемы:

1. Асинхронность сверху донизу: Никогда не блокируйтесь на асинхронных методах. Всегда используйте await вместо .Result, .Wait() или .GetAwaiter().GetResult().

C#
1
2
3
4
5
// Правильно — весь стек вызовов асинхронный
public async Task<int> GetValueAsync()
{
    return await OtherAsyncMethod();
}
2. ConfigureAwait(false): Если блокировки нельзя избежать, отключите захват контекста синхронизации.

C#
1
2
3
4
5
6
7
8
9
10
11
public async Task<int> GetValueAsync()
{
    await Task.Delay(1000).ConfigureAwait(false);
    return 42;
}
 
// Теперь можно безопасно блокироваться (хотя этого лучше избегать)
public int GetValueSync()
{
    return GetValueAsync().Result;
}

ConfigureAwait и контекст синхронизации



Метод .ConfigureAwait(bool) часто вызывает путаницу. Его ключевой параметр continueOnCapturedContext определяет, будет ли продолжение выполняться в исходном контексте синхронизации:

C#
1
2
3
4
5
// Продолжение будет выполнено в оригинальном контексте (UI-поток, контекст ASP.NET и т.д.)
await SomeAsyncMethod();
 
// Продолжение будет выполнено в любом свободном потоке из пула потоков
await SomeAsyncMethod().ConfigureAwait(false);
Важно понимать, когда применять ConfigureAwait(false):

1. Используйте в библиотечном коде: В библиотеках всегда предпочтительнее .ConfigureAwait(false), чтобы не навязывать потребителям ваш контекст.

C#
1
2
3
4
5
6
7
// В библиотечном коде
public async Task<string> ParseFileAsync(string path)
{
    using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
    using var reader = new StreamReader(stream);
    return await reader.ReadToEndAsync().ConfigureAwait(false);
}
2. Избегайте в UI-коде: В обработчиках UI-событий обычно нужен оригинальный контекст для безопасного обновления интерфейса.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// В WPF/Windows Forms/UWP
private async void Button_Click(object sender, EventArgs e)
{
    button.IsEnabled = false;
    try
    {
        var result = await dataService.GetDataAsync(); // Здесь не используем ConfigureAwait(false)
        resultTextBlock.Text = result; // Нужен UI-поток
    }
    finally
    {
        button.IsEnabled = true;
    }
}
3. В ASP.NET Core можно не использовать: В ASP.NET Core нет контекста синхронизации по умолчанию, поэтому ConfigureAwait(false) не дает никакого выигрыша в производительности.

Помните: ConfigureAwait(false) влияет только на ближайший оператор await. Последующие операторы await снова захватят текущий контекст, если не указано иное.

C#
1
2
3
4
5
6
async Task ExampleAsync()
{
    await Task.Delay(1000).ConfigureAwait(false); // Отключаем контекст
    await Task.Delay(1000); // Снова захватываем контекст!
    await Task.Delay(1000).ConfigureAwait(false); // Опять отключаем
}

Проблема "асинхронного void" и пути её решения



Методы с сигнатурой async void создают особые проблемы:
1. Исключения в таких методах невозможно перехватить обычным способом.
2. Их нельзя ожидать с помощью await.
3. Они не могут быть проверены на успешное завершение.

Рассмотрим пример:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Плохой код
async void FireAndForgetMethod()
{
    await Task.Delay(1000);
    throw new Exception("Это исключение крашнет приложение!");
}
 
public void DoSomething()
{
    try
    {
        FireAndForgetMethod(); // Вызываем и... забываем
        // Исключение не будет поймано здесь!
    }
    catch (Exception ex)
    {
        // Этот блок не поймает исключения из FireAndForgetMethod
        Console.WriteLine(ex.Message);
    }
}
Решения:

1. Возвращайте Task вместо void:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Хороший код
async Task FireAndForgetMethodAsync()
{
    await Task.Delay(1000);
    throw new Exception("Это исключение будет поймано!");
}
 
public async Task DoSomethingAsync()
{
    try
    {
        await FireAndForgetMethodAsync(); // Исключение будет поймано
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}
2. Если async void неизбежен (например, обработчики событий), используйте обработку исключений:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
async void Button_Click(object sender, EventArgs e)
{
    try
    {
        await RiskyOperationAsync();
    }
    catch (Exception ex)
    {
        // Логирование или обработка исключения
        Logger.LogError(ex);
        MessageBox.Show(ex.Message);
    }
}
3. Используйте обёртки для "пожарных" операций:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static class TaskExtensions
{
    public static void FireAndForget(this Task task, Action<Exception> onException = null)
    {
        task.ContinueWith(t => {
            if (t.IsFaulted && onException != null)
            {
                onException(t.Exception.InnerException);
            }
        });
    }
}
 
// Использование
async Task RiskyOperationAsync()
{
    // рискованный код
}
 
// Пожарный вызов с обработкой ошибок
RiskyOperationAsync().FireAndForget(ex => Logger.LogError(ex));

Потеря исключений в асинхронных методах и способы их корректной обработки



Асинхронный код требует особого подхода к обработке исключений. Типичные проблемы:

1. Пропущенные исключения в необработанных задачах:

C#
1
2
3
4
5
6
7
8
9
10
public void StartProcess()
{
    // ПЛОХО: исключения в этой задаче не обрабатываются
    Task.Run(async () => {
        await Task.Delay(100);
        throw new InvalidOperationException("Потерянное исключение");
    });
    
    // Код продолжает работу, не подозревая о проблеме
}
2. Скрытые исключения в WhenAll:

C#
1
2
3
4
5
6
7
8
// При возникновении нескольких исключений будет доступно только одно
public async Task ProcessFilesAsync(string[] filePaths)
{
    var tasks = filePaths.Select(path => ProcessFileAsync(path));
    
    await Task.WhenAll(tasks); // Если несколько задач завершились с ошибкой,
                              // получим только первое исключение
}
Решения:

1. Всегда обрабатывайте исключения в асинхронных методах:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public async Task StartProcessAsync()
{
    try
    {
        await Task.Run(async () => {
            await Task.Delay(100);
            throw new InvalidOperationException("Это исключение будет поймано");
        });
    }
    catch (Exception ex)
    {
        // Обработка исключения
    }
}
2. Для Task.WhenAll сохраняйте ссылку на задачу:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public async Task ProcessFilesAsync(string[] filePaths)
{
    var tasks = filePaths.Select(path => ProcessFileAsync(path)).ToArray();
    
    try
    {
        var whenAllTask = Task.WhenAll(tasks);
        await whenAllTask;
    }
    catch
    {
        // Доступ ко всем исключениям через задачу
        foreach (var task in tasks.Where(t => t.IsFaulted))
        {
            Console.WriteLine(task.Exception.InnerException.Message);
        }
        
        throw; // Пробрасываем дальше, если нужно
    }
}
3. Для Task.WhenAny проверяйте статус каждой завершенной задачи:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public async Task ProcessWithTimeoutAsync(string[] urls, TimeSpan timeout)
{
    var tasks = urls.Select(url => FetchDataAsync(url)).ToList();
    
    while (tasks.Count > 0)
    {
        var timeoutTask = Task.Delay(timeout);
        var completedTask = await Task.WhenAny(tasks.Concat(new[] { timeoutTask }));
        
        if (completedTask == timeoutTask)
        {
            // Обработка таймаута
            break;
        }
        
        tasks.Remove(completedTask);
        
        // Проверяем завершенную задачу на наличие ошибок
        if (completedTask.Status == TaskStatus.Faulted)
        {
            // Обработка ошибки
            LogError(completedTask.Exception.InnerException);
        }
        else if (completedTask.Status == TaskStatus.RanToCompletion)
        {
            // Обработка результата
            ProcessResult(await completedTask);
        }
    }
}

Распространенные антипаттерны при работе с async/await



Вот несколько типичных ошибок и антипаттернов при работе с async/await:

1. Sync over Async (Синхронный код над асинхронным):

C#
1
2
3
4
5
6
7
8
9
10
11
// ПЛОХО: блокировка на асинхронной операции
public int GetUserCount()
{
    return GetUserCountAsync().Result; // Потенциальный deadlock
}
 
// Лучше сделать весь стек асинхронным
public async Task<int> GetUserCountAsync()
{
    return await _repository.GetUserCountAsync();
}
2. Async Void (исключая обработчики событий):

C#
1
2
3
4
5
6
7
8
9
10
11
// ПЛОХО: нельзя ожидать и отслеживать ошибки
async void SaveData(Data data)
{
    await repository.SaveAsync(data);
}
 
// ХОРОШО: возвращаем Task
async Task SaveDataAsync(Data data)
{
    await repository.SaveAsync(data);
}
3. Async All The Way (не создание асинхронных замыканий):

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
// ПЛОХО: начинаем синхронно, затем становимся асинхронными
public string GetFirstUser()
{
    var users = GetUsersAsync().Result; // Блокировка
    return users.FirstOrDefault();
}
 
// ХОРОШО: асинхронный код на всех уровнях
public async Task<string> GetFirstUserAsync()
{
    var users = await GetUsersAsync();
    return users.FirstOrDefault();
}
4. Отказ от использования ConfigureAwait(false) в библиотечном коде:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
// ПЛОХО для библиотек: может вызвать deadlock у потребителя
public async Task<int> LibraryMethodAsync()
{
    await Task.Delay(1000); // Используется контекст вызывающего кода
    return 42;
}
 
// ХОРОШО для библиотек
public async Task<int> LibraryMethodAsync()
{
    await Task.Delay(1000).ConfigureAwait(false);
    return 42;
}
5. Игнорирование возвращаемой задачи:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ПЛОХО: исключения теряются
public void ProcessData()
{
    SaveDataAsync(); // Задача игнорируется
    // Продолжаем выполнение до завершения SaveDataAsync
}
 
// ХОРОШО: либо ожидаем задачу
public async Task ProcessDataAsync()
{
    await SaveDataAsync();
}
 
// Или обрабатываем ошибки явно
public void ProcessData()
{
    SaveDataAsync().ContinueWith(t => {
        if (t.IsFaulted)
            LogError(t.Exception);
    });
}
6. Ложная асинхронность — обёртывание синхронного кода в Task.Run:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ПЛОХО: искусственная асинхронность
public async Task<List<Customer>> GetCustomersAsync()
{
    return await Task.Run(() => {
        using var conn = new SqlConnection(_connectionString);
        conn.Open(); // Блокирующий вызов!
        // Синхронный код доступа к БД
        return LoadCustomers(conn);
    });
}
 
// ХОРОШО: настоящая асинхронность
public async Task<List<Customer>> GetCustomersAsync()
{
    using var conn = new SqlConnection(_connectionString);
    await conn.OpenAsync(); // Асинхронный вариант
    // Асинхронное чтение из БД
    return await LoadCustomersAsync(conn);
}
Task.Run следует использовать только для переноса CPU-интенсивных вычислений в фоновый поток, а не для "асинхронизации" IO-операций, ведь это не даёт преимуществ настоящей асинхронности.

7. Await в циклах неправильным образом:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ПЛОХО: последовательное выполнение, когда параллельного было бы достаточно
public async Task ProcessFilesAsync(string[] filePaths)
{
    foreach (var path in filePaths)
    {
        await ProcessFileAsync(path); // Операции выполняются по очереди
    }
}
 
// ХОРОШО: параллельная обработка с контролем
public async Task ProcessFilesAsync(string[] filePaths)
{
    var tasks = filePaths.Select(path => ProcessFileAsync(path));
    await Task.WhenAll(tasks); // Все операции выполняются параллельно
}
Однако в некоторых случаях последовательное выполнение обосновано (например, если нужно соблюдать порядок или контролировать нагрузку).

8. Использование кода в стиле ContinueWith вместо await:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ПЛОХО: низкоуровневая работа с продолжениями
public Task<int> CalculateValueAsync()
{
    return GetDataAsync().ContinueWith(t => {
        var data = t.Result; // Потенциальный дедлок
        return data * 2;
    });
}
 
// ХОРОШО: простой и понятный код с await
public async Task<int> CalculateValueAsync()
{
    var data = await GetDataAsync();
    return data * 2;
}
9. Нейтральный к контексту async void метод:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
// ПЛОХО: async void с ConfigureAwait(false)
async void Button_Click(object sender, RoutedEventArgs e)
{
    await Task.Delay(100).ConfigureAwait(false);
    textBlock.Text = "Готово"; // Ошибка! Неправильный поток
}
 
// ХОРОШО: сохраняем контекст синхронизации
async void Button_Click(object sender, RoutedEventArgs e)
{
    await Task.Delay(100); // Сохраняем контекст UI
    textBlock.Text = "Готово"; // Корректная работа с UI
}
10. Создание взаимозависимых асинхронных методов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ПЛОХО: взаимная зависимость
public async Task<User> GetOrCreateUserAsync(string username)
{
    var user = await _userRepository.FindByUsernameAsync(username);
    if (user == null)
    {
        user = new User { Username = username };
        await SaveUserAsync(user);
    }
    return user;
}
 
public async Task SaveUserAsync(User user)
{
    // Проверка на существующего пользователя перед сохранением
    var existingUser = await GetOrCreateUserAsync(user.Username);
    if (existingUser.Id != user.Id)
    {
        throw new DuplicateUserException();
    }
    await _userRepository.SaveAsync(user);
}
Такая циклическая зависимость может привести к бесконечной рекурсии или блокировке. Разделяйте ответственность методов.

Еще более тонкие ошибки в асинхронном коде



Существуют и менее очевидные проблемы:

1. Предположение, что await всегда переключает потоки:

C#
1
2
3
4
5
6
7
8
9
10
public async Task<int> ComputeValueAsync()
{
    Debug.WriteLine($"Поток до await: {Thread.CurrentThread.ManagedThreadId}");
    
    // Если задача завершается мгновенно, поток НЕ переключается
    await Task.FromResult(42);
    
    Debug.WriteLine($"Поток после await: {Thread.CurrentThread.ManagedThreadId}");
    return 42;
}
Если асинхронная операция уже завершилась к моменту await, код продолжит выполнение в том же потоке беспрерывно.

2. Считывание изменяемой переменной внутри замыкания:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ПЛОХО: переменная цикла захватывается в замыкании
public async Task ProcessItemsAsync(string[] items)
{
    var tasks = new List<Task>();
    
    for (int i = 0; i < items.Length; i++)
    {
        tasks.Add(Task.Run(async () => {
            // Здесь i может быть уже увеличена на момент выполнения!
            await ProcessItemAsync(items[i]);
        }));
    }
    
    await Task.WhenAll(tasks);
}
 
// ХОРОШО: создаём локальную копию для каждой итерации
public async Task ProcessItemsAsync(string[] items)
{
    var tasks = new List<Task>();
    
    for (int i = 0; i < items.Length; i++)
    {
        int localIndex = i; // Локальная копия
        tasks.Add(Task.Run(async () => {
            await ProcessItemAsync(items[localIndex]);
        }));
    }
    
    await Task.WhenAll(tasks);
}
3. Использование локальных переменных после await без учёта их изменений:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ОПАСНО: значение может измениться во время ожидания
public async Task<string> GetUserDataAsync(User user)
{
    var userData = await _httpClient.GetStringAsync($"api/users/{user.Id}");
    
    // Если во время ожидания user.Id изменился, мы получим данные для старого Id
    return $"Данные пользователя {user.Id}: {userData}";
}
 
// БЕЗОПАСНО: фиксируем значение до асинхронной операции
public async Task<string> GetUserDataAsync(User user)
{
    var userId = user.Id; // Фиксируем значение
    var userData = await _httpClient.GetStringAsync($"api/users/{userId}");
    
    return $"Данные пользователя {userId}: {userData}";
}
4. Игнорирование правил очистки ресурсов с async/await:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ПЛОХО: потенциальная утечка ресурсов
public async Task<string> ReadFileAsync(string path)
{
    var file = new StreamReader(path);
    var content = await file.ReadToEndAsync();
    file.Dispose(); // Может не выполниться при исключении
    return content;
}
 
// ХОРОШО: используем using для гарантированной очистки
public async Task<string> ReadFileAsync(string path)
{
    using (var file = new StreamReader(path))
    {
        return await file.ReadToEndAsync();
    }
}
 
// Еще лучше с C# 8.0+
public async Task<string> ReadFileAsync(string path)
{
    using var file = new StreamReader(path);
    return await file.ReadToEndAsync();
}
5. **Использование Task<T> там, где подходит ValueTask<T>**:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Менее эффективно, если часто завершается синхронно
public async Task<int> GetCachedValueAsync(string key)
{
    if (_cache.TryGetValue(key, out var value))
        return value; // Всё равно создаётся Task<int>
        
    value = await ComputeValueAsync(key);
    _cache[key] = value;
    return value;
}
 
// Более эффективно при частых кеш-хитах
public async ValueTask<int> GetCachedValueAsync(string key)
{
    if (_cache.TryGetValue(key, out var value))
        return value; // Не создаётся лишний объект Task<int>
        
    value = await ComputeValueAsync(key);
    _cache[key] = value;
    return value;
}

Полезные практики при работе с async/await



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

1. Стандартизация именования:
- Суффикс Async для всех асинхронных методов (кроме асинхронных лямбд и обработчиков событий)
- Соблюдение соглашения OperationAsync для методов, возвращающих Task

2. Использование асинхронных фабрик:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ResourceManager
{
    // Фабричный метод вместо асинхронного конструктора
    public static async Task<ResourceManager> CreateAsync()
    {
        var manager = new ResourceManager();
        await manager.InitializeAsync();
        return manager;
    }
    
    private async Task InitializeAsync()
    {
        // Асинхронная инициализация
    }
}
3. Предоставление для каждого асинхронного API перегрузки с CancellationToken:

C#
1
2
3
4
5
6
7
8
public Task<Result> GetDataAsync() => GetDataAsync(CancellationToken.None);
 
public async Task<Result> GetDataAsync(CancellationToken cancellationToken)
{
    // Реализация с поддержкой отмены
    cancellationToken.ThrowIfCancellationRequested();
    // ...
}
4. Избегание асинхронных void методов через делегирование:

C#
1
2
3
4
5
6
7
8
9
10
// Вместо async void обертываем в безопасный метод
private void Button_Click(object sender, EventArgs e)
{
    HandleButtonClickAsync().FireAndForget(ex => LogError(ex));
}
 
private async Task HandleButtonClickAsync()
{
    // Асинхронная логика обработки
}
5. Использование async-aware объектов синхронизации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Вместо обычных Lock объектов
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
 
public async Task<Data> GetDataAsync()
{
    await _lock.WaitAsync();
    try
    {
        return await InternalGetDataAsync();
    }
    finally
    {
        _lock.Release();
    }
}
6. Тестирование исключительных ситуаций:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Test]
public async Task HandleExceptions_ShouldCatchAndLogErrors()
{
    // Устанавливаем мок, возвращающий исключение
    _mockService
        .Setup(s => s.GetDataAsync(It.IsAny<string>()))
        .ThrowsAsync(new InvalidOperationException("Test exception"));
    
    // Проверяем, что наш код корректно обрабатывает исключение
    var result = await _sut.ProcessDataAsync("test");
    
    Assert.That(result.IsSuccess, Is.False);
    Assert.That(result.Error, Contains.Substring("Test exception"));
}
7. Комбинирование асинхронных и синхронных API методов правильно:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
// Синхронный метод, вызывающий асинхронный безопасно
public int GetValue(string key)
{
    // Запускаем асинхронную операцию новым потоком, избегая deadlock
    return Task.Run(() => GetValueAsync(key)).GetAwaiter().GetResult();
}
 
public async Task<int> GetValueAsync(string key)
{
    // Асинхронная реализация
    await Task.Delay(100);
    return 42;
}
Понимание этих нюансов async/await существенно повышает качество и надежность асинхронного кода, позволяя избежать множества подводных камней.

Производительность и оптимизация



Асинхронное программирование с использованием async/await — это не только удобство разработки, но и мощный инструмент оптимизации производительности приложений. Чтобы максимально использовать его потенциал, необходимо глубоко понимать, как асинхронный код влияет на систему ресурсов и как правильно его оптимизировать.

Пулы потоков и как async/await влияет на их использование



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

C#
1
2
3
4
5
6
7
8
9
10
11
public async Task ProcessFilesAsync(string[] files)
{
   foreach (var file in files)
   {
       // Поток возвращается в пул во время ожидания операции ввода-вывода
       var content = await File.ReadAllTextAsync(file);
       // После завершения чтения, поток из пула (возможно другой) 
       // продолжает выполнение кода
       ProcessContent(content);
   }
}
В .NET ThreadPool по умолчанию имеет минимальный размер, равный количеству логических процессоров, и может динамически увеличиваться при необходимости. При использовании async/await нагрузка на пул потоков значительно снижается, поскольку потоки не блокируются на операциях ввода-вывода.

Сравним синхронный и асинхронный подходы.

Синхронный код:
1. На каждую параллельную операцию ввода-вывода требуется отдельный поток.
2. Потоки блокируются и простаивают во время ожидания.
3. Быстрое исчерпание пула потоков при высокой нагрузке.

Асинхронный код с async/await:
1. Единовременно используется минимальное количество потоков, даже при тысячах параллельных операций.
2. Потоки никогда не простаивают, возвращаются в пул.
3. Пул потоков редко исчерпывается, что предотвращает нехватку ресурсов.

Важно отметить, что использование async/await наиболее эффективно для операций ввода-вывода (IO-bound), таких как работа с сетью, файловой системой или базами данных. Для вычислительно-интенсивных операций (CPU-bound) преимущества менее заметны, и в некоторых случаях асинхронный код может даже добавить лишние накладные расходы.

Измерение производительности асинхронного кода



Для эффективной оптимизации необходимо правильно измерять производительность асинхронного кода. Это сложнее, чем может показаться на первый взгляд, поскольку традиционные методы профилирования не всегда корректно работают с кодом, использующим async/await.

Ключевые метрики для асинхронного кода



1. Пропускная способность — количество операций, которые система может выполнить за единицу времени (запросы в секунду, транзакции в секунду).
2. Эффективность использования ресурсов — количество используемых потоков, объем памяти, загрузка процессора.
3. Время отклика — время между запросом и получением результата.
4. Масштабируемость — как изменяются показатели при увеличении нагрузки.

Для измерения этих метрик можно использовать следующие инструменты:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public async Task<PerformanceResult> MeasurePerformanceAsync(Func<Task> asyncMethod, 
                                                        int iterations)
{
    var stopwatch = new Stopwatch();
    var memoryBefore = GC.GetTotalMemory(true);
    
    stopwatch.Start();
    for (int i = 0; i < iterations; i++)
    {
        await asyncMethod();
    }
    stopwatch.Stop();
    
    var memoryAfter = GC.GetTotalMemory(false);
    var memoryUsed = memoryAfter - memoryBefore;
    
    return new PerformanceResult
    {
        TotalTime = stopwatch.Elapsed,
        AverageOperationTime = TimeSpan.FromTicks(stopwatch.Elapsed.Ticks / iterations),
        MemoryUsed = memoryUsed,
        Throughput = iterations / stopwatch.Elapsed.TotalSeconds
    };
}
Для более сложных сценариев рекомендуется использовать специализированные библиотеки, такие как BenchmarkDotNet, которые учитывают особенности асинхронного кода и обеспечивают более точные результаты.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[MemoryDiagnoser]
public class AsyncBenchmarks
{
    [Benchmark]
    public async Task AsyncImplementation()
    {
        await PerformAsyncOperation();
    }
    
    [Benchmark]
    public void SyncImplementation()
    {
        PerformSyncOperation();
    }
    
    private async Task PerformAsyncOperation()
    {
        // Асинхронная реализация...
    }
    
    private void PerformSyncOperation()
    {
        // Синхронная реализация...
    }
}

Лучшие практики для высоконагруженных систем



В высоконагруженных системах критически важно максимально эффективное использование ресурсов. Вот несколько практик, которые помогут оптимизировать асинхронный код:

1. Минимизация аллокаций памяти



C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Неоптимально: создаёт новый Task<int> при каждом вызове, даже если результат кэширован
public async Task<int> GetCachedValueAsync(string key)
{
    if (_cache.TryGetValue(key, out var value))
        return value;
    
    value = await FetchValueAsync(key);
    _cache[key] = value;
    return value;
}
 
// Оптимально: использует ValueTask<T> для уменьшения аллокаций
public ValueTask<int> GetCachedValueAsync(string key)
{
    if (_cache.TryGetValue(key, out var value))
        return new ValueTask<int>(value);
    
    return new ValueTask<int>(FetchAndCacheValueAsync(key));
}
 
private async Task<int> FetchAndCacheValueAsync(string key)
{
    var value = await FetchValueAsync(key);
    _cache[key] = value;
    return value;
}

2. Повторное использование объектов



Для избежания излишних аллокаций в циклах высоконагруженных операций, используйте пулы объектов и буферов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class BufferedHttpClient
{
    private readonly HttpClient _client;
    private readonly ObjectPool<MemoryStream> _streamPool;
    
    public BufferedHttpClient(HttpClient client)
    {
        _client = client;
        _streamPool = new DefaultObjectPool<MemoryStream>(
            new DefaultPooledObjectPolicy<MemoryStream>());
    }
    
    public async Task<byte[]> GetDataAsync(string url)
    {
        var response = await _client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
        response.EnsureSuccessStatusCode();
        
        var stream = _streamPool.Get();
        try
        {
            await using var contentStream = await response.Content.ReadAsStreamAsync();
            await contentStream.CopyToAsync(stream);
            return stream.ToArray();
        }
        finally
        {
            stream.SetLength(0); // Очищаем перед возвратом в пул
            _streamPool.Return(stream);
        }
    }
}

3. Правильная настройка ServicePointManager



В сетевых приложениях критически важно настроить параметры соединений:

C#
1
2
3
4
5
6
7
8
// Увеличиваем количество одновременных соединений к одному хосту
ServicePointManager.DefaultConnectionLimit = 100;
 
// Предотвращаем повторное использование DNS-записей слишком долго
ServicePointManager.DnsRefreshTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds;
 
// Позволяем повторно использовать порты быстрее после закрытия соединения
ServicePointManager.MaxServicePointIdleTime = (int)TimeSpan.FromSeconds(30).TotalMilliseconds;

4. Контроль степени параллелизма



Иногда требуется ограничить количество одновременно выполняемых операций:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ThrottledTaskProcessor
{
    private readonly SemaphoreSlim _throttle;
    
    public ThrottledTaskProcessor(int maxConcurrency)
    {
        _throttle = new SemaphoreSlim(maxConcurrency);
    }
    
    public async Task<IEnumerable<TResult>> ProcessBatchAsync<TItem, TResult>(
        IEnumerable<TItem> items,
        Func<TItem, Task<TResult>> processAsync)
    {
        var tasks = new List<Task<TResult>>();
        
        foreach (var item in items)
        {
            await _throttle.WaitAsync();
            
            tasks.Add(Task.Run(async () =>
            {
                try
                {
                    return await processAsync(item);
                }
                finally
                {
                    _throttle.Release();
                }
            }));
        }
        
        return await Task.WhenAll(tasks);
    }
}

5. Использование пользовательских пулов потоков для CPU-интенсивных операций



Если вам нужно выполнить тяжелые вычисления, лучше не загружать стандартный пул потоков:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ComputationService
{
    private readonly TaskFactory _computationTaskFactory;
    
    public ComputationService(int dedicatedThreads)
    {
        var threadPoolSettings = new ThreadPoolSettings
        {
            MinThreads = dedicatedThreads,
            MaxThreads = dedicatedThreads,
            StackSize = 1024 * 1024 // 1 МБ стек
        };
        
        _computationTaskFactory = new TaskFactory(
            new ThreadPoolTaskScheduler(threadPoolSettings));
    }
    
    public Task<ComputationResult> ComputeAsync(ComputationRequest request)
    {
        return _computationTaskFactory.StartNew(() => 
        {
            // Тяжелые вычисления...
            return new ComputationResult();
        });
    }
}

Техники профилирования асинхронного кода



Профилирование асинхронного кода имеет свои особенности. Традиционные инструменты профилирования не всегда корректно отображают поток выполнения через границы await.

1. Трассировка асинхронных операций с помощью Activity



C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public async Task<OrderResult> ProcessOrderAsync(Order order)
{
    using (var activity = new Activity("ProcessOrder").Start())
    {
        activity.AddTag("OrderId", order.Id);
        activity.AddTag("CustomerId", order.CustomerId);
        
        using (var validateActivity = new Activity("ValidateOrder").Start())
        {
            await ValidateOrderAsync(order);
        }
        
        using (var paymentActivity = new Activity("ProcessPayment").Start())
        {
            await ProcessPaymentAsync(order);
        }
        
        using (var fulfillActivity = new Activity("FulfillOrder").Start())
        {
            await FulfillOrderAsync(order);
        }
        
        return new OrderResult { Success = true };
    }
}

2. Асинхронно-совместимые счетчики производительности



Для измерения производительности асинхронных методов лучше использовать инструменты, понимающие асинхронный контекст:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class AsyncPerformanceMonitor
{
    private readonly ConcurrentDictionary<string, Histogram> _histograms = 
        new ConcurrentDictionary<string, Histogram>();
    
    public async Task<T> TrackOperationAsync<T>(string operationName, Func<Task<T>> operation)
    {
        var histogram = _histograms.GetOrAdd(operationName, _ => new Histogram());
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            return await operation();
        }
        finally
        {
            stopwatch.Stop();
            histogram.Record(stopwatch.Elapsed.TotalMilliseconds);
        }
    }
    
    public PerformanceMetrics GetMetrics(string operationName)
    {
        if (_histograms.TryGetValue(operationName, out var histogram))
        {
            return new PerformanceMetrics
            {
                OperationName = operationName,
                AverageMs = histogram.Mean,
                MedianMs = histogram.Median,
                P95Ms = histogram.Percentile95,
                P99Ms = histogram.Percentile99,
                MinMs = histogram.Min,
                MaxMs = histogram.Max,
                Count = histogram.Count
            };
        }
        
        return null;
    }
}

3. Комплексное профилирование с распределенной трассировкой



Для сложных распределенных систем полезно использовать распределенную трассировку:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class TracingHttpClient
{
    private readonly HttpClient _client;
    private readonly ITracer _tracer;
    
    public TracingHttpClient(HttpClient client, ITracer tracer)
    {
        _client = client;
        _tracer = tracer;
    }
    
    public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
    {
        using (var scope = _tracer.BuildSpan("HttpClient.SendAsync").StartActive(true))
        {
            scope.Span.SetTag("http.method", request.Method.ToString());
            scope.Span.SetTag("http.url", request.RequestUri.ToString());
            
            try
            {
                var response = await _client.SendAsync(request);
                
                scope.Span.SetTag("http.status_code", (int)response.StatusCode);
                
                if (!response.IsSuccessStatusCode)
                {
                    scope.Span.SetTag("error", true);
                    scope.Span.SetTag("error.message", $"HTTP {response.StatusCode}");
                }
                
                return response;
            }
            catch (Exception ex)
            {
                scope.Span.SetTag("error", true);
                scope.Span.SetTag("error.message", ex.Message);
                throw;
            }
        }
    }
}

Асинхронность в микросервисной архитектуре



Микросервисная архитектура предъявляет особые требования к асинхронному коду, так как включает множество сетевых взаимодействий между сервисами.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ServiceClient
{
    private readonly HttpClient _httpClient;
    private readonly ICircuitBreaker _circuitBreaker;
    private readonly IRetryPolicy _retryPolicy;
    
    public async Task<T> GetDataAsync<T>(string endpoint)
    {
        return await _retryPolicy.ExecuteAsync(async () =>
        {
            return await _circuitBreaker.ExecuteAsync(async () =>
            {
                var response = await _httpClient.GetAsync(endpoint);
                response.EnsureSuccessStatusCode();
                var json = await response.Content.ReadAsStringAsync();
                return JsonSerializer.Deserialize<T>(json);
            });
        });
    }
}
В микросервисной среде особенно важно реализовать шаблоны устойчивости, такие как Circuit Breaker (прерыватель цепи), Retry (повторные попытки), Timeout (таймауты) и Bulkhead (изоляция отсеков). Асинхронное программирование играет ключевую роль в этих шаблонах.

Сравнение потребления памяти в синхронном и асинхронном коде



Разница в потреблении памяти между синхронным и асинхронным кодом часто недооценивается. Асинхронное программирование меняет не только производительность, но и характер использования памяти. При синхронном выполнении каждый поток требует выделения стека размером около 1 МБ. Если приложение обрабатывает тысячи одновременных операций, это может привести к значительному потреблению памяти. Напротив, асинхронный код может обрабатывать тысячи операций с использованием всего нескольких потоков.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Синхронный подход - для каждого соединения нужен отдельный поток
public void HandleConnectionsSync(int connectionCount)
{
    var threads = new List<Thread>();
    for (int i = 0; i < connectionCount; i++)
    {
        var thread = new Thread(() => 
        {
            // Обработка соединения занимает поток на всё время
            ProcessConnection();
        });
        threads.Add(thread);
        thread.Start();
    }
    
    // Ожидание завершения всех потоков
    foreach (var thread in threads)
    {
        thread.Join();
    }
}
 
// Асинхронный подход - использует пул потоков эффективнее
public async Task HandleConnectionsAsync(int connectionCount)
{
    var tasks = new List<Task>();
    for (int i = 0; i < connectionCount; i++)
    {
        tasks.Add(ProcessConnectionAsync());
    }
    
    await Task.WhenAll(tasks);
}
При обработке 10 000 соединений синхронный код может потребовать около 10 ГБ только для стеков потоков, в то время как асинхронный подход может справиться с тем же объемом работы, используя лишь несколько потоков и значительно меньше памяти. Однако, асинхронное программирование не всегда экономит память. Каждый объект Task и состояние асинхронной машины также требуют выделения памяти. Для оптимизации следует:
1. Использовать ValueTask вместо Task, когда операция часто завершается синхронно.
2. Кэшировать часто используемые задачи, особенно для предопределённых результатов.
3. Использовать пулы буферов для операций ввода-вывода.
4. Избегать бесконтрольного создания новых задач в циклах.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Оптимизированное использование памяти с помощью ArrayPool
public async Task<string> ReadFileOptimizedAsync(string path)
{
    const int BufferSize = 4096;
    byte[] rentedBuffer = ArrayPool<byte>.Shared.Rent(BufferSize);
    
    try
    {
        using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, 
            FileShare.Read, BufferSize, useAsync: true);
            
        using var memoryStream = new MemoryStream();
        int bytesRead;
        
        while ((bytesRead = await fileStream.ReadAsync(
            rentedBuffer, 0, BufferSize)) > 0)
        {
            await memoryStream.WriteAsync(rentedBuffer, 0, bytesRead);
        }
        
        return Encoding.UTF8.GetString(memoryStream.ToArray());
    }
    finally
    {
        // Возвращаем буфер в пул
        ArrayPool<byte>.Shared.Return(rentedBuffer);
    }
}

Влияние размера пула потоков на эффективность асинхронного кода



Размер пула потоков — критический фактор, влияющий на производительность асинхронных приложений. По умолчанию .NET ThreadPool динамически регулирует количество потоков, но в некоторых сценариях может потребоваться ручная настройка. Правильная настройка пула потоков особенно важна, когда асинхронный код включает CPU-интенсивные операции или блокирующие вызовы:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void ConfigureThreadPool()
{
    // Получаем текущие настройки
    ThreadPool.GetMinThreads(out int workerThreads, out int completionPortThreads);
    
    // Увеличиваем минимальное количество потоков
    // Это может помочь избежать задержек при запуске приложения
    ThreadPool.SetMinThreads(Environment.ProcessorCount * 4, completionPortThreads);
    
    // Устанавливаем максимальное количество потоков
    // Осторожно: слишком большое значение может привести к избыточному 
    // переключению контекста
    ThreadPool.SetMaxThreads(Environment.ProcessorCount * 16, completionPortThreads);
}
Оптимальные значения зависят от характера приложения:
1. IO-интенсивные приложения (веб-серверы, API) могут выиграть от большего количества потоков, поскольку потоки часто освобождаются для повторного использования.
2. CPU-интенсивные приложения должны ограничивать количество одновременно активных потоков примерно до количества логических процессоров.

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

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class OptimizedParallelProcessor
{
    private readonly ParallelOptions _parallelOptions;
    
    public OptimizedParallelProcessor(int maxDegreeOfParallelism = 0)
    {
        _parallelOptions = new ParallelOptions
        {
            // Если не указано, используем количество процессоров
            MaxDegreeOfParallelism = maxDegreeOfParallelism > 0 
                ? maxDegreeOfParallelism 
                : Environment.ProcessorCount
        };
    }
    
    public async Task ProcessItemsAsync<T>(IEnumerable<T> items, Func<T, Task> processor)
    {
        // Используем Parallel.ForEachAsync для контролируемого параллельного выполнения
        await Parallel.ForEachAsync(
            items, 
            _parallelOptions, 
            async (item, token) => await processor(item));
    }
}
Измерение влияния размера пула потоков следует проводить с помощью нагрузочного тестирования в условиях, близких к рабочим. Часто оптимальные настройки отличаются от ожидаемых.

Сценарии, когда асинхронность может ухудшить производительность



Хотя async/await — мощный инструмент, существуют случаи, когда его применение может снизить производительность:

1. Чрезмерная детализация асинхронных методов



Если разбить крупную операцию на множество мелких асинхронных методов, это может привести к избыточным накладным расходам:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Неоптимально: слишком много асинхронных границ
public async Task<User> GetUserDetailsAsync(int userId)
{
    User user = await GetUserAsync(userId);
    user.Profile = await GetProfileAsync(userId);
    user.Settings = await GetSettingsAsync(userId);
    user.Preferences = await GetPreferencesAsync(userId);
    return user;
}
 
// Лучше: уменьшаем количество асинхронных границ
public async Task<User> GetUserDetailsEfficientAsync(int userId)
{
    var userTask = GetUserAsync(userId);
    var profileTask = GetProfileAsync(userId);
    var settingsTask = GetSettingsAsync(userId);
    var preferencesTask = GetPreferencesAsync(userId);
    
    // Запускаем все задачи параллельно и ждем их завершения
    await Task.WhenAll(userTask, profileTask, settingsTask, preferencesTask);
    
    var user = await userTask;
    user.Profile = await profileTask;
    user.Settings = await settingsTask;
    user.Preferences = await preferencesTask;
    return user;
}

2. Асинхронность в критических путях с низкой задержкой



В сценариях, где критична минимальная задержка, операции с очень низкой задержкой могут выиграть от синхронного выполнения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Для локальных кэшей синхронная версия может быть быстрее
public int GetCachedValueSync(string key)
{
    if (_cache.TryGetValue(key, out var value))
        return value;
    
    return ComputeValueSync(key);
}
 
// Асинхронная версия добавляет накладные расходы на Task
public ValueTask<int> GetCachedValueAsync(string key)
{
    if (_cache.TryGetValue(key, out var value))
        return new ValueTask<int>(value);
    
    return new ValueTask<int>(ComputeValueAsync(key));
}

3. Блокировка на асинхронных методах в высоконагруженных путях



Иногда блокировка асинхронного кода может быть производительнее в очень ограниченных сценариях:

C#
1
2
3
4
5
6
7
8
// В некоторых специфических сценариях блокирующий код может быть эффективнее
// ВНИМАНИЕ: этот подход имеет серьезные ограничения и требует тщательного тестирования!
public string GetDataBlocking()
{
    // Только если мы уверены, что это выполняется в фоновом потоке,
    // и не вызовет deadlock
    return HttpClient.GetStringAsync("http://example.com").GetAwaiter().GetResult();
}

4. CPU-интенсивные операции с незначительной асинхронностью



Если операция в основном использует CPU и только незначительную часть времени тратит на ожидание, асинхронность может не дать преимуществ:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Оператор await создаёт дополнительные накладные расходы
public async Task<int> ComputeWithMinimalIOAsync(int input)
{
    // 99% времени - вычисления, 1% - ввод-вывод
    var result = ExpensiveComputation(input);
    
    // Только небольшая часть операции асинхронная
    await Task.Delay(10); // имитация небольшого ввода-вывода
    
    return result + 1;
}
 
// Может быть эффективнее запустить весь метод в Task.Run
public Task<int> ComputeMoreEfficientlyAsync(int input)
{
    return Task.Run(() => {
        var result = ExpensiveComputation(input);
        Thread.Sleep(10); // имитация небольшого ввода-вывода
        return result + 1;
    });
}

5. Излишнее распараллеливание



Чрезмерное распараллеливание без учета характера задач может привести к снижению производительности:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Неправильное использование асинхронности для мелких операций
public async Task ProcessDataAsync(string[] items)
{
    // Для очень маленьких операций накладные расходы Task могут перевесить выгоду
    var tasks = items.Select(async item => {
        await Task.Yield(); // Имитация асинхронности
        return item.ToUpperInvariant(); // Простая операция
    }).ToList();
    
    var results = await Task.WhenAll(tasks);
    // Дальнейшая обработка...
}
 
// Для маленьких операций синхронный подход может быть эффективнее
public Task<string[]> ProcessDataEfficientlyAsync(string[] items)
{
    return Task.Run(() => {
        return items.Select(item => item.ToUpperInvariant()).ToArray();
    });
}
При разработке высокопроизводительных систем важно помнить, что асинхронность — не универсальное решение. Каждый сценарий требует анализа и часто экспериментальной проверки для нахождения оптимального подхода. Только понимая внутренние механизмы async/await и правильно измеряя производительность, можно принимать обоснованные решения о том, когда и как использовать асинхронное программирование.

Новейшие возможности в современных версиях C#



Язык C# непрерывно эволюционирует, и каждая новая версия добавляет возможности, делающие асинхронное программирование еще более мощным и удобным. Рассмотрим самые значимые нововведения в области асинхронного программирования, появившиеся в последних версиях C#.

Асинхронные стримы в C# 8.0 и выше



До C# 8.0 мы могли возвращать из асинхронного метода только один результат. Если требовалось вернуть последовательность значений, приходилось возвращать Task<IEnumerable<T>> или Task<List<T>>, но это означало, что нужно дожидаться получения всех элементов коллекции перед началом их обработки.

C# 8.0 представил асинхронные стримы — механизм, позволяющий метода возвращать элементы последовательно, по мере их готовности. Для этого используются новые типы:

IAsyncEnumerable<T> — асинхронный аналог IEnumerable,
IAsyncEnumerator<T> — асинхронный перечислитель,
await foreach — новый синтаксис для перебора элементов асинхронного стрима.

Вот как выглядит базовый пример:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public async IAsyncEnumerable<int> GenerateSequenceAsync()
{
    for (int i = 0; i < 20; i++)
    {
        await Task.Delay(100); // Имитация асинхронной работы
        yield return i;        // Возвращаем элемент, не дожидаясь формирования всей коллекции
    }
}
 
public async Task ConsumeSequenceAsync()
{
    await foreach (var number in GenerateSequenceAsync())
    {
        Console.WriteLine(number);
        // Обработка начинается до того, как сформирована вся последовательность
    }
}
Этот подход особенно полезен при работе с потоковой передачей данных, обработкой больших наборов данных, и для API, которые природно возвращают данные последовательно (например, потоковое чтение из базы данных).

Отмена и настройка контекста в асинхронных стримах



Асинхронные стримы поддерживают токены отмены и настройки контекста через атрибуты:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public async IAsyncEnumerable<string> ReadLinesAsync(
    string filePath,
    [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    using var reader = File.OpenText(filePath);
    while (!reader.EndOfStream)
    {
        cancellationToken.ThrowIfCancellationRequested();
        var line = await reader.ReadLineAsync();
        if (line != null)
            yield return line;
    }
}
 
// Использование
async Task ProcessFileAsync(string path, CancellationToken token)
{
    await foreach (var line in ReadLinesAsync(path, token).ConfigureAwait(false))
    {
        // Обработка каждой строки
    }
}
Атрибут [EnumeratorCancellation] указывает, что параметр должен использоваться для отмены перечисления. Метод ConfigureAwait(false) работает аналогично обычным задачам, указывая, что продолжение не требует возврата в исходный контекст синхронизации.

Асинхронные LINQ операторы и их практическое применение



C# 9.0 и новее получили поддержку асинхронных версий LINQ-операторов, что позволяет использовать мощь LINQ совместно с асинхронными стримами:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
 
// Имитация асинхронного источника данных
public async IAsyncEnumerable<SensorData> GetSensorDataAsync()
{
    var random = new Random();
    
    for (int i = 0; i < 100; i++)
    {
        await Task.Delay(50); // Имитация задержки получения данных
        yield return new SensorData
        {
            Id = i,
            Temperature = random.Next(10, 40),
            Humidity = random.Next(30, 90),
            Timestamp = DateTime.Now
        };
    }
}
 
// Использование асинхронных LINQ-операторов
public async Task ProcessSensorDataAsync()
{
    // Фильтрация асинхронной последовательности
    var highTempReadings = GetSensorDataAsync()
        .Where(reading => reading.Temperature > 30);
        
    // Проекция данных
    var temperatureReports = highTempReadings
        .Select(reading => new
        {
            reading.Id,
            reading.Temperature,
            Alert = reading.Temperature > 35 ? "Критичная" : "Высокая",
            reading.Timestamp
        });
        
    // Асинхронная агрегация
    double averageTemp = await temperatureReports
        .AverageAsync(report => report.Temperature);
        
    // Вывод результатов
    await foreach (var report in temperatureReports)
    {
        Console.WriteLine($"Датчик {report.Id}: {report.Temperature}°C ({report.Alert})");
    }
    
    Console.WriteLine($"Средняя температура: {averageTemp:F1}°C");
}
 
public class SensorData
{
    public int Id { get; set; }
    public double Temperature { get; set; }
    public int Humidity { get; set; }
    public DateTime Timestamp { get; set; }
}
Эта комбинация асинхронных стримов и LINQ операторов открывает новые возможности для элегантной обработки последовательных данных.

Примеры асинхронных LINQ операторов



Библиотеки, такие как System.Linq.Async, предоставляют асинхронные версии большинства стандартных LINQ методов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Фильтрация с асинхронным предикатом
var largeFiles = directoryFiles
    .WhereAwait(async file => 
        (await file.GetInfoAsync()).Length > 1024 * 1024);
 
// Асинхронная проекция
var fileContents = directoryFiles
    .SelectAwait(async file => await File.ReadAllTextAsync(file));
 
// Асинхронная сортировка
var sortedResults = await queryResults
    .OrderByAwaitAsync(async item => await GetSortKeyAsync(item));
 
// Асинхронная группировка
var groupedData = await dataStream
    .GroupByAwaitAsync(
        async item => await DetermineGroupAsync(item),
        async item => await TransformItemAsync(item)
    );

Внутренние оптимизации компилятора для современных асинхронных конструкций



Современные версии C# компилятора реализуют несколько важных оптимизаций для асинхронного кода:

1. Пулинг объектов машины состояний



Начиная с C# 7.0, компилятор может использовать структуры вместо классов для реализации конечных автоматов асинхронного кода. Это значительно снижает нагрузку на сборщик мусора.

2. Устранение избыточных асинхронных операций



Компилятор может оптимизировать случаи, когда асинхронный метод имеет простые пути, не требующие реального асинхронного выполнения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// До оптимизации
public async Task<int> GetValueAsync(bool condition)
{
    if (condition)
    {
        return 42; // Компилятор создаст Task<int> даже для этого пути
    }
    
    return await ExpensiveOperationAsync();
}
 
// После внутренней оптимизации
public Task<int> GetValueAsync(bool condition)
{
    if (condition)
    {
        return Task.FromResult(42); // Использует кешированные задачи
    }
    
    return ExpensiveOperationAsync();
}

3. Улучшение работы await на горячих путях



Более эффективная проверка IsCompleted для оптимизации случаев, когда задача уже завершена перед await:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Концептуальное представление оптимизации
public async Task<int> OptimizedAwaitAsync()
{
    Task<int> task = ComputeValueAsync();
    
    if (task.IsCompleted)
    {
        // Оптимизированный путь - избегаем создания конечного автомата
        return task.Result;  
    }
    
    // Стандартный путь с машиной состояний
    return await task;
}

4. PoolingAsyncValueTaskMethodBuilder



В новых версиях .NET добавлены оптимизированные построители для ValueTask, которые могут переиспользовать объекты продолжений из пулов, значительно снижая аллокации памяти в горячих путях с частыми асинхронными вызовами.

Интеграция с другими современными возможностями C#



Асинхронное программирование эффективно сочетается с другими современными функциями языка:

Использование с записями (records) и init-only свойствами



C#
1
2
3
4
5
6
7
8
9
10
11
public record UserData(int Id, string Name, string Email);
 
public async Task<UserData> FetchUserAsync(int id)
{
    var data = await _repository.GetUserDataAsync(id);
    return new UserData(
        Id: data.Id,
        Name: data.Name,
        Email: await _emailService.NormalizeEmailAsync(data.Email)
    );
}

Интеграция с паттерном сопоставления (pattern matching)



C#
1
2
3
4
5
6
7
8
9
10
11
12
public async Task ProcessResultAsync(Task<ApiResult> resultTask)
{
    var result = await resultTask;
    
    await (result switch
    {
        SuccessResult success => HandleSuccessAsync(success.Data),
        ErrorResult { Code: 404 } => HandleNotFoundAsync(),
        ErrorResult error when error.Code >= 500 => HandleServerErrorAsync(error),
        _ => HandleUnknownResultAsync(result)
    });
}

Интеграция с null-условными операторами



C#
1
2
3
4
5
6
public async Task<string> GetUserNameAsync(int? userId)
{
    return await (userId is null 
        ? Task.FromResult("Гость") 
        : _userService.GetUserNameAsync(userId.Value));
}
С каждой новой версией C#, асинхронное программирование становится все более интегрированным с остальными возможностями языка, что позволяет писать более чистый, эффективный и легко поддерживаемый код.

Асинхронное программирование await async
Всем привет! Пытаюсь разобраться с асинхронным вызовом методов. Узнал такую вещь: если...

Асинхронное программирование, Async и Await
нужно сделать программу, которая в отдельном методе заполняет массив случайными числами. Пока...

Переписать асинхронный метод без использования async/await
Ломаю голову как переписать асинхронный метод что бы достичь того же эффекта как при использовании...

Асинхронность (async-await)
Доброе утро! Помогите разобраться на простом примере. Ответ мне пригодится для моего проекта. Есть...

Асинхронность (async/await) в ASP.NET MVC
Доброго дня форумчане, помогите пожалуйста разобраться. Речь идет об использовании асинхронности в...

Асинхронное программирование, await, async
Здравствуйте, нужна помощь с этим методом. Мне надо создать бота для Telegram что бы он отправлял...

Async/await. Как сделать метод асинхронным?
Есть метод, который получает html private static string GetHtml(string id) { ...

Использую async и await но асинхронность так и не работает
Добрый день. Глупый вопрос, но почему в данной коде не работает асинхронность? И что надо...

Thread, async, await, tasks. Асинхронность
В одной из лаб нам надо было посчитать интегралы разными способами, в разном количестве thread,...

Многопоточность и асинхронность в дочерних формах. Async/await
Здравствуйте! Имеется такая задача: Из главной формы создается несколько дочерних форм (к...

HttpWebRequest Async как отправить запросы асинхронно?
Как возможно выполнить это асинхронно? public static HttpWebRequest POST(string data1, string...

ValidationRule Async Асинхронная валидация данных
Доброго времени суток. Есть форма один текст бокс два пасворд бокса. В первый текст бокс вводится...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Лето уходит
kumehtar 17.08.2026
Мысли в слух
kumehtar 17.08.2026
Забавно, насколько сейчас стала доступна информация. Например о магии, духовном развитии, медитациях, и других подобных направлениях, ранее зачастую тайных, передаваемых от учителя к ученику. Хотя. . .
Перемещение строк из ТЧ в другой документ с учетом текущего пробега
Maks 17.08.2026
Реализация из решения ниже выполнена на примере нетипового документа "Автозапчасти", с ТЧ "Шины". За основу взят алгоритм отсюда: https:/ / www. cyberforum. ru/ blogs/ 359708/ 10838. html Задача: . . .
Саморегулирующийся социальный контракт для сервера cross-section.
Hrethgir 14.08.2026
С кодом конечно таких глубоких размышлений пока не было, впрочем я уже привык к алгоритмизации. Суть предмета записи: снова в диалоге с нейросетью (я взял пока себе ник для учётки админа - Rector). . . .
Часы электронные
Uhbif79 12.08.2026
Выкладываю программу часов. Программа позволяет: 1. Использовать системное время и дату, 2. Есть возможность вводить время и дату вручную. 3. Реализованы 2 будильника: начало и конец рабочего дня. . . .
Часы с будильником на основе класса QLCDNumber
Uhbif79 12.08.2026
Всем добрый день, выкладываю программу часов с будильником на основе класса QLCDNumber. Здесь я пробовал самостоятельно создавал классы, впервые столкнулся с видимостью переменной одного класса из. . .
Установка MinGW GCC 16.2 и CMake
8Observer8 10.08.2026
VK Видео: https:/ / vkvideo. ru/ video-240781534_456239017 YouTube: eY5-5PyI9NM Текстовая версия
Неделя из жизни имитационной модели склада: мои кривые руки растут, откуда надо
anaschu 10.08.2026
Неделя из жизни имитационной модели склада: как я почти написал неправильную логику и что с этим делать Работаю сейчас над учебно-рабочим проектом: строю в AnyLogic имитационную модель процессов. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru