Українська
Обмеження, антипатерни й налагодження
Обмеження кількості одночасних операцій
Task.WhenAll для тисячі задач запускає тисячу операцій одночасно. Для файлів це тисяча відкритих дескрипторів, для мережі – тисяча з’єднань, які сервер може вважати атакою. Кількість одночасних операцій обмежують асинхронним семафором SemaphoreSlim (тема 3): метод WaitAsync() очікує вільне місце, не блокуючи потік. Кожна операція виконує await gate.WaitAsync(token) перед початком і gate.Release() у блоці finally, а всі задачі очікуються Task.WhenAll (приклад «Асинхронне читання файлів»).
Альтернатива для простих випадків – Parallel.ForEachAsync з параметром MaxDegreeOfParallelism (тема 6).
HttpClient
HttpClient призначений для багаторазового використання: один екземпляр на застосунок (або IHttpClientFactory), а не новий на кожен запит, інакше вичерпуються сокети. Властивість Timeout за замовчуванням дорівнює 100 с і діє на весь запит; індивідуальний таймаут окремого запиту задають токеном CancellationTokenSource з CancelAfter. Рекомендації: https://learn.microsoft.com/dotnet/fundamentals/networking/http/httpclient-guidelines.
У прикладах цього курсу HttpClient звертається лише до локального тестового сервера на HttpListener, запущеного в тій самій програмі, тому програми працюють без інтернету. Сам HttpListener Microsoft не рекомендує для нових розробок – справжні вебсервіси будують на ASP.NET Core (тема 14), але для навчального сервера з кількох рядків він зручний.
Антипатерни асинхронного коду
async void. Викликальник не може дочекатися такого методу, а виняток з нього не потрапляє вtry/catchвикликальника і завершує процес.async voidдопустимий лише для обробників подій; в усіх інших випадках метод повертаєTask.- Синхронне очікування асинхронного коду (sync-over-async):
GetDataAsync().Result,.Wait(),.GetAwaiter().GetResult(). Це блокує потік і в контексті синхронізації веде до взаємоблокування, а на сервері – до голодування пулу потоків (thread pool starvation): усі потоки пулу заблоковані й чекають на продовження, яким бракує потоків. - Асинхронне обгортання синхронного (async-over-sync) у бібліотеках: метод
Task<int> ComputeAsync() => Task.Run(Compute)вводить в оману, бо виглядає як I/O-операція.Task.Runвирішує викликальник. - «Забуті» задачі (fire-and-forget):
DoWorkAsync();безawait. Помилка стає незаспостереженою, а програма може завершитися раніше за задачу. Якщо задачу справді не чекають, це позначають відкиданням_ = DoWorkAsync();і обробляють винятки всередині. Thread.Sleepв асинхронному методі блокує потік пулу; замість нього –await Task.Delay.- Спільний стан без синхронізації. Продовження після
awaitможуть виконуватися паралельно на різних потоках, тому зміна спільного лічильника з кількох задач потребуєInterlockedабо блокування (тема 3).lockне може міститиawait; для асинхронного взаємного виключення використовуютьSemaphoreSlim(1, 1).
Налагодження задач у Rider
Вікно налагодження Rider (Debug) має кілька вкладок для багатопотокових і асинхронних програм (https://www.jetbrains.com/help/rider/Debugging_Multithreaded_Applications.html):
- Threads & Variables – список потоків, стек вибраного потоку і змінні кадру;
- Parallel Stacks – діаграма стеків усіх потоків: однакові частини стеків об’єднано, тому видно, скільки потоків виконують той самий код;
- Tasks – стан об’єктів
TaskіValueTaskу точці зупину: ідентифікатор, статус, метод.
Щоб побачити очікування, програму призупиняють кнопкою Pause Program під час роботи прикладу з кількома завантаженнями (рис. 5.8). Асинхронний метод, який чекає, не займає потоку, тому у вкладці Threads & Variables його не видно – його показує вкладка Tasks і асинхронні ланцюжки викликів.
Знімок екрана
Rider: Debug the local HttpClient download example, Pause Program while downloads wait; Debug window, Parallel Stacks tab (and Tasks tab) with several awaiting method chains
Рис. 5.8. Паралельні стеки та задачі в Rider