Lab 21
Async / Await
async/await, Task.WhenAll, CancellationToken, IProgress<T>
Лаба 21 — Async / Await
Мета
Навчитися писати асинхронний код: async/await з EF Core, скасування через CancellationToken, паралельне виконання задач (Task.WhenAll, Task.WhenAny, Parallel.ForEachAsync), обробку помилок паралельних задач, звітування про прогрес (IProgress<T>) і асинхронні HTTP-запити.
Контекст
У Лабах 17–20 код був синхронним навмисно — щоб було видно, що відбувається:
var patients = context.Patients.ToList(); // потік ЗАБЛОКОВАНИЙ ~50 мс
var doctors = context.Doctors.ToList(); // потік ЗАБЛОКОВАНИЙ ~50 мсТепер уявіть 100 одночасних запитів у вебзастосунку: кожен блокує потік, пул потоків вичерпується, нові запити стають у чергу — застосунок «провисає». Рішення — async/await: не блокувати потік, а звільняти його на час очікування введення-виведення.
var patients = await context.Patients.ToListAsync(); // потік ВІЛЬНИЙ під час очікуванняСтруктура проєкту на початку лаби
Це результат Лаби 20 — стан main після її злиття:
oop-course/ ← гілка main (після злиття Лаби 20)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs
├── Clinic.cs
├── Enums/ (4 файли)
├── Models/ (19 файлів)
├── Managers/ (13 файлів)
├── Utils/ (12 файлів)
├── Interfaces/ (4 файли)
├── Comparators/ (4 файли)
├── Attributes/ (3 файли)
├── Events/ (4 файли)
├── Extensions/ (3 файли)
├── UI/ (1 файл)
├── Data/
│ ├── ClinicDbContext.cs
│ ├── DbSeeder.cs
│ ├── ClinicRepository.cs
│ └── ClinicQueryService.cs
└── Migrations/ (9 файлів — генерує EF)Структуру наприкінці лаби (з позначками, що створюється і змінюється в кожній задачі) наведено в розділі «Структура проєкту наприкінці лаби» перед перевіркою.
Ключові поняття
async/await.async— «метод може міститиawait»;await— «призупини метод, звільни потік, продовж, коли задача завершиться». Типи результату:void→async Task,T→async Task<T>.async voidдопустимий лише для обробників подій: його виняток неможливо перехопити ззовні, і на нього неможливо чекати.CancellationToken— сигнал ззовні «скасуй операцію». При скасуванні кидаєтьсяOperationCanceledException— його ловлять окремо від інших винятків.Task.WhenAll— запустити кілька задач одночасно і дочекатися всіх: час ≈ найдовша, а не сума.Task.WhenAny— дочекатися першої (гонка, таймаут).Parallel.ForEachAsync— паралельна обробка колекції з обмеженням кількості одночасних задач.DbContextне потокобезпечний. Два одночасні запити через один контекст кидаютьInvalidOperationException. Паралельні запити до БД — кожен через свій контекст; паралельна обробка в пам'яті — лише над уже завантаженими об'єктами.AggregateException.await Task.WhenAll(...)при кількох помилках кидає лише першу; усі доступні черезException.InnerExceptionsсамої задачі.IProgress<T>— спосіб повідомляти про прогрес, не знаючи, хто і як його показує.
Що нового дозволено (і тільки воно)
async/await,Task,Task<T>, async-методи EF Core (ToListAsync,CountAsync,SaveChangesAsync…);CancellationToken,CancellationTokenSource;Task.WhenAll,Task.WhenAny,Task.Delay,Parallel.ForEachAsync,Interlocked;ContinueWith;IProgress<T>/Progress<T>;HttpClient,System.Net.Http.Json,[JsonPropertyName].
Крок 1. Гілка
Робочий процес (повністю — Git Воркшоп): лаба = гілка
Lab-XXвідmain, коміт на кожне завдання (LabXX TaskYY), у кінці — злиття вmain.
Проєкт ClinicApp/ уже існує. Тут лише нова гілка від main:
git checkout main
git checkout -b Lab-21Коміт — на кожне завдання (Lab21 TaskNN).
Ваш домен
За замовчуванням виконуйте завдання як написано (домен «клініка»). Для власного домену дивіться таблицю «Адаптація до вашого домену» в кінці кожного завдання.
Як користуватися підказками
Підказки — напрям думки, не готовий код. «Що реалізувати» і «Специфікація» кажуть що; підказки — як міркувати; блок 📖 Документація — де прочитати синтаксис. Спершу документація і власна спроба.
Задача 1. Асинхронний сідер ⭐
Умова
Побачте, як async/await поширюється від точки входу вниз по стеку викликів: переведіть сідер на асинхронні методи EF.
Що реалізувати:
- У
DbSeederдодатиSeedAsync(ClinicDbContext context, CancellationToken ct = default). - Кожен приватний крок перевести на асинхронний (
SeedPatients→SeedPatientsAsyncтощо):Any()→AnyAsync(ct),SaveChanges()→SaveChangesAsync(ct). - У
Program.csвикликатиawait DbSeeder.SeedAsync(...).
Специфікація
| Було | Стало |
|---|---|
static void Seed(ClinicDbContext context) |
static async Task SeedAsync(ClinicDbContext context, CancellationToken ct = default) |
context.Patients.Any() |
await context.Patients.AnyAsync(ct) |
context.SaveChanges() |
await context.SaveChangesAsync(ct) |
Приклад
await DbSeeder.SeedAsync(context); // у Program.cs, прямо на верхньому рівніПідказки
Program.csз top-level statements: щойно в ньому з'являєтьсяawait, компілятор сам створюєasync Task Main.CancellationToken ct = default— «токен, який ніколи не скасовується»: викликати можна і без нього.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
SeedPatientsAsync, SeedDoctorsAsync … |
SeedGuestsAsync … |
SeedCustomersAsync … |
SeedStudentsAsync … |
SeedClientsAsync … |
SeedReadersAsync … |
SeedMembersAsync … |
Коміт
git add ClinicApp/Data/DbSeeder.cs ClinicApp/Program.cs
git commit -m "Lab21 Task01"Задача 2. `AsyncClinicService`: асинхронні операції EF ⭐⭐
Умова
Створіть сервіс з асинхронними версіями типових операцій і додайте асинхронні варіанти в репозиторій.
Що реалізувати:
- Клас
AsyncClinicServiceуClinicApp/Data/з конструктором(ClinicDbContext context)і чотирма методами зі специфікації — коженawait-ить асинхронний метод EF. - У
ClinicRepositoryдодатиGetPatientWithAppointmentsAsyncіGetUpcomingAppointmentsAsync. - У методах репозиторію використати
ConfigureAwait(false)і пояснити в коментарі, навіщо.
Специфікація
| Клас | Метод | Повертає |
|---|---|---|
AsyncClinicService |
GetAllPatientsAsync(CancellationToken ct = default) |
Task<List<Patient>> |
AsyncClinicService |
GetPatientByIdAsync(int id, CancellationToken ct = default) |
Task<Patient?> |
AsyncClinicService |
GetUpcomingAppointmentsAsync(CancellationToken ct = default) |
Task<List<Appointment>> |
AsyncClinicService |
SaveAppointmentAsync(Appointment a, CancellationToken ct = default) |
Task<int> — кількість збережених рядків |
ClinicRepository |
GetPatientWithAppointmentsAsync(int id, CancellationToken ct = default) |
Task<Patient?> |
ClinicRepository |
GetUpcomingAppointmentsAsync(CancellationToken ct = default) |
Task<List<Appointment>> |
Приклад
var service = new AsyncClinicService(context);
Patient? p = await service.GetPatientByIdAsync(1);Підказки
- Токен передавайте далі в кожен асинхронний метод EF — інакше скасування до БД не дійде.
async-метод без жодногоawaitкомпілюється з попередженням і виконується синхронно..Resultзамістьawaitблокує потік і в застосунках ізSynchronizationContextможе призвести до взаємоблокування.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
AsyncClinicService |
AsyncHotelService |
AsyncRestaurantService |
AsyncUniversityService |
AsyncRentalService |
AsyncLibraryService |
AsyncGymService |
Коміт
git add ClinicApp/Data/AsyncClinicService.cs ClinicApp/Data/ClinicRepository.cs
git commit -m "Lab21 Task02"Задача 3. Паралельні задачі і скасування ⭐⭐⭐
Умова
Додайте в AsyncClinicService чотири методи, кожен демонструє окремий інструмент: паралельні запити, таймаут, паралельну обробку колекції і скасування. Покажіть їх роботу в Program.cs.
Що реалізувати:
record ClinicDashboardуClinicApp/Models/(поля нижче).GetDashboardAsync()— п'ять значень дашборда одночасно черезTask.WhenAll; кожен запит — через окремийClinicDbContext.GetDashboardWithTimeoutAsync(int timeoutMs)— гонка дашборда зTask.Delay(timeoutMs)черезTask.WhenAny; при таймауті —null.MarkAppointmentsAsPaidAsync(IEnumerable<int> ids, CancellationToken ct)— завантажити записи одним запитом, позначити оплаченими паралельно в пам'яті (Parallel.ForEachAsync, лічильник черезInterlocked.Increment), зберегти однимSaveChangesAsyncпісля циклу.SearchPatientsAsync(string query, CancellationToken ct)— пошук за прізвищем із штучною затримкоюTask.Delay(200, ct).- У
Program.csпоказати: дашборд, таймаут, оплату списку записів і скасування пошуку токеном зі строком 100 мс з перехопленнямOperationCanceledException.
Специфікація
ClinicDashboard |
Як рахується |
|---|---|
PatientCount |
CountAsync пацієнтів |
DoctorCount |
CountAsync лікарів |
TotalRevenue |
сума GetCost() оплачених записів — у пам'яті, після завантаження |
UpcomingCount |
записи в майбутньому зі статусом Scheduled |
TodayCount |
записи на сьогодні |
Приклад
Дашборд: пацієнтів 5, лікарів 5, виручка 1230.00 грн, майбутніх 2, сьогодні 1
Дашборд з таймаутом 1 мс: таймаут
Оплачено записів: 3
Пошук скасовано (таймаут токена).Підказки
- Один контекст — одна операція за раз. Запустивши п'ять запитів через спільний контекст, отримаєте
InvalidOperationException(«A second operation was started on this context»). ДляGetDashboardAsyncстворюйте окремий контекст у кожній з п'яти задач (уusing). GetCost()не перекладається в SQL (Лаба 18):TotalRevenue— це окрема асинхронна функція, що завантажує оплачені записи і рахує суму в пам'яті.- Після
Task.WhenAllрезультати задач читайте черезawaitабо.Result— задачі вже завершені. - Програвша в
WhenAnyзадача не зупиняється сама — вона продовжує працювати у фоні. - У
Parallel.ForEachAsyncне звертайтесь доDbContext— лише до вже завантажених об'єктів.count++з кількох потоків губить збільшення — томуInterlocked.Increment.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
ClinicDashboard |
HotelDashboard |
RestaurantDashboard |
UniversityDashboard |
RentalDashboard |
LibraryDashboard |
GymDashboard |
Коміт
git add ClinicApp/Models/ClinicDashboard.cs ClinicApp/Data/AsyncClinicService.cs ClinicApp/Program.cs
git commit -m "Lab21 Task03"Задача 4. Помилки паралельних задач ⭐⭐⭐
Умова
Зберіть звіт про пацієнта з чотирьох паралельних задач і обробіть їхні помилки двома способами — побачте різницю між «першою помилкою» і «всіма помилками».
Що реалізувати:
- У
AsyncClinicService.csоголоситиrecord PatientReport(поля нижче). - Метод
BuildPatientReportAsync(int patientId)— чотири задачі паралельно, кожна через власний контекст. - Обробити помилки двома способами: продовження
ContinueWithзOnlyOnFaulted, що виводить усіInnerExceptions, іtry/catchнавколоawait, що отримує лише першу помилку. - Після завершення вивести стан кожної задачі (
IsCompletedSuccessfully,IsFaulted,IsCanceled). - У
Program.csвикликати звіт для існуючого і для неіснуючогоpatientId.
Специфікація
PatientReport |
Задача |
|---|---|
Patient |
пацієнт за Id (немає — виняток) |
RecentAppointments |
останні 5 записів пацієнта |
MedicalRecords |
медичні записи пацієнта |
Dashboard |
GetDashboardAsync() |
Приклад
Звіт для пацієнта #99:
[усі помилки] Пацієнта #99 не знайдено.
[перша помилка] Пацієнта #99 не знайдено.
Задача пацієнта: Faulted; записів: RanToCompletion; медкартки: RanToCompletion; дашборда: RanToCompletionПідказки
await Task.WhenAll(...)розгортаєAggregateExceptionі кидає лише першу з помилок — так зручніше в типовому коді.- Щоб побачити всі помилки, потрібна сама задача
Task.WhenAll(...)(неawait), а їїException.InnerExceptions— у продовженні. - Скасована задача має стан
IsCanceled, а неIsFaulted.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
PatientReport |
GuestReport |
CustomerReport |
StudentReport |
ClientReport |
ReaderReport |
MemberReport |
Коміт
git add ClinicApp/Data/AsyncClinicService.cs ClinicApp/Program.cs
git commit -m "Lab21 Task04"Задача 5. Прогрес через `IProgress` ⭐⭐
Умова
Масова зміна статусу записів триває довго. Нехай метод повідомляє про прогрес, не знаючи, де і як його покажуть, і підтримує скасування.
Що реалізувати:
- У
AsyncClinicServiceметодBulkProcessAppointmentsAsync(сигнатура нижче): для кожного запису змінює статус, звітує про прогрес, перевіряє скасування, імітує обробку затримкою 80 мс; повертає кількість оброблених. - У
Program.csвикликати його зProgress<T>, що виводить[i/N] повідомлення. - (За бажанням) показати той самий прогрес через
AnsiConsole.Progress()зі Spectre.Console (Лаба 16).
Специфікація
Task<int> BulkProcessAppointmentsAsync(
AppointmentStatus newStatus,
IProgress<(int Current, int Total, string Message)>? progress = null,
CancellationToken ct = default)| На кожній ітерації | |
|---|---|
ct.ThrowIfCancellationRequested() |
явна перевірка скасування |
await Task.Delay(80, ct) |
імітація обробки |
progress?.Report((i + 1, total, опис запису)) |
звіт про прогрес; без progress — нічого |
Приклад
[1/4] Запис #1 → Completed
[2/4] Запис #2 → Completed
[3/4] Запис #3 → Completed
[4/4] Запис #4 → Completed
Оброблено: 4Підказки
progress?.Report(...)— безпечний виклик: метод працює і без того, хто стежить за прогресом.Progress<T>викликає обробник черезSynchronizationContext. У консолі його немає — обробник виконується в пулі потоків, тож рядки можуть з'явитися з невеликою затримкою.- Зміни статусу збережіть одним
SaveChangesAsyncпісля циклу.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
BulkProcessAppointmentsAsync |
BulkProcessBookingsAsync |
BulkProcessReservationsAsync |
BulkProcessEnrollmentsAsync |
BulkProcessRentalsAsync |
BulkProcessLoansAsync |
BulkProcessSessionsAsync |
Коміт
git add ClinicApp/Data/AsyncClinicService.cs ClinicApp/Program.cs
git commit -m "Lab21 Task05"Задача 6. `ClinicHttpClient`: асинхронні HTTP-запити і JSON ⭐⭐⭐
Умова
Отримайте довідку про препарат із публічного FDA Open API (реєстрація не потрібна) — асинхронно, з десеріалізацією JSON і коректною обробкою мережевих помилок і таймаутів.
Що реалізувати:
- Клас
ClinicHttpClientуClinicApp/Data/з одним статичнимHttpClient(таймаут 10 с). - У тому самому файлі — публічний
record DrugInfoі приватніrecord-и для відповіді API з[JsonPropertyName]. - Три методи зі специфікації.
- Обробка помилок: мережеву помилку і таймаут
HttpClient— перехопити (повернутиnull); скасування через переданий токен — не перехоплювати. - У
Program.csзапитати довідку проAspirin,Ibuprofen,Omeprazole.
Специфікація
Запит: GET https://api.fda.gov/drug/label.json?search=openfda.brand_name:{назва}&limit=1
| Метод | Повертає |
|---|---|
GetDrugInfoAsync(string drugName, CancellationToken ct = default) |
Task<DrugInfo?> — null, якщо не знайдено або помилка мережі/таймаут |
IsApiAvailableAsync(CancellationToken ct = default) |
Task<bool> |
GetDrugInfoWithRaceAsync(string drugName, int timeoutMs) |
Task<DrugInfo?> — гонка запиту з Task.Delay(timeoutMs); програв запит — null |
DrugInfo |
З поля відповіді |
|---|---|
Name |
openfda.brand_name[0] |
Purpose |
purpose[0] |
Warnings |
warnings[0] |
Dosage |
dosage_and_administration[0] |
| Виняток | Що робити |
|---|---|
HttpRequestException |
мережева помилка — повернути null |
TaskCanceledException, коли ct не скасовано |
таймаут HttpClient — повернути null |
OperationCanceledException від ct |
не перехоплювати |
Приклад
Aspirin: Pain reliever/fever reducer
Warnings: Reye's syndrome: Children and teenagers who have or are recovering…Підказки
- Один
HttpClientна застосунок. Створення й знищенняHttpClientна кожен запит вичерпує сокети ОС — тому полеstatic readonly, хочHttpClientі реалізуєIDisposable. GetFromJsonAsync<T>= GET + десеріалізація JSON одним викликом (using System.Net.Http.Json;).- Назву препарату в URL екрануйте
Uri.EscapeDataString. - Розрізнити таймаут
HttpClientі скасування токеном допомагає фільтр виняткуwhen (!ct.IsCancellationRequested).
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
| FDA: препарати | API курсу валют | API рецептів | API розкладу | API довідника авто | Open Library API | API вправ |
Коміт
git add ClinicApp/Data/ClinicHttpClient.cs ClinicApp/Program.cs
git commit -m "Lab21 Task06"Структура проєкту наприкінці лаби
Так має виглядати ClinicApp/, коли всі завдання виконано:
oop-course/ ← гілка Lab-21 (після злиття — main)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs ✏ Т1 Т3 Т4 Т5 Т6
├── Clinic.cs
├── Enums/ (4 файли)
├── Models/
│ ├── ClinicDashboard.cs 🆕 Т3
│ └── … ще 19 файлів без змін
├── Managers/ (13 файлів)
├── Utils/ (12 файлів)
├── Interfaces/ (4 файли)
├── Comparators/ (4 файли)
├── Attributes/ (3 файли)
├── Events/ (4 файли)
├── Extensions/ (3 файли)
├── UI/ (1 файл)
├── Data/
│ ├── ClinicDbContext.cs
│ ├── DbSeeder.cs ✏ Т1
│ ├── ClinicRepository.cs ✏ Т2
│ ├── ClinicQueryService.cs
│ ├── AsyncClinicService.cs 🆕 Т2 ✏ Т3 Т4 Т5
│ └── ClinicHttpClient.cs 🆕 Т6
└── Migrations/ (9 файлів — генерує EF)Легенда: 🆕 — новий файл · ✏ — змінено вміст · Тn — номер задачі, у якій ви працюєте з файлом. Файли без позначки лишились такими, як були після Лаби 20.
Назви файлів наведено для домену «клініка»; у власному домені назви ваші — важливо, що саме створюється й змінюється.
Перевірка перед здачею
dotnet build ClinicApp
dotnet run --project ClinicAppПереконайтесь, що:
- Структура проєкту збігається зі схемою вище
- Сідер працює асинхронно; у
Program.cs—await DbSeeder.SeedAsync(...) -
GetDashboardAsyncповертає всі п'ять значень безInvalidOperationExceptionпро паралельні операції - Дашборд із таймаутом 1 мс повертає
null - Пошук із токеном на 100 мс завершується
OperationCanceledException, програма не падає - Звіт для неіснуючого пацієнта виводить помилку обома способами і стани задач
- Прогрес виводиться рядками
[i/N] … - Довідка FDA для трьох препаратів (без мережі —
nullбез падіння) - У коді немає
async void(окрім обробників подій) і.Resultна незавершених задачах
Питання для самоперевірки
- Deadlock через
.Result. ЧомуGetPatientsAsync().Resultу WinForms або старому ASP.NET може «зависнути»? Яку роль тут граютьSynchronizationContextіConfigureAwait(false)? Чому в ASP.NET Core цього немає? - «Async всю дорогу». Чому не можна зробити один метод асинхронним, а його виклик лишити синхронним через
.Result? DbContextі потоки. Чому паралельні запити через один контекст небезпечні? Як це вирішують у реальних застосунках (IDbContextFactory<T>)?Task.WhenAllчиParallel.ForEachAsync. Що буде зTask.WhenAll(items.Select(ProcessAsync))для 10 000 елементів? НавіщоMaxDegreeOfParallelism?IProgress<T>чи подія. У чому перевагаIProgress<T>перед подією для прогресу?- Вичерпання сокетів. Що відбувається на рівні ОС, якщо 1000 разів за секунду створювати і знищувати
HttpClient? Як це вирішуєIHttpClientFactory?
Статус гілки
Після всіх 6 завдань (кожне — окремий коміт Lab21 TaskNN на гілці Lab-21):
git push -u origin Lab-21
git checkout main
git merge --no-ff Lab-21 -m "Merge Lab-21: Async/Await"
git pushНаступна лаба:
git checkout main→git checkout -b Lab-22.